From cb633da9a676c28d5f5062aa8c4e431458b2c241 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 19:04:37 +0530 Subject: [PATCH 01/43] docs: stop a dead review subagent stalling the self-review loop The loop required a fresh subagent reviewer but said nothing about what to do when that reviewer never comes back. Every failure mode (a declined permission prompt, an internal error, a killed background task) ended the same way: the loop stalled silently, or worse, quietly degraded to an inline self-review that then counted as a completed round. That degradation is the expensive part. On PR #1156 two inline rounds reported clean and the first real subagent round that followed found ten findings, three of them genuine bugs in code those rounds had already passed. An inline pass is worse than no review because it looks like one. So: harness status is the only liveness signal (an agent's own prose never is), a failed spawn means the round did not happen, an inline pass is never a round, and a blocked reviewer stops the loop and is reported in the same turn rather than becoming an unreported wait. The reporting shape now says only a round a subagent completed counts toward the total. --- .claude/skills/webjs-start-work/SKILL.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 0fb19120d..89da8c344 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -314,7 +314,12 @@ Beyond review findings, proactively record the *reasoning* behind a PR as commen The draft PR is already open (step 6), so reviews post to it from the first round. Do NOT mark it ready for review or report "ready for merge" yet. Run rounds of self-review until ONE round finds zero new issues. Each round must: -1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. +1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. A foreground spawn returns its failure as a visible result; a backgrounded one can be declined, killed, or lost, leaving the loop sitting on a task that will never report. + + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. A spawn that was declined at the permission prompt, returned an internal error, was killed, or came back with an empty result means NOTHING is running, whatever the agent's last message said. An agent whose final text reads "I'm partway through the careful pass" while its harness status reads `killed` is not alive, and treating that prose as liveness is exactly what produces an unbounded wait. Confirm with `TaskList` (empty means nothing is in flight) and move on. Never poll it, never wait on it, never report a reviewer as still going on the strength of its own words. Three rules follow: + - **A failed spawn means the round did not happen.** Do not count it, do not report it, and do not let it advance the loop toward "last round clean". + - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. + - **Say so in the same turn.** State plainly that the review is blocked, what blocked it, and what the options are. Retry ONCE when the failure looks transient (an internal error, a timeout). A declined permission prompt is not transient, so do not re-spawn into the same refusal. A blocked reviewer stops the loop and becomes a question for the user, never a silent wait. In autonomous mode, where there is nobody to ask, it still stops the loop: leave the PR as a draft, write the blockage into the PR body, and report it. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. **Working-tree safety (non-negotiable).** Review subagents share THIS session's working directory. A reviewer that runs `git checkout` / `switch` / `reset` / `stash` / `rebase` silently moves the main session's HEAD off the branch (it has happened: a reviewer ran `git checkout main` mid-loop and the local checkout regressed; commits were safe only because they were already pushed). Two defenses, apply BOTH: - Spawn the reviewer with `isolation: "worktree"` so any git op it runs is contained in its own throwaway worktree, never the main checkout. (A read-only reviewer needs no shared state; `gh pr diff` reads from GitHub.) @@ -358,6 +363,8 @@ After the loop converges, report exactly this shape to the user: > PR # is up at . Self-review loop ran rounds; last round clean. Issues found and fixed during the loop: . Out-of-scope findings filed as follow-ups: . Ready to merge. +**Only a round that a fresh subagent actually completed counts toward ``.** A spawn that was declined, errored, was killed, or returned nothing did not produce a round, so it can be neither the clean last round nor part of the total, and an inline pass you did yourself is not a round at all (see the liveness rules in step 1). A loop whose reviewer never ran has run ZERO rounds, and the honest report there is that the review is blocked and why, not a count and not "ready to merge". Never write this shape from rounds you cannot point to a subagent result for. + If you cannot honestly say "last round clean", you cannot say "ready to merge". If a finding is rejected as a false positive, mention it in the report so the user can second-guess the rejection. Every finding the loop surfaced must be accounted for in this report as fixed, rejected-with-reason, or filed-with-issue-number; if you reported an out-of-scope finding to the user but cannot point to its issue number, you have not finished the loop. Every genuine finding must ALSO appear as a comment on the PR (see step 2), so the report and the PR agree. **Merge is gated on green CI, enforced at the branch level, not by trust.** A PR must not merge until all CI checks pass. `main` branch protection requires the five `ci.yml` checks (Conventions, Unit+integration, Browser, E2E, Build) before any merge; if `gh api repos/webjsdev/webjs/branches/main/protection` shows `required_status_checks: null`, run `bash scripts/protect-main.sh` once (needs repo admin) to restore it. Do not work around a red or pending check; wait for green. From 23559814de1ddbcd9c04c86b642be75fec430ee6 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 19:10:40 +0530 Subject: [PATCH 02/43] docs: fix the liveness check to name a mechanism that works TaskList is the shared to-do list, not a registry of running subagents, so "empty means nothing in flight" reads empty in any session that never called TaskCreate. It reports "dead" identically whether the reviewer is dead or running, which is worse than no check. Issue #1158 specified it, but the premise does not hold. The real signal for the mandated foreground spawn is the Agent call's own result in the same turn, and for a stray background agent it is the harness completion notification or TaskOutput on its name. Also drops the claim that backgrounding is what lets a spawn be declined (a refusal lands on the Agent call either way), and cross-references the new blocked path from the two downstream absolutes that said the loop can only ever end on a clean round. --- .claude/skills/webjs-start-work/SKILL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 89da8c344..8e3604e73 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -314,9 +314,9 @@ Beyond review findings, proactively record the *reasoning* behind a PR as commen The draft PR is already open (step 6), so reviews post to it from the first round. Do NOT mark it ready for review or report "ready for merge" yet. Run rounds of self-review until ONE round finds zero new issues. Each round must: -1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. A foreground spawn returns its failure as a visible result; a backgrounded one can be declined, killed, or lost, leaving the loop sitting on a task that will never report. +1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. A spawn that was declined at the permission prompt, returned an internal error, was killed, or came back with an empty result means NOTHING is running, whatever the agent's last message said. An agent whose final text reads "I'm partway through the careful pass" while its harness status reads `killed` is not alive, and treating that prose as liveness is exactly what produces an unbounded wait. Confirm with `TaskList` (empty means nothing is in flight) and move on. Never poll it, never wait on it, never report a reviewer as still going on the strength of its own words. Three rules follow: + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status comes from the harness completion notification or from `TaskOutput` on that agent's name, never from what it last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. Three rules follow: - **A failed spawn means the round did not happen.** Do not count it, do not report it, and do not let it advance the loop toward "last round clean". - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. - **Say so in the same turn.** State plainly that the review is blocked, what blocked it, and what the options are. Retry ONCE when the failure looks transient (an internal error, a timeout). A declined permission prompt is not transient, so do not re-spawn into the same refusal. A blocked reviewer stops the loop and becomes a question for the user, never a silent wait. In autonomous mode, where there is nobody to ask, it still stops the loop: leave the PR as a draft, write the blockage into the PR body, and report it. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. @@ -349,9 +349,9 @@ The draft PR is already open (step 6), so reviews post to it from the first roun The minimum is TWO rounds. A clean first round is rare and usually means the review was too shallow; if round 1 is clean, spawn a second one with a sharper, narrower focus before believing the result. -**The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. +**The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. The ONE case that ends the loop without a clean round is a reviewer you cannot spawn at all (see the liveness rules in step 1), and that ends it as BLOCKED, never as done. -**A standalone "review the PR" request IS the loop, not a one-shot.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop: spawn a fresh round, and if it finds anything, fix/reject/file it and run further rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. +**A standalone "review the PR" request IS the loop, not a one-shot.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop: spawn a fresh round, and if it finds anything, fix/reject/file it and run further rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. This entry point skips step 1's preamble, so read the liveness rules there before spawning: the sole reason to stop short of clean is a reviewer that cannot be spawned, which you report as blocked and never paper over with an inline pass of your own. ### When to skip the loop From 6b32c9e43c82c35a875bfe7d05419f298e07dbc5 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 19:16:09 +0530 Subject: [PATCH 03/43] docs: name a background reviewer's real status signal, not TaskOutput TaskOutput is deprecated and its own description says a local agent's result comes from the Agent tool result, not from the output file (which is the whole subagent transcript). Pointing at it by agent name was the same defect as the TaskList one it replaced: a named mechanism that does not do the job. The harness completion notification arrives on its own and carries the real result, so that is what the file names now. Also: the failed-spawn bullet said "do not report it" next to a bullet demanding you report the blockage, which reads as the exact silent stall this is meant to kill, so it now says do not report it AS A ROUND. And the standalone-review entry point now points at both halves of step 1, since it was sending readers to the liveness rules while skipping the working-tree-safety block that has actually corrupted a checkout. --- .claude/skills/webjs-start-work/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 8e3604e73..4c1e72a0c 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -316,8 +316,8 @@ The draft PR is already open (step 6), so reviews post to it from the first roun 1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status comes from the harness completion notification or from `TaskOutput` on that agent's name, never from what it last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. Three rules follow: - - **A failed spawn means the round did not happen.** Do not count it, do not report it, and do not let it advance the loop toward "last round clean". + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. Three rules follow: + - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". You still report the FAILURE itself, loudly, per the third bullet. - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. - **Say so in the same turn.** State plainly that the review is blocked, what blocked it, and what the options are. Retry ONCE when the failure looks transient (an internal error, a timeout). A declined permission prompt is not transient, so do not re-spawn into the same refusal. A blocked reviewer stops the loop and becomes a question for the user, never a silent wait. In autonomous mode, where there is nobody to ask, it still stops the loop: leave the PR as a draft, write the blockage into the PR body, and report it. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. @@ -351,7 +351,7 @@ The minimum is TWO rounds. A clean first round is rare and usually means the rev **The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. The ONE case that ends the loop without a clean round is a reviewer you cannot spawn at all (see the liveness rules in step 1), and that ends it as BLOCKED, never as done. -**A standalone "review the PR" request IS the loop, not a one-shot.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop: spawn a fresh round, and if it finds anything, fix/reject/file it and run further rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. This entry point skips step 1's preamble, so read the liveness rules there before spawning: the sole reason to stop short of clean is a reviewer that cannot be spawned, which you report as blocked and never paper over with an inline pass of your own. +**A standalone "review the PR" request IS the loop, not a one-shot.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop: spawn a fresh round, and if it finds anything, fix/reject/file it and run further rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each round), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be spawned, which you report as blocked and never paper over with an inline pass of your own. ### When to skip the loop From bf3a7e9cba6ba6f23036bb7411f672cf2738cc92 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 19:22:52 +0530 Subject: [PATCH 04/43] docs: close the paths that still stall or pass a non-review as a round Tracing the four failure paths through the new wording found daylight in all of them. A spawn that dies after its isolated worktree exists is the likeliest leaker of a stray worktree, and declaring a failed spawn "not a round" had quietly excused it from the repo-health check that fires after each round. The check now keys off the spawn. A killed BACKGROUND agent was routed straight to a blocked loop even though the mandated foreground reviewer had never been tried, so a perfectly reviewable PR could halt on evidence about an agent that was never the reviewer. It now writes that agent off and spawns the real one. A reviewer can return successfully having reviewed nothing ("I could not fetch the diff"), which was neither a finding list nor a literal CLEAN and hit no branch at all. That is now explicitly a failed round, because absence of findings is not a clean round. Retry is pinned: one per round, mechanical failures only, and a fresh round is not a way to buy a second retry. The durable record (draft PR plus the blockage in the body) now applies in every mode rather than only autonomously, since the interactive mode had the weaker record. Failure handling gains the blocked-reviewer case, and the spawned prompt template no longer justifies its git prohibition with a shared working directory the mandated isolation makes false; worktrees sharing one .git directory is the true reason and it survives isolation. --- .claude/skills/webjs-start-work/SKILL.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 4c1e72a0c..945503b3d 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -316,15 +316,17 @@ The draft PR is already open (step 6), so reviews post to it from the first roun 1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. Three rules follow: - - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". You still report the FAILURE itself, loudly, per the third bullet. + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. Four rules follow: + - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". You still report the FAILURE itself, loudly, per the fourth bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. + - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). That is neither a finding list nor the literal `CLEAN` the prompt asks for, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened. Re-spawn once with the diff or context it said it was missing, and if it comes back empty-handed again, the loop is blocked and you say so. - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. - - **Say so in the same turn.** State plainly that the review is blocked, what blocked it, and what the options are. Retry ONCE when the failure looks transient (an internal error, a timeout). A declined permission prompt is not transient, so do not re-spawn into the same refusal. A blocked reviewer stops the loop and becomes a question for the user, never a silent wait. In autonomous mode, where there is nobody to ask, it still stops the loop: leave the PR as a draft, write the blockage into the PR body, and report it. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. + - **Say so in the same turn.** State plainly that the review is blocked, what blocked it, and what the options are. Whatever the mode, leave the PR a DRAFT and write the blockage into the PR body, so the record lives on the PR and not only in a chat transcript that nobody reading the PR later can see. Then, interactively, it becomes a question for the user and never a silent wait; in autonomous mode, where there is nobody to ask, the loop still stops and you report it. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. + - **Retry ONCE per round, and only for a mechanical failure.** Transient means the spawn machinery broke: an internal error, a timeout, a returned-without-reviewing result. Everything else is terminal on the first hit, most of all a declined permission prompt, which will only be declined again. One retry per round is the whole budget: if the retry fails, that round is blocked, so do not roll a fresh round to buy another retry. ("Terminal" in the liveness paragraph above is about not WAITING on a dead agent; it does not forbid this one clean re-spawn.) **Working-tree safety (non-negotiable).** Review subagents share THIS session's working directory. A reviewer that runs `git checkout` / `switch` / `reset` / `stash` / `rebase` silently moves the main session's HEAD off the branch (it has happened: a reviewer ran `git checkout main` mid-loop and the local checkout regressed; commits were safe only because they were already pushed). Two defenses, apply BOTH: - Spawn the reviewer with `isolation: "worktree"` so any git op it runs is contained in its own throwaway worktree, never the main checkout. (A read-only reviewer needs no shared state; `gh pr diff` reads from GitHub.) - Include the read-only git prohibition in the prompt (it is baked into the template below). Belt and suspenders: isolation contains the damage, the prompt prevents the attempt. - - After EACH round returns, before acting on findings, run a one-line repo-health check and repair if needed (the worktree isolation in the first bullet has itself corrupted the main repo: spawning isolated reviewers flipped the main repo's `core.bare` to `true` and left a locked `.claude/worktrees/agent-*` worktree, after which `git checkout` failed with "this operation must be run in a work tree"). Check ALL of: + - After EACH spawn resolves, before acting on findings, run a one-line repo-health check. Every spawn, not every round: a spawn that was declined, errored, or died did not produce a round but may well have created a worktree first, so it needs the check MORE than a clean return does. Run it and repair if needed (the worktree isolation in the first bullet has itself corrupted the main repo: spawning isolated reviewers flipped the main repo's `core.bare` to `true` and left a locked `.claude/worktrees/agent-*` worktree, after which `git checkout` failed with "this operation must be run in a work tree"). Check ALL of: - `git rev-parse --is-inside-work-tree` is `true` and `git config --get core.bare` is NOT `true`. If the work tree is broken, repair with `git config core.bare false`, then `git worktree prune` (and `git worktree remove -f -f .claude/worktrees/agent-*` for any locked leftover). Do NOT touch the user's OWN unrelated worktrees (anything outside `.claude/worktrees/`). - `git rev-parse --abbrev-ref HEAD` is still the feature branch and `git status` is clean. If HEAD moved (or points at a now-deleted branch / `0000000`), `git checkout -f ` (or `main` post-merge) restores it. The merge / commits are always safe on GitHub regardless; this only repairs the LOCAL repo. Nothing is lost. If you cannot cleanly repair, `git checkout -f main && git pull` to resync, then continue. @@ -376,7 +378,7 @@ If you cannot honestly say "last round clean", you cannot say "ready to merge". ``` Review PR # at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). -HARD CONSTRAINT, read first: you are running in a SHARED working directory that the main session is actively using. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work). You do NOT need to switch branches to review: the branch is already checked out, so read files in place; use `gh pr diff ` and `gh pr view ` (which read from GitHub, not the local tree) for the diff and metadata. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. +HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review: the branch is already checked out, so read files in place; use `gh pr diff ` and `gh pr view ` (which read from GitHub, not the local tree) for the diff and metadata. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. You start with no prior context on this PR. Steps: @@ -442,4 +444,5 @@ A merge updates `main` and npm, but the four in-repo apps deployed to Railway (` - If `git status` is dirty: stop and ask the user to stash, commit, or abandon their pending work before creating the new branch. Never silently lose changes. - If the issue is already in `In progress` (someone else's work, or a prior branch left open): report this and ask the user whether to continue on the existing branch, branch off a fresh main, or pick a different issue. - If the local checkout regressed mid-loop (HEAD on `main` or a base commit, the feature branch's work seemingly "gone"): a review subagent mutated shared git state. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. +- If the review subagent cannot be spawned (declined at the permission prompt, an internal error, a spawn that dies, or one that returns having reviewed nothing): the round did not happen, so the loop is BLOCKED, which is its one exit that is not a clean round. Retry once for a mechanical failure, never into a declined prompt. Then leave the PR a draft, write the blockage into the PR body, say plainly in the same turn that the review did not run and why, and ask the user how to proceed. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are; only the flip to ready for review is withheld. Full rules in step 1 of the self-review loop. - If the `gh project item-edit` call fails (auth scope, missing field): report the failure clearly and offer to do the move manually via the web UI. The branch creation still stands. From edf3505dcf3565d2a56aab616a52850767a414d4 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 19:30:50 +0530 Subject: [PATCH 05/43] chore: allow the Agent tool so review spawns stop prompting A declined permission prompt is one of the four reviewer failures #1158 names, and it is the only one with a configuration fix rather than a prose one. The repo mandates the self-review loop for every agent that works it, so the permission that lets the loop run belongs in the repo's own committed settings next to the hooks that enforce the rest. Tool-level is the only granularity available, so this allows every Agent spawn rather than only reviewers. --- .claude/settings.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index edba8f7e2..b3bb3ff9e 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,4 +1,9 @@ { + "permissions": { + "allow": [ + "Agent" + ] + }, "hooks": { "UserPromptSubmit": [ { From f226f41a49a5f93be1b9c9b7aff8dc7642bd77b3 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 19:45:45 +0530 Subject: [PATCH 06/43] docs: order the blocked-reviewer rules and drop the stale duplicates Read top-down, step 1 told an agent to declare the loop blocked and edit the PR body before it ever reached the bullet granting a retry, so an internal error ended in a blocked PR while still being retryable. Retry now comes first and the ordering is stated outright. The bullets are also no longer referenced by ordinal, since the previous commit added two and left the lead-in saying four, which would have stopped a literal reader one bullet short of the retry rule itself. Three duplicates had drifted from the rules they restate. The working-tree block still opened with the SHARED working directory premise that the prompt template stopped using, the standalone-review pointer still summarized the repo-health check as per-round after it moved to per-spawn, and the Failure handling entry told an autonomous agent to ask the user how to proceed, which AGENTS.md forbids outright. Each now matches its source, and the blocked path no longer demotes a ready PR that another session opened. --- .claude/skills/webjs-start-work/SKILL.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 945503b3d..6d204463d 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -316,14 +316,14 @@ The draft PR is already open (step 6), so reviews post to it from the first roun 1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. Four rules follow: - - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". You still report the FAILURE itself, loudly, per the fourth bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. The rules below follow, and they apply IN THE ORDER WRITTEN: classify the failure, retry it if it earns a retry, and only then declare the loop blocked. + - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". You still report the FAILURE itself, loudly, per the say-so bullet below. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). That is neither a finding list nor the literal `CLEAN` the prompt asks for, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened. Re-spawn once with the diff or context it said it was missing, and if it comes back empty-handed again, the loop is blocked and you say so. - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. - - **Say so in the same turn.** State plainly that the review is blocked, what blocked it, and what the options are. Whatever the mode, leave the PR a DRAFT and write the blockage into the PR body, so the record lives on the PR and not only in a chat transcript that nobody reading the PR later can see. Then, interactively, it becomes a question for the user and never a silent wait; in autonomous mode, where there is nobody to ask, the loop still stops and you report it. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. - - **Retry ONCE per round, and only for a mechanical failure.** Transient means the spawn machinery broke: an internal error, a timeout, a returned-without-reviewing result. Everything else is terminal on the first hit, most of all a declined permission prompt, which will only be declined again. One retry per round is the whole budget: if the retry fails, that round is blocked, so do not roll a fresh round to buy another retry. ("Terminal" in the liveness paragraph above is about not WAITING on a dead agent; it does not forbid this one clean re-spawn.) + - **Retry ONCE per round, and only for a mechanical failure. Do this BEFORE declaring anything blocked.** Mechanical means the spawn machinery broke: an internal error, a timeout, a returned-without-reviewing result. Everything else is terminal on the first hit, most of all a declined permission prompt, which will only be declined again. One retry per round is the whole budget: if the retry fails, that round is blocked, so do not roll a fresh round to buy another retry. ("Terminal" in the liveness paragraph above is about not WAITING on a dead agent; it does not forbid this one clean re-spawn.) + - **Then say so in the same turn.** Once the retry is spent or the failure was terminal to begin with, state plainly that the review is blocked, what blocked it, and what the options are. Whatever the mode, write the blockage into the PR body so the record lives on the PR and not only in a chat transcript nobody reading the PR later can see, and withhold the flip to ready for review. If this session opened the PR, that means leaving it a draft; if you were asked to review a PR that is ALREADY ready (the standalone entry point below), leave its state alone and add the note, since demoting someone else's ready PR is not yours to do. Then, interactively, it becomes a question for the user and never a silent wait. In autonomous mode there is nobody to ask, so do NOT block on a question: report the blockage and stop the loop. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. - **Working-tree safety (non-negotiable).** Review subagents share THIS session's working directory. A reviewer that runs `git checkout` / `switch` / `reset` / `stash` / `rebase` silently moves the main session's HEAD off the branch (it has happened: a reviewer ran `git checkout main` mid-loop and the local checkout regressed; commits were safe only because they were already pushed). Two defenses, apply BOTH: + **Working-tree safety (non-negotiable).** A review subagent runs against the repository THIS session is using, and every worktree of a repo shares ONE `.git` directory, so its git writes reach the main checkout even from an isolated worktree. A reviewer that runs `git checkout` / `switch` / `reset` / `stash` / `rebase` silently moves the main session's HEAD off the branch (it has happened: a reviewer ran `git checkout main` mid-loop and the local checkout regressed; commits were safe only because they were already pushed). Two defenses, apply BOTH: - Spawn the reviewer with `isolation: "worktree"` so any git op it runs is contained in its own throwaway worktree, never the main checkout. (A read-only reviewer needs no shared state; `gh pr diff` reads from GitHub.) - Include the read-only git prohibition in the prompt (it is baked into the template below). Belt and suspenders: isolation contains the damage, the prompt prevents the attempt. - After EACH spawn resolves, before acting on findings, run a one-line repo-health check. Every spawn, not every round: a spawn that was declined, errored, or died did not produce a round but may well have created a worktree first, so it needs the check MORE than a clean return does. Run it and repair if needed (the worktree isolation in the first bullet has itself corrupted the main repo: spawning isolated reviewers flipped the main repo's `core.bare` to `true` and left a locked `.claude/worktrees/agent-*` worktree, after which `git checkout` failed with "this operation must be run in a work tree"). Check ALL of: @@ -353,7 +353,7 @@ The minimum is TWO rounds. A clean first round is rare and usually means the rev **The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. The ONE case that ends the loop without a clean round is a reviewer you cannot spawn at all (see the liveness rules in step 1), and that ends it as BLOCKED, never as done. -**A standalone "review the PR" request IS the loop, not a one-shot.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop: spawn a fresh round, and if it finds anything, fix/reject/file it and run further rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each round), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be spawned, which you report as blocked and never paper over with an inline pass of your own. +**A standalone "review the PR" request IS the loop, not a one-shot.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop: spawn a fresh round, and if it finds anything, fix/reject/file it and run further rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each SPAWN, including one that failed), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be spawned, which you report as blocked and never paper over with an inline pass of your own. ### When to skip the loop @@ -444,5 +444,5 @@ A merge updates `main` and npm, but the four in-repo apps deployed to Railway (` - If `git status` is dirty: stop and ask the user to stash, commit, or abandon their pending work before creating the new branch. Never silently lose changes. - If the issue is already in `In progress` (someone else's work, or a prior branch left open): report this and ask the user whether to continue on the existing branch, branch off a fresh main, or pick a different issue. - If the local checkout regressed mid-loop (HEAD on `main` or a base commit, the feature branch's work seemingly "gone"): a review subagent mutated shared git state. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. -- If the review subagent cannot be spawned (declined at the permission prompt, an internal error, a spawn that dies, or one that returns having reviewed nothing): the round did not happen, so the loop is BLOCKED, which is its one exit that is not a clean round. Retry once for a mechanical failure, never into a declined prompt. Then leave the PR a draft, write the blockage into the PR body, say plainly in the same turn that the review did not run and why, and ask the user how to proceed. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are; only the flip to ready for review is withheld. Full rules in step 1 of the self-review loop. +- If the review subagent cannot be spawned (declined at the permission prompt, an internal error, a spawn that dies, or one that returns having reviewed nothing): the round did not happen, so the loop is BLOCKED, which is its one exit that is not a clean round. Retry once FIRST for a mechanical failure, never into a declined prompt. Only once that retry is spent: write the blockage into the PR body, withhold the flip to ready for review (leaving it a draft if this session opened it), and say plainly in the same turn that the review did not run and why. Interactively, ask the user how to proceed; in autonomous mode do NOT block on a question, just report it and stop the loop. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. - If the `gh project item-edit` call fails (auth scope, missing field): report the failure clearly and offer to do the move manually via the web UI. The branch creation still stands. From 8eb522cbc6d7fbe7721f8c05189cf831ee20fef2 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 19:57:16 +0530 Subject: [PATCH 07/43] docs: give the reviewer a way to say it could not review The returns-without-reviewing rule assumed an output shape the prompt template forbade. The template sanctioned exactly two answers, a finding list or the literal CLEAN, so a reviewer that never fetched the diff had found nothing genuinely wrong and was steered straight into CLEAN, which the loop accepts as converged. The template now defines a BLOCKED sentinel for that case and says outright that reporting CLEAN for a review you could not perform is the worst available outcome. Two rules that admitted the same non-round are fixed with it: the reporting shape tested for a completed call and a subagent result, both of which a non-review satisfies, and the per-round posting rule told a findless round to post a clean review object even when nothing was reviewed. Also: worktree isolation was described as containing every git op, which is the claim the shared .git premise replaces, so it now says isolation covers the working tree while the read-only prohibition covers the refs and config it cannot reach. The two retry paths were unified onto one budget, a recovered failure is reported in the turn rather than through the blocked-PR procedure, the reviewer prompt no longer assumes the branch is checked out locally, and a blocked review is reported to the user instead of being written onto the PR, since a spawn that could not run is session tooling and not a fact about the change. --- .claude/skills/webjs-start-work/SKILL.md | 25 ++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 6d204463d..d0e9e1c25 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -296,7 +296,7 @@ Use `event: "COMMENT"` (GitHub forbids APPROVE / REQUEST_CHANGES on your own PR) gh api graphql -f query='mutation($t:ID!){resolveReviewThread(input:{threadId:$t}){thread{isResolved}}}' -f t= ``` -**Every round repeats the whole flow.** Each round of the self-review loop, and each manual re-review the user asks for, is a NEW review object: a fresh `POST /pulls//reviews` carrying that round's summary and findings, followed by fix + reply + resolve for that round's threads. Never append a later round's findings into an earlier round's review, and never edit a prior finding to say it is fixed (reply instead). A round that finds nothing still posts a short summary review saying it is clean, with no inline comments. +**Every round repeats the whole flow.** Each round of the self-review loop, and each manual re-review the user asks for, is a NEW review object: a fresh `POST /pulls//reviews` carrying that round's summary and findings, followed by fix + reply + resolve for that round's threads. Never append a later round's findings into an earlier round's review, and never edit a prior finding to say it is fixed (reply instead). A round that REVIEWED and found nothing still posts a short summary review saying it is clean, with no inline comments. A round whose reviewer did not review (see step 1) posts nothing, because there is no round to summarize and a clean review object on the PR would be a lie. Banned prose glyphs (AGENTS.md invariant 11) apply to every comment, reply, and summary body, so keep them clean. @@ -310,6 +310,8 @@ Beyond review findings, proactively record the *reasoning* behind a PR as commen **What's worth capturing (judgement, not a checklist):** why an approach won over a credible alternative; an experiment tried and reverted, with the reason; a tradeoff accepted knowingly (a cold-start cost, a known-small race window left in, a documented edge case); a constraint or invariant discovered mid-work; anything you would want explained if you returned to the PR with no memory of the conversation. Skip the trivial: routine fixes, mechanical edits, anything the diff already makes obvious. The bar is "would a future agent be missing important context without this", not "log everything". When the PR body already covers a decision, a short comment is fine or skip it; do not duplicate the whole body into a comment. +**A PR carries exactly two kinds of content: the code change and the review rounds.** Everything on it (body, commits, review summaries, inline findings, context comments) must be meaningful data about one or the other. Session and harness machinery is NOT PR content and must never be posted there. Concretely, keep OFF the PR: a subagent that could not be spawned or died, a tool that errored or was declined, a retry, an interruption, how many turns something took, and above all your own process mistakes in running the PR (a stale body you then fixed, a mirror you forgot to sync, a mis-posted comment). Those are conversation, not record. The test: would this still matter to someone reading the PR in a year who has no idea which agent or session produced it? A design decision passes. A rejected alternative passes. A review finding passes. "My reviewer spawn was declined and I retried" does not, and neither does "I got this wrong earlier in the PR and then corrected it", which reads as noise around a diff that already shows the correction. Fix the mistake and move on; do not narrate it onto the PR. + ### How the loop works The draft PR is already open (step 6), so reviews post to it from the first round. Do NOT mark it ready for review or report "ready for merge" yet. Run rounds of self-review until ONE round finds zero new issues. Each round must: @@ -317,15 +319,15 @@ The draft PR is already open (step 6), so reviews post to it from the first roun 1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. The rules below follow, and they apply IN THE ORDER WRITTEN: classify the failure, retry it if it earns a retry, and only then declare the loop blocked. - - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". You still report the FAILURE itself, loudly, per the say-so bullet below. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). That is neither a finding list nor the literal `CLEAN` the prompt asks for, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened. Re-spawn once with the diff or context it said it was missing, and if it comes back empty-handed again, the loop is blocked and you say so. + - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". You still report the FAILURE itself, loudly, in the turn it happens: say the spawn failed and what you did next, whether that was the one retry or declaring the loop blocked. A failure that the retry recovers from is a sentence in your reply, not a blocked PR (the say-so bullet's PR-body procedure is for a round that stays blocked). Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. + - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing in exactly this case, so treat that sentinel as the signal. But do not rely on it alone: a reviewer that ignores the instruction and answers with anything that is neither a finding list nor a literal `CLEAN` is in the same state, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened, and it spends the SAME single retry the bullet below allows, not a second one. Feed that retry whatever the reviewer said it was missing (the diff, a path, the base branch). If it comes back empty-handed again, the round is blocked and you say so. - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. - **Retry ONCE per round, and only for a mechanical failure. Do this BEFORE declaring anything blocked.** Mechanical means the spawn machinery broke: an internal error, a timeout, a returned-without-reviewing result. Everything else is terminal on the first hit, most of all a declined permission prompt, which will only be declined again. One retry per round is the whole budget: if the retry fails, that round is blocked, so do not roll a fresh round to buy another retry. ("Terminal" in the liveness paragraph above is about not WAITING on a dead agent; it does not forbid this one clean re-spawn.) - - **Then say so in the same turn.** Once the retry is spent or the failure was terminal to begin with, state plainly that the review is blocked, what blocked it, and what the options are. Whatever the mode, write the blockage into the PR body so the record lives on the PR and not only in a chat transcript nobody reading the PR later can see, and withhold the flip to ready for review. If this session opened the PR, that means leaving it a draft; if you were asked to review a PR that is ALREADY ready (the standalone entry point below), leave its state alone and add the note, since demoting someone else's ready PR is not yours to do. Then, interactively, it becomes a question for the user and never a silent wait. In autonomous mode there is nobody to ask, so do NOT block on a question: report the blockage and stop the loop. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. + - **Then say so in the same turn.** Once the retry is spent or the failure was terminal to begin with, tell the USER plainly that the review is blocked, what blocked it, and what the options are. This one stays in the conversation: a spawn that could not run is session tooling, not a fact about the change, so it does NOT go into the PR body or a PR comment (the PR records the review, not the harness). What DOES change on the PR is that the flip to ready for review is withheld, since the loop never converged. Interactively the blockage then becomes a question for the user and never a silent wait. In autonomous mode there is nobody to ask, so do NOT block on a question: report it and stop the loop. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. **Working-tree safety (non-negotiable).** A review subagent runs against the repository THIS session is using, and every worktree of a repo shares ONE `.git` directory, so its git writes reach the main checkout even from an isolated worktree. A reviewer that runs `git checkout` / `switch` / `reset` / `stash` / `rebase` silently moves the main session's HEAD off the branch (it has happened: a reviewer ran `git checkout main` mid-loop and the local checkout regressed; commits were safe only because they were already pushed). Two defenses, apply BOTH: - - Spawn the reviewer with `isolation: "worktree"` so any git op it runs is contained in its own throwaway worktree, never the main checkout. (A read-only reviewer needs no shared state; `gh pr diff` reads from GitHub.) - - Include the read-only git prohibition in the prompt (it is baked into the template below). Belt and suspenders: isolation contains the damage, the prompt prevents the attempt. + - Spawn the reviewer with `isolation: "worktree"` so its WORKING TREE is its own throwaway checkout and a stray `git checkout` cannot move the files under this session. It does NOT wall off the repo: the worktree shares this repo's `.git`, so ref and config writes still land on the shared repository. (A read-only reviewer needs no shared state; `gh pr diff` reads from GitHub.) + - Include the read-only git prohibition in the prompt (it is baked into the template below). Belt and suspenders: isolation contains the working-tree half of the damage, the prompt prevents the attempt at the shared half that isolation cannot reach. - After EACH spawn resolves, before acting on findings, run a one-line repo-health check. Every spawn, not every round: a spawn that was declined, errored, or died did not produce a round but may well have created a worktree first, so it needs the check MORE than a clean return does. Run it and repair if needed (the worktree isolation in the first bullet has itself corrupted the main repo: spawning isolated reviewers flipped the main repo's `core.bare` to `true` and left a locked `.claude/worktrees/agent-*` worktree, after which `git checkout` failed with "this operation must be run in a work tree"). Check ALL of: - `git rev-parse --is-inside-work-tree` is `true` and `git config --get core.bare` is NOT `true`. If the work tree is broken, repair with `git config core.bare false`, then `git worktree prune` (and `git worktree remove -f -f .claude/worktrees/agent-*` for any locked leftover). Do NOT touch the user's OWN unrelated worktrees (anything outside `.claude/worktrees/`). - `git rev-parse --abbrev-ref HEAD` is still the feature branch and `git status` is clean. If HEAD moved (or points at a now-deleted branch / `0000000`), `git checkout -f ` (or `main` post-merge) restores it. @@ -336,7 +338,8 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. - Instructs it to look for: bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, stylistic problems against `AGENTS.md` and `CONVENTIONS.md` (root and per-package). - Asks for a numbered list with `file:line` references. Problems only, no suggestions. - - Ends with: "If you find nothing genuinely wrong, say exactly `CLEAN` and stop. Do not pad." + - Ends with: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop. Do not pad." Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. + - Tells it to say `BLOCKED` plus what it is missing if it CANNOT review (no diff, no access, wrong repo), instead of falling through to `CLEAN`. Without this the two sanctioned outputs are a finding list or `CLEAN`, so a reviewer that never saw the diff has found nothing genuinely wrong and reports itself clean, which is precisely the non-review the liveness rules must catch. 2. **For each finding the subagent reports**, do exactly ONE of these three. There is no fourth option, and "mention it and move on" is not allowed: - **Fix it** on the branch (commit + push to update the PR), OR @@ -365,7 +368,7 @@ After the loop converges, report exactly this shape to the user: > PR # is up at . Self-review loop ran rounds; last round clean. Issues found and fixed during the loop: . Out-of-scope findings filed as follow-ups: . Ready to merge. -**Only a round that a fresh subagent actually completed counts toward ``.** A spawn that was declined, errored, was killed, or returned nothing did not produce a round, so it can be neither the clean last round nor part of the total, and an inline pass you did yourself is not a round at all (see the liveness rules in step 1). A loop whose reviewer never ran has run ZERO rounds, and the honest report there is that the review is blocked and why, not a count and not "ready to merge". Never write this shape from rounds you cannot point to a subagent result for. +**Only a round in which a fresh subagent actually REVIEWED counts toward ``.** A spawn that was declined, errored, was killed, or returned nothing did not produce a round, so it can be neither the clean last round nor part of the total, and an inline pass you did yourself is not a round at all (see the liveness rules in step 1). Neither does a spawn that returned a result without reviewing: a completed call and a real subagent result are NOT the test, because a reviewer that says "I could not fetch the diff" satisfies both and reviewed nothing. The test is that it came back with findings or with the literal `CLEAN`. A loop whose reviewer never reviewed has run ZERO rounds, and the honest report there is that the review is blocked and why, not a count and not "ready to merge". If you cannot honestly say "last round clean", you cannot say "ready to merge". If a finding is rejected as a false positive, mention it in the report so the user can second-guess the rejection. Every finding the loop surfaced must be accounted for in this report as fixed, rejected-with-reason, or filed-with-issue-number; if you reported an out-of-scope finding to the user but cannot point to its issue number, you have not finished the loop. Every genuine finding must ALSO appear as a comment on the PR (see step 2), so the report and the PR agree. @@ -378,7 +381,7 @@ If you cannot honestly say "last round clean", you cannot say "ready to merge". ``` Review PR # at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). -HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review: the branch is already checked out, so read files in place; use `gh pr diff ` and `gh pr view ` (which read from GitHub, not the local tree) for the diff and metadata. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. +HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review. Use `gh pr diff ` and `gh pr view ` for the diff and metadata, and read any file at its PR-branch state with `gh api repos///contents/?ref= --jq .content | base64 -d`. All of those read from GitHub, so they work whether or not the branch exists locally, which matters because a PR you were asked to review may not be checked out here at all. If the branch does happen to be the one checked out, reading files in place is fine too. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. You start with no prior context on this PR. Steps: @@ -389,6 +392,8 @@ You start with no prior context on this PR. Steps: 5. Specifically check: . Report findings as a numbered list with file:line references. Problems only. No suggestions, no nits about style if the rule isn't enforceable. If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop. Do not pad with "looks good overall" or summaries. + +If you CANNOT review (you could not fetch the diff, you have no access to the repo or PR, the branch does not resolve), say exactly `BLOCKED` on its own line followed by one line naming what you are missing. Do NOT report `CLEAN` in that case: `CLEAN` means you looked and found nothing, and reporting it for a review you could not perform is the single worst outcome here, because it ends the loop on a review that never happened. ``` ## After a merge: decide on a version bump, automatically @@ -444,5 +449,5 @@ A merge updates `main` and npm, but the four in-repo apps deployed to Railway (` - If `git status` is dirty: stop and ask the user to stash, commit, or abandon their pending work before creating the new branch. Never silently lose changes. - If the issue is already in `In progress` (someone else's work, or a prior branch left open): report this and ask the user whether to continue on the existing branch, branch off a fresh main, or pick a different issue. - If the local checkout regressed mid-loop (HEAD on `main` or a base commit, the feature branch's work seemingly "gone"): a review subagent mutated shared git state. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. -- If the review subagent cannot be spawned (declined at the permission prompt, an internal error, a spawn that dies, or one that returns having reviewed nothing): the round did not happen, so the loop is BLOCKED, which is its one exit that is not a clean round. Retry once FIRST for a mechanical failure, never into a declined prompt. Only once that retry is spent: write the blockage into the PR body, withhold the flip to ready for review (leaving it a draft if this session opened it), and say plainly in the same turn that the review did not run and why. Interactively, ask the user how to proceed; in autonomous mode do NOT block on a question, just report it and stop the loop. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. +- If the review subagent cannot be spawned (declined at the permission prompt, an internal error, a spawn that dies, or one that returns having reviewed nothing): the round did not happen, so the loop is BLOCKED, which is its one exit that is not a clean round. Retry once FIRST for a mechanical failure, never into a declined prompt. Only once that retry is spent: withhold the flip to ready for review, and say plainly to the USER in the same turn that the review did not run and why. Keep this out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Interactively, ask the user how to proceed; in autonomous mode do NOT block on a question, just report it and stop the loop. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. - If the `gh project item-edit` call fails (auth scope, missing field): report the failure clearly and offer to do the move manually via the web UI. The branch creation still stands. From 0f7fb74a034375b46898793ad5ae7fd388029d07 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:01:18 +0530 Subject: [PATCH 08/43] docs: re-spawn a broken reviewer instead of interrupting the user The blocked path stopped the loop and asked the user how to proceed after a single retry. That is the wrong trade: a spawn that broke mechanically is harness noise, recovering costs seconds, and handing back a half-finished loop costs the review its whole momentum. The user asked for a converged review, not a status update. So a mechanical failure is now re-spawned until one takes, with the prompt adjusted when the reviewer named what it was missing, and varied rather than repeated after a few identical failures. Nothing about this relaxes the rule the block exists for: an inline pass is still never a substitute for keeping the loop moving. A reviewer that genuinely cannot be produced remains the one stopping point, it withholds ready-for-review, and it is reported once at the end rather than as a mid-loop interruption. --- .claude/skills/webjs-start-work/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index d0e9e1c25..dfe40b3f8 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -322,8 +322,8 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". You still report the FAILURE itself, loudly, in the turn it happens: say the spawn failed and what you did next, whether that was the one retry or declaring the loop blocked. A failure that the retry recovers from is a sentence in your reply, not a blocked PR (the say-so bullet's PR-body procedure is for a round that stays blocked). Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing in exactly this case, so treat that sentinel as the signal. But do not rely on it alone: a reviewer that ignores the instruction and answers with anything that is neither a finding list nor a literal `CLEAN` is in the same state, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened, and it spends the SAME single retry the bullet below allows, not a second one. Feed that retry whatever the reviewer said it was missing (the diff, a path, the base branch). If it comes back empty-handed again, the round is blocked and you say so. - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. - - **Retry ONCE per round, and only for a mechanical failure. Do this BEFORE declaring anything blocked.** Mechanical means the spawn machinery broke: an internal error, a timeout, a returned-without-reviewing result. Everything else is terminal on the first hit, most of all a declined permission prompt, which will only be declined again. One retry per round is the whole budget: if the retry fails, that round is blocked, so do not roll a fresh round to buy another retry. ("Terminal" in the liveness paragraph above is about not WAITING on a dead agent; it does not forbid this one clean re-spawn.) - - **Then say so in the same turn.** Once the retry is spent or the failure was terminal to begin with, tell the USER plainly that the review is blocked, what blocked it, and what the options are. This one stays in the conversation: a spawn that could not run is session tooling, not a fact about the change, so it does NOT go into the PR body or a PR comment (the PR records the review, not the harness). What DOES change on the PR is that the flip to ready for review is withheld, since the loop never converged. Interactively the blockage then becomes a question for the user and never a silent wait. In autonomous mode there is nobody to ask, so do NOT block on a question: report it and stop the loop. Autonomous mode auto-decides branches, rebases, and failing tests, not whether a review happened. + - **Re-spawn it. Do not interrupt the user over a failed spawn.** A spawn that broke mechanically (an internal error, a timeout, a returned-without-reviewing result) is harness noise, so just spawn it again, adjusting the prompt if the reviewer said what it was missing. Keep re-spawning until one takes. A handful of consecutive identical failures means something structural is wrong, so vary the approach (a smaller scope, a different focus, a fresh prompt) rather than firing the same call a tenth time. Recovering costs a few seconds; asking the user costs the whole loop its momentum. **Never** stop mid-loop to report a failed spawn, ask how to proceed, or hand back a half-finished loop. The user asked for a converged review, not a status update, and the loop is not done until a round comes back clean. + - **A genuinely unrecoverable reviewer is the only stopping point, and even that is reported at the end.** If re-spawning cannot produce a working reviewer at all, the loop has not converged, so withhold the flip to ready for review and say plainly what happened, once, when you report back. Keep it in the conversation: a spawn that could not run is session tooling, not a fact about the change, so it does NOT go into the PR body or a PR comment (the PR records the review, not the harness). What you must never do is substitute an inline pass to keep the loop moving, which is the failure this whole block exists to prevent. **Working-tree safety (non-negotiable).** A review subagent runs against the repository THIS session is using, and every worktree of a repo shares ONE `.git` directory, so its git writes reach the main checkout even from an isolated worktree. A reviewer that runs `git checkout` / `switch` / `reset` / `stash` / `rebase` silently moves the main session's HEAD off the branch (it has happened: a reviewer ran `git checkout main` mid-loop and the local checkout regressed; commits were safe only because they were already pushed). Two defenses, apply BOTH: - Spawn the reviewer with `isolation: "worktree"` so its WORKING TREE is its own throwaway checkout and a stray `git checkout` cannot move the files under this session. It does NOT wall off the repo: the worktree shares this repo's `.git`, so ref and config writes still land on the shared repository. (A read-only reviewer needs no shared state; `gh pr diff` reads from GitHub.) From a20475f288601d5fec971378dbad1574c12c9649 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:09:27 +0530 Subject: [PATCH 09/43] docs: keep the loop fast and stop restating the review-posting rules Two fixes that share a cause with everything else in this PR: a rule stated twice drifts, and a loop that stops to wait is a loop nobody runs. Closes #1157. A round is a reading pass over the diff and costs seconds, but the skill never said what NOT to run around one, so sessions re-ran multi-minute suites between rounds and then watched CI on top. Measured on #1156: a single 32-minute gap between two commits, spent running the Node and e2e suites twice each, none of which changed a finding and all of which CI re-ran anyway. Now only the touched test files run per fix, the suites and the CI read happen once after the last clean round, and the text says outright that this changes WHEN they run and never WHETHER, since an earlier attempt at this wording read as permission to skip a layer. Closes #1160. Step 2 of the loop restated the review-posting mechanics and had drifted from the authoritative section on both points: it sent findings through the standalone-comment API rather than one review object, used issue comments for round summaries, and had each finding carry its own disposition. All three are what the posting rules forbid, and the first is how a review on #1115 ended up ungrouped. Step 2 now defers to those rules instead of paraphrasing them, keeping only what is genuinely its own. A pointer cannot drift; a paraphrase will. --- .claude/skills/webjs-start-work/SKILL.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index dfe40b3f8..c9bad81c8 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -314,7 +314,14 @@ Beyond review findings, proactively record the *reasoning* behind a PR as commen ### How the loop works -The draft PR is already open (step 6), so reviews post to it from the first round. Do NOT mark it ready for review or report "ready for merge" yet. Run rounds of self-review until ONE round finds zero new issues. Each round must: +The draft PR is already open (step 6), so reviews post to it from the first round. Do NOT mark it ready for review or report "ready for merge" yet. Run rounds of self-review until ONE round finds zero new issues. + +**Keep the loop fast. A round is a reading pass over the diff and should cost seconds, so nothing slow belongs between rounds.** Two rules, and both change WHEN work happens, never WHETHER: + +- **No full test suite inside the loop.** After a fix, run only the specific test file(s) covering the line you just changed (`node --test `), which is also what the commit rule needs (a logical unit commits when its own tests pass). Do NOT run the e2e suite, the full Node suite, the browser suite, the Bun matrix, or the four-app dogfood boot check between rounds. Those run ONCE, after the last clean round, and the Definition of done still requires every layer the change touches. This is a timing rule, not permission to skip a layer. +- **Never wait on CI between rounds.** Pushing is fire-and-forget: push, then start the next round immediately. No `gh pr checks --watch` mid-loop. Read CI once, after the last clean round. A round that finds something means another push anyway, which supersedes the run you would have been watching, and merge is gated on green CI regardless, so waiting mid-loop buys nothing. + +Each round must: 1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) @@ -346,11 +353,13 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **Reject it** explicitly with a one-sentence reason written in your reply to the user and in the PR body. Rejection has to be defensible (e.g. "the agent flagged X as a security issue but X runs server-side only and never reaches user input"). False positives are real; reject them on the merits, don't just hand-wave. OR - **File it** as a tracked issue when it is genuine but out of scope for THIS PR (a pre-existing bug, an unrelated dependency/hygiene problem, a separate feature). "Out of scope" is NOT a reason to drop a finding. INVOKE the `webjs-file-issue` skill rather than hand-rolling `gh issue create`: it gates on grounding the body in the real codebase first, which is exactly what an out-of-scope review finding needs and exactly what a hurried two-command version omits. Capture the issue number. Verify it landed (`gh project item-list 1 --owner webjsdev --limit 20000`, the board is past 200 items so the default cap of 30 reports a false negative). A finding you call out-of-scope without an issue number is an unfiled finding, which is the exact mistake this clause exists to prevent. If unsure whether something is in-scope, default to fixing it in this PR; only file-and-defer when it is clearly separable and fixing it here would mean scope creep. - **Record every genuine finding as a comment ON THE PR, the way a human reviewer would.** The self-review trail must live on the PR, not only in your reply to the user (a finding that exists only in the chat transcript is invisible to anyone reading the PR later). For findings tied to specific lines, post an INLINE review comment at `file:line` (`gh api repos/webjsdev/webjs/pulls//comments --input comment.json`, writing the body into that JSON file first, or a `gh pr review` with line comments). Build that JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference in its inline comments and had to be deleted and reposted. For cross-cutting or round-summary notes, use `gh pr comment --body-file /tmp/pr-comment.md`. Each comment states the finding, its `file:line`, and its disposition (`fixed in ` / `rejected because ` / `filed as #`). Post rejected findings and false positives too, so the reasoning is auditable on the PR. A `CLEAN` round can be noted briefly. Do this as part of the loop, not as an afterthought. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to PR comment bodies too, since they go through the same tooling.) + **Record every genuine finding ON THE PR, exactly the way a person reviewing on GitHub would.** The review trail must live on the PR, not only in your reply to the user, since a finding that exists only in the chat transcript is invisible to anyone reading the PR later. **Post it through the mechanics in `### Every PR review is posted ON the PR` and `### Follow the real review flow`, which are authoritative; do not improvise a different shape here.** In short: the round's summary and all its inline `file:line` findings go up as ONE review object, each finding states the problem only, and the disposition (`fixed in ` / `rejected because ` / `filed as #`) goes in a threaded reply, after which the thread is resolved. That is what a real reviewer does on GitHub, and it is the only shape that renders as a grouped review rather than scattered boxes. + + Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) 3. **If the round found any findings (even rejected ones)**, run another round with a fresh subagent. The new round picks a slightly different focus prompt: if round 1 was broad, round 2 zooms in on the file you most edited; if round 2 zoomed in, round 3 zooms out to cross-file consistency, etc. Rotate focus to avoid the agent rediscovering the same surface. -4. **If the round reports `CLEAN`**, the loop is done. +4. **If the round reports `CLEAN`**, the loop is done. NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. The minimum is TWO rounds. A clean first round is rare and usually means the review was too shallow; if round 1 is clean, spawn a second one with a sharper, narrower focus before believing the result. From 7c69315a5790420b9ae3984da26a36bca85d501b Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:13:00 +0530 Subject: [PATCH 10/43] docs: resync the three readers of the re-spawn rule Switching the blocked path from one-retry-then-ask to re-spawn-until-it- takes updated the rule and left three places still describing the old one: the failed-spawn bullet told you to report each failure in the turn it happened, the returns-without-reviewing bullet still budgeted a single shared retry, and the Failure handling entry still said to retry once and then ask the user how to proceed. That last one is the same drift this PR keeps finding, in the same file, from my own edit. --- .claude/skills/webjs-start-work/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index c9bad81c8..5fcfac801 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -326,8 +326,8 @@ Each round must: 1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. The rules below follow, and they apply IN THE ORDER WRITTEN: classify the failure, retry it if it earns a retry, and only then declare the loop blocked. - - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". You still report the FAILURE itself, loudly, in the turn it happens: say the spawn failed and what you did next, whether that was the one retry or declaring the loop blocked. A failure that the retry recovers from is a sentence in your reply, not a blocked PR (the say-so bullet's PR-body procedure is for a round that stays blocked). Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing in exactly this case, so treat that sentinel as the signal. But do not rely on it alone: a reviewer that ignores the instruction and answers with anything that is neither a finding list nor a literal `CLEAN` is in the same state, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened, and it spends the SAME single retry the bullet below allows, not a second one. Feed that retry whatever the reviewer said it was missing (the diff, a path, the base branch). If it comes back empty-handed again, the round is blocked and you say so. + - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the bullet below, and mention it only in the eventual wrap-up. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. + - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing in exactly this case, so treat that sentinel as the signal. But do not rely on it alone: a reviewer that ignores the instruction and answers with anything that is neither a finding list nor a literal `CLEAN` is in the same state, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened, so re-spawn per the bullet below, feeding it whatever the reviewer said it was missing (the diff, a path, the base branch). - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. - **Re-spawn it. Do not interrupt the user over a failed spawn.** A spawn that broke mechanically (an internal error, a timeout, a returned-without-reviewing result) is harness noise, so just spawn it again, adjusting the prompt if the reviewer said what it was missing. Keep re-spawning until one takes. A handful of consecutive identical failures means something structural is wrong, so vary the approach (a smaller scope, a different focus, a fresh prompt) rather than firing the same call a tenth time. Recovering costs a few seconds; asking the user costs the whole loop its momentum. **Never** stop mid-loop to report a failed spawn, ask how to proceed, or hand back a half-finished loop. The user asked for a converged review, not a status update, and the loop is not done until a round comes back clean. - **A genuinely unrecoverable reviewer is the only stopping point, and even that is reported at the end.** If re-spawning cannot produce a working reviewer at all, the loop has not converged, so withhold the flip to ready for review and say plainly what happened, once, when you report back. Keep it in the conversation: a spawn that could not run is session tooling, not a fact about the change, so it does NOT go into the PR body or a PR comment (the PR records the review, not the harness). What you must never do is substitute an inline pass to keep the loop moving, which is the failure this whole block exists to prevent. @@ -458,5 +458,5 @@ A merge updates `main` and npm, but the four in-repo apps deployed to Railway (` - If `git status` is dirty: stop and ask the user to stash, commit, or abandon their pending work before creating the new branch. Never silently lose changes. - If the issue is already in `In progress` (someone else's work, or a prior branch left open): report this and ask the user whether to continue on the existing branch, branch off a fresh main, or pick a different issue. - If the local checkout regressed mid-loop (HEAD on `main` or a base commit, the feature branch's work seemingly "gone"): a review subagent mutated shared git state. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. -- If the review subagent cannot be spawned (declined at the permission prompt, an internal error, a spawn that dies, or one that returns having reviewed nothing): the round did not happen, so the loop is BLOCKED, which is its one exit that is not a clean round. Retry once FIRST for a mechanical failure, never into a declined prompt. Only once that retry is spent: withhold the flip to ready for review, and say plainly to the USER in the same turn that the review did not run and why. Keep this out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Interactively, ask the user how to proceed; in autonomous mode do NOT block on a question, just report it and stop the loop. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. +- If the review subagent cannot be spawned (declined at the permission prompt, an internal error, a spawn that dies, or one that returns having reviewed nothing): the round did not happen. Re-spawn it, varying the approach after a few identical failures, and do NOT stop mid-loop to report the failure or ask how to proceed: recovering costs seconds and interrupting costs the loop its momentum. Only a reviewer that cannot be produced at all blocks the loop, which is its one exit that is not a clean round; then withhold the flip to ready for review and say once, at the end, that the review did not run and why. Keep that out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. - If the `gh project item-edit` call fails (auth scope, missing field): report the failure clearly and offer to do the move manually via the web UI. The branch creation still stands. From 65607f87e04aa656f910edd22585d35332449904 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:22:22 +0530 Subject: [PATCH 11/43] docs: shape the loop as a parallel fan-out plus delta verification Measured on this PR's own review: five sequential broad rounds cost 608k subagent tokens and 25 minutes, dominated not by local commands (5.6s of I/O per round) but by each reviewer ingesting a growing payload, and the prior-comments feed alone reached 171 KB by round 5. A parallel fan-out of four narrow lenses then covered more surface in one round's wall clock. So the loop now spawns round 1 as 3-4 narrow reviewers in parallel (correctness, internal consistency, issue satisfaction, mechanical), makes every later round delta-scoped on the fixes, and starves reviewers of context: the diff and touched files only, never prior PR comments, never an exclusion list, with duplicates deduped by the author for free. The same fan-out reviewed this change and its findings are fixed here too: the order lead-in referenced a retry distinction that no longer exists, two bullets pointed at "the bullet below" that was not below, the prompt-spec claimed the template ends with CLEAN when BLOCKED comes after it, the returns-without-reviewing bullet over-claimed the sentinel's coverage, the template never received the branch its spec promised, the reject path still wrote reasons into the PR body, the two blocked-loop summaries omitted the never-reviews case, ready-to-merge was announceable before the deferred suites and CI read, and the no-machinery-tells voice rule contradicted the body's required test plan and dogfood evidence. --- .claude/skills/webjs-start-work/SKILL.md | 36 ++++++++++++++---------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 5fcfac801..342bc7924 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -235,13 +235,13 @@ A finished PR is not just a diff. It carries four artifacts, and the PR is consi 3. **Context comments.** The reasoning from the working conversation that the diff and body do not capture, posted on the PR as the discussion happens (see "Capture significant design discussion as PR comments" below). The PR is the durable memory; the chat transcript is not. 4. **Review comments: a summary AND per-code-line comments.** Every review (each self-review round and any manual review) posts a summary review plus an inline comment on each finding's `file:line` (see "Every PR review is posted ON the PR" below). -All four are written in the owner's voice (first person, plain, no AI/agent framing, no machinery tells) and free of AGENTS.md invariant 11 banned glyphs. The sections below specify the mechanics for items 3 and 4. +All four are written in the owner's voice (first person, plain, no AI/agent framing) and free of AGENTS.md invariant 11 banned glyphs. The no-machinery-tells rule binds the review and context comments; the PR BODY is the one place machinery evidence is REQUIRED content (the test plan and the dogfood results the Definition of done demands), so reporting it there is not a tell. The sections below specify the mechanics for items 3 and 4. **Header every standalone comment with a short, meaningful bold heading** so a future reader (human or AI) knows what the comment is and what it is about before reading it. Put the heading on its own first line as bold markdown, blank line, then the body. Write the heading to fit THIS comment, do not pick from a fixed list. A good heading names the kind of comment and its topic, e.g. `**Design rationale: why analysis moved off boot, and what it costs**`, `**Review: lazy-boot model holds, one real bug**`, `**Decision: kept the derived gate over a declared allowlist**`, `**Follow-up: aliased-expose 404 filed as #N**`. A bare category word like `Context` or `Review` is the floor, not the goal; prefer a heading that also says the subject, so a reader scanning the PR's comment list can tell the boot-rationale note from the elision-review note without opening either. **Per-line inline review comments do NOT need a heading** because their `file:line` anchor already classifies them as review; keep those terse. The heading rule is for standalone, top-level comments (the PR body in item 2 is exempt, since it has its own `## Summary` structure). ## Pre-merge self-review loop (MUST run before reporting "ready for merge") -Saying "ready for merge" before the review loop completes is the single biggest source of low-quality PRs. The recurring pattern to AVOID: claim ready-for-merge, the user requests a review, find issues, fix them, claim ready-for-merge again, repeat 4-5 cycles before a review comes back clean. The cure is to run that loop internally BEFORE the first "ready" signal. The user should only hear "ready to merge" after the loop has converged. +Saying "ready for merge" before the review loop completes is the single biggest source of low-quality PRs. The recurring pattern to AVOID: claim ready-for-merge, the user requests a review, find issues, fix them, claim ready-for-merge again, repeat 4-5 cycles before a review comes back clean. The cure is to run that loop internally BEFORE the first "ready" signal. The user should only hear "ready to merge" after the loop has converged AND the suites the loop deferred have run AND CI has been read green. ### Every PR review is posted ON the PR (summary + per-line comments) @@ -321,13 +321,19 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **No full test suite inside the loop.** After a fix, run only the specific test file(s) covering the line you just changed (`node --test `), which is also what the commit rule needs (a logical unit commits when its own tests pass). Do NOT run the e2e suite, the full Node suite, the browser suite, the Bun matrix, or the four-app dogfood boot check between rounds. Those run ONCE, after the last clean round, and the Definition of done still requires every layer the change touches. This is a timing rule, not permission to skip a layer. - **Never wait on CI between rounds.** Pushing is fire-and-forget: push, then start the next round immediately. No `gh pr checks --watch` mid-loop. Read CI once, after the last clean round. A round that finds something means another push anyway, which supersedes the run you would have been watching, and merge is gated on green CI regardless, so waiting mid-loop buys nothing. +**Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-read the same surface N times; this shape reads it once, in parallel, then re-reads only what changed. + +- **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. +- **Every later round is delta-scoped: ONE reviewer on the fixes**, fed the fix commits' diff plus the paragraphs they touched, never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. +- **Starve reviewers of context they do not need.** Each gets the PR diff and the touched files, full stop. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. + Each round must: -1. **Spawn a fresh general-purpose subagent** (use the Agent tool with `subagent_type: "general-purpose"`, and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) +1. **Spawn fresh general-purpose subagents: the round-1 fan-out, or the single delta verifier** (the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: false`; a fan-out is several such calls in one message). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. The rules below follow, and they apply IN THE ORDER WRITTEN: classify the failure, retry it if it earns a retry, and only then declare the loop blocked. - - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the bullet below, and mention it only in the eventual wrap-up. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing in exactly this case, so treat that sentinel as the signal. But do not rely on it alone: a reviewer that ignores the instruction and answers with anything that is neither a finding list nor a literal `CLEAN` is in the same state, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened, so re-spawn per the bullet below, feeding it whatever the reviewer said it was missing (the diff, a path, the base branch). + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. + - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. + - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing when it CANNOT review, which covers the could-not-fetch case; a refusal or an off-topic answer will not carry the sentinel, so do not rely on it alone: any answer that is neither a finding list nor a literal `CLEAN` is the same state, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened, so re-spawn per the re-spawn bullet, feeding it whatever the reviewer said it was missing (the diff, a path, the base branch). - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. - **Re-spawn it. Do not interrupt the user over a failed spawn.** A spawn that broke mechanically (an internal error, a timeout, a returned-without-reviewing result) is harness noise, so just spawn it again, adjusting the prompt if the reviewer said what it was missing. Keep re-spawning until one takes. A handful of consecutive identical failures means something structural is wrong, so vary the approach (a smaller scope, a different focus, a fresh prompt) rather than firing the same call a tenth time. Recovering costs a few seconds; asking the user costs the whole loop its momentum. **Never** stop mid-loop to report a failed spawn, ask how to proceed, or hand back a half-finished loop. The user asked for a converged review, not a status update, and the loop is not done until a round comes back clean. - **A genuinely unrecoverable reviewer is the only stopping point, and even that is reported at the end.** If re-spawning cannot produce a working reviewer at all, the loop has not converged, so withhold the flip to ready for review and say plainly what happened, once, when you report back. Keep it in the conversation: a spawn that could not run is session tooling, not a fact about the change, so it does NOT go into the PR body or a PR comment (the PR records the review, not the harness). What you must never do is substitute an inline pass to keep the loop moving, which is the failure this whole block exists to prevent. @@ -342,30 +348,30 @@ Each round must: Pass a prompt that: - Names the PR number and branch. - - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. + - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. ONLY those: never prior PR comments or reviews, per the starve rule above. - Instructs it to look for: bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, stylistic problems against `AGENTS.md` and `CONVENTIONS.md` (root and per-package). - Asks for a numbered list with `file:line` references. Problems only, no suggestions. - - Ends with: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop. Do not pad." Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. + - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop. Do not pad." Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. - Tells it to say `BLOCKED` plus what it is missing if it CANNOT review (no diff, no access, wrong repo), instead of falling through to `CLEAN`. Without this the two sanctioned outputs are a finding list or `CLEAN`, so a reviewer that never saw the diff has found nothing genuinely wrong and reports itself clean, which is precisely the non-review the liveness rules must catch. 2. **For each finding the subagent reports**, do exactly ONE of these three. There is no fourth option, and "mention it and move on" is not allowed: - **Fix it** on the branch (commit + push to update the PR), OR - - **Reject it** explicitly with a one-sentence reason written in your reply to the user and in the PR body. Rejection has to be defensible (e.g. "the agent flagged X as a security issue but X runs server-side only and never reaches user input"). False positives are real; reject them on the merits, don't just hand-wave. OR + - **Reject it** explicitly with a one-sentence reason, stated in your reply to the user and recorded on the finding's thread (`rejected because `, per the posting rules; not in the PR body). Rejection has to be defensible (e.g. "the agent flagged X as a security issue but X runs server-side only and never reaches user input"). False positives are real; reject them on the merits, don't just hand-wave. OR - **File it** as a tracked issue when it is genuine but out of scope for THIS PR (a pre-existing bug, an unrelated dependency/hygiene problem, a separate feature). "Out of scope" is NOT a reason to drop a finding. INVOKE the `webjs-file-issue` skill rather than hand-rolling `gh issue create`: it gates on grounding the body in the real codebase first, which is exactly what an out-of-scope review finding needs and exactly what a hurried two-command version omits. Capture the issue number. Verify it landed (`gh project item-list 1 --owner webjsdev --limit 20000`, the board is past 200 items so the default cap of 30 reports a false negative). A finding you call out-of-scope without an issue number is an unfiled finding, which is the exact mistake this clause exists to prevent. If unsure whether something is in-scope, default to fixing it in this PR; only file-and-defer when it is clearly separable and fixing it here would mean scope creep. **Record every genuine finding ON THE PR, exactly the way a person reviewing on GitHub would.** The review trail must live on the PR, not only in your reply to the user, since a finding that exists only in the chat transcript is invisible to anyone reading the PR later. **Post it through the mechanics in `### Every PR review is posted ON the PR` and `### Follow the real review flow`, which are authoritative; do not improvise a different shape here.** In short: the round's summary and all its inline `file:line` findings go up as ONE review object, each finding states the problem only, and the disposition (`fixed in ` / `rejected because ` / `filed as #`) goes in a threaded reply, after which the thread is resolved. That is what a real reviewer does on GitHub, and it is the only shape that renders as a grouped review rather than scattered boxes. Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) -3. **If the round found any findings (even rejected ones)**, run another round with a fresh subagent. The new round picks a slightly different focus prompt: if round 1 was broad, round 2 zooms in on the file you most edited; if round 2 zoomed in, round 3 zooms out to cross-file consistency, etc. Rotate focus to avoid the agent rediscovering the same surface. +3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched. Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape, not the fan-out. 4. **If the round reports `CLEAN`**, the loop is done. NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. -The minimum is TWO rounds. A clean first round is rare and usually means the review was too shallow; if round 1 is clean, spawn a second one with a sharper, narrower focus before believing the result. +The minimum is TWO rounds: the fan-out, then at least one delta verify. A clean fan-out is rare and usually means the lenses were too shallow; before believing it, run one delta-shaped pass over the riskiest section anyway. -**The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. The ONE case that ends the loop without a clean round is a reviewer you cannot spawn at all (see the liveness rules in step 1), and that ends it as BLOCKED, never as done. +**The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. The ONE case that ends the loop without a clean round is a reviewer that cannot be produced at all, meaning it cannot be spawned or never returns an actual review despite the re-spawns (see the liveness rules in step 1), and that ends it as BLOCKED, never as done. -**A standalone "review the PR" request IS the loop, not a one-shot.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop: spawn a fresh round, and if it finds anything, fix/reject/file it and run further rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each SPAWN, including one that failed), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be spawned, which you report as blocked and never paper over with an inline pass of your own. +**A standalone "review the PR" request IS the loop, not a one-shot.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop: spawn a fresh round, and if it finds anything, fix/reject/file it and run further rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each SPAWN, including one that failed), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be produced, which you report as blocked and never paper over with an inline pass of your own. ### When to skip the loop @@ -373,7 +379,7 @@ Skip only for PRs that change a single line of trivially-correct content (a doc ### Reporting after the loop -After the loop converges, report exactly this shape to the user: +After the loop converges and the deferred suites plus the CI read (step 4) are done, report exactly this shape to the user: > PR # is up at . Self-review loop ran rounds; last round clean. Issues found and fixed during the loop: . Out-of-scope findings filed as follow-ups: . Ready to merge. @@ -388,7 +394,7 @@ If you cannot honestly say "last round clean", you cannot say "ready to merge". ### Subagent prompt template ``` -Review PR # at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). +Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review. Use `gh pr diff ` and `gh pr view ` for the diff and metadata, and read any file at its PR-branch state with `gh api repos///contents/?ref= --jq .content | base64 -d`. All of those read from GitHub, so they work whether or not the branch exists locally, which matters because a PR you were asked to review may not be checked out here at all. If the branch does happen to be the one checked out, reading files in place is fine too. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. From d476e635a272e25d55977b7e5d3d9a6a15af7b47 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:28:37 +0530 Subject: [PATCH 12/43] docs: convert the per-round machinery to the fan-out shape Adding fan-out and delta rounds to the preamble left the downstream machinery written for one broad sequential reviewer. The delta verify on the previous commit caught all of it: the template still told reviewers to rotate focus per round, a delta round had no prompt shape and would re-read the whole surface, the starve rule forbade the AGENTS.md read the spec required two bullets later, step 3 banned the fan-out re-run the preamble allows, step 4 ended the loop on a clean round 1 that the minimum-two-rounds rule sends to a delta pass, nothing said a fan-out is clean only when every lens is, and the strengthened Contains-verbatim requirement was violated by the file's own template. A fan-out now has explicit round semantics: each lens re-spawns individually without voiding the others, the round completes when every lens returns findings or CLEAN, its findings post as one aggregated review object, and the whole fan-out counts as one round. --- .claude/skills/webjs-start-work/SKILL.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 342bc7924..27341439b 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -323,9 +323,9 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-read the same surface N times; this shape reads it once, in parallel, then re-reads only what changed. -- **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. +- **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. - **Every later round is delta-scoped: ONE reviewer on the fixes**, fed the fix commits' diff plus the paragraphs they touched, never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. -- **Starve reviewers of context they do not need.** Each gets the PR diff and the touched files, full stop. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. +- **Starve reviewers of context they do not need.** Each gets the PR diff, the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. Each round must: @@ -348,10 +348,10 @@ Each round must: Pass a prompt that: - Names the PR number and branch. - - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. ONLY those: never prior PR comments or reviews, per the starve rule above. + - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched. - Instructs it to look for: bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, stylistic problems against `AGENTS.md` and `CONVENTIONS.md` (root and per-package). - Asks for a numbered list with `file:line` references. Problems only, no suggestions. - - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop. Do not pad." Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. + - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop." plus a no-padding instruction. Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. - Tells it to say `BLOCKED` plus what it is missing if it CANNOT review (no diff, no access, wrong repo), instead of falling through to `CLEAN`. Without this the two sanctioned outputs are a finding list or `CLEAN`, so a reviewer that never saw the diff has found nothing genuinely wrong and reports itself clean, which is precisely the non-review the liveness rules must catch. 2. **For each finding the subagent reports**, do exactly ONE of these three. There is no fourth option, and "mention it and move on" is not allowed: @@ -363,9 +363,9 @@ Each round must: Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) -3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched. Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape, not the fan-out. +3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched. Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. -4. **If the round reports `CLEAN`**, the loop is done. NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. +4. **If the round reports `CLEAN`** (a fan-out is clean only when EVERY lens reported it), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. The minimum is TWO rounds: the fan-out, then at least one delta verify. A clean fan-out is rare and usually means the lenses were too shallow; before believing it, run one delta-shaped pass over the riskiest section anyway. @@ -383,7 +383,7 @@ After the loop converges and the deferred suites plus the CI read (step 4) are d > PR # is up at . Self-review loop ran rounds; last round clean. Issues found and fixed during the loop: . Out-of-scope findings filed as follow-ups: . Ready to merge. -**Only a round in which a fresh subagent actually REVIEWED counts toward ``.** A spawn that was declined, errored, was killed, or returned nothing did not produce a round, so it can be neither the clean last round nor part of the total, and an inline pass you did yourself is not a round at all (see the liveness rules in step 1). Neither does a spawn that returned a result without reviewing: a completed call and a real subagent result are NOT the test, because a reviewer that says "I could not fetch the diff" satisfies both and reviewed nothing. The test is that it came back with findings or with the literal `CLEAN`. A loop whose reviewer never reviewed has run ZERO rounds, and the honest report there is that the review is blocked and why, not a count and not "ready to merge". +**Only a round in which fresh subagents actually REVIEWED counts toward ``, and a fan-out, all its lenses together, is ONE round.** A spawn that was declined, errored, was killed, or returned nothing did not produce a round, so it can be neither the clean last round nor part of the total, and an inline pass you did yourself is not a round at all (see the liveness rules in step 1). Neither does a spawn that returned a result without reviewing: a completed call and a real subagent result are NOT the test, because a reviewer that says "I could not fetch the diff" satisfies both and reviewed nothing. The test is that it came back with findings or with the literal `CLEAN`. A loop whose reviewer never reviewed has run ZERO rounds, and the honest report there is that the review is blocked and why, not a count and not "ready to merge". If you cannot honestly say "last round clean", you cannot say "ready to merge". If a finding is rejected as a false positive, mention it in the report so the user can second-guess the rejection. Every finding the loop surfaced must be accounted for in this report as fixed, rejected-with-reason, or filed-with-issue-number; if you reported an out-of-scope finding to the user but cannot point to its issue number, you have not finished the loop. Every genuine finding must ALSO appear as a comment on the PR (see step 2), so the report and the PR agree. @@ -394,7 +394,7 @@ If you cannot honestly say "last round clean", you cannot say "ready to merge". ### Subagent prompt template ``` -Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). +Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review. Use `gh pr diff ` and `gh pr view ` for the diff and metadata, and read any file at its PR-branch state with `gh api repos///contents/?ref= --jq .content | base64 -d`. All of those read from GitHub, so they work whether or not the branch exists locally, which matters because a PR you were asked to review may not be checked out here at all. If the branch does happen to be the one checked out, reading files in place is fine too. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. @@ -404,7 +404,7 @@ You start with no prior context on this PR. Steps: 2. Run `gh pr view --repo webjsdev/webjs --json title,body` to see what the author claims it does. 3. Read every file the diff touches in its current state (not just the diff hunks) so you see edits in context. 4. Read root AGENTS.md, the per-package AGENTS.md for each touched package, and CONVENTIONS.md if a scaffolded template was touched. -5. Specifically check: . +5. Specifically check: . Report findings as a numbered list with file:line references. Problems only. No suggestions, no nits about style if the rule isn't enforceable. If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop. Do not pad with "looks good overall" or summaries. From 544dc2c8f5e70ad38d65514634cd2929f105df73 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:33:31 +0530 Subject: [PATCH 13/43] docs: close the last two seams in the fan-out conversion The starve rule enumerated a reviewer's permitted context exhaustively and left out the PR title and body that template step 2 has every reviewer fetch, so the rule and the template disagreed about what may be ingested. The body is the author's claims, which the review checks against, so it joins the enumeration rather than leaving the template. And the delta pass a clean fan-out earns had no instantiable scope: with no fix commits, every delta prompt shape pointed at a diff that does not exist. The spec and template now say that pass scopes to the riskiest section of the original diff, matching the minimum-two-rounds rule that created it. --- .claude/skills/webjs-start-work/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 27341439b..d865ddf0a 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -325,7 +325,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. - **Every later round is delta-scoped: ONE reviewer on the fixes**, fed the fix commits' diff plus the paragraphs they touched, never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. -- **Starve reviewers of context they do not need.** Each gets the PR diff, the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. +- **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. Each round must: @@ -348,7 +348,7 @@ Each round must: Pass a prompt that: - Names the PR number and branch. - - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched. + - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched; the delta pass after a clean fan-out, where no fix commits exist, scopes to the riskiest section of the original diff instead. - Instructs it to look for: bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, stylistic problems against `AGENTS.md` and `CONVENTIONS.md` (root and per-package). - Asks for a numbered list with `file:line` references. Problems only, no suggestions. - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop." plus a no-padding instruction. Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. @@ -394,7 +394,7 @@ If you cannot honestly say "last round clean", you cannot say "ready to merge". ### Subagent prompt template ``` -Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). +Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review. Use `gh pr diff ` and `gh pr view ` for the diff and metadata, and read any file at its PR-branch state with `gh api repos///contents/?ref= --jq .content | base64 -d`. All of those read from GitHub, so they work whether or not the branch exists locally, which matters because a PR you were asked to review may not be checked out here at all. If the branch does happen to be the one checked out, reading files in place is fine too. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. From 2c092dfcbcdecde15553298a7824be5f19669205 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:36:23 +0530 Subject: [PATCH 14/43] docs: state the delta scope identically in all three places it appears The previous commit widened the delta scope in two of the three places that state it and missed the round-shape bullet that governs the loop, which still scoped every later round to fix commits that do not exist after a clean fan-out. Same for the starve enumeration: expanded in the rule, unexpanded in the prompt-construction bullet built from it, so an agent composing from the bullets starved the reviewer of the title and body the rule had just admitted. The template's charter placeholder also offered no instantiation for the clean-fan-out pass. All three now carry the same rule with the same carve-out. --- .claude/skills/webjs-start-work/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index d865ddf0a..1b7d0a524 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -324,7 +324,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-read the same surface N times; this shape reads it once, in parallel, then re-reads only what changed. - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. -- **Every later round is delta-scoped: ONE reviewer on the fixes**, fed the fix commits' diff plus the paragraphs they touched, never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. +- **Every later round is delta-scoped: ONE reviewer on what the last round changed**, fed the fix commits' diff plus the paragraphs they touched (or, after a clean fan-out with no fix commits, the riskiest section of the original diff), never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. Each round must: @@ -348,7 +348,7 @@ Each round must: Pass a prompt that: - Names the PR number and branch. - - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched; the delta pass after a clean fan-out, where no fix commits exist, scopes to the riskiest section of the original diff instead. + - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the PR title and body (`gh pr view --json title,body`) and the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched; the delta pass after a clean fan-out, where no fix commits exist, scopes to the riskiest section of the original diff instead. - Instructs it to look for: bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, stylistic problems against `AGENTS.md` and `CONVENTIONS.md` (root and per-package). - Asks for a numbered list with `file:line` references. Problems only, no suggestions. - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop." plus a no-padding instruction. Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. @@ -404,7 +404,7 @@ You start with no prior context on this PR. Steps: 2. Run `gh pr view --repo webjsdev/webjs --json title,body` to see what the author claims it does. 3. Read every file the diff touches in its current state (not just the diff hunks) so you see edits in context. 4. Read root AGENTS.md, the per-package AGENTS.md for each touched package, and CONVENTIONS.md if a scaffolded template was touched. -5. Specifically check: . +5. Specifically check: . Report findings as a numbered list with file:line references. Problems only. No suggestions, no nits about style if the rule isn't enforceable. If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop. Do not pad with "looks good overall" or summaries. From f204f87df2e79357084364635323b37571d25253 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:40:36 +0530 Subject: [PATCH 15/43] docs: give the all-rejected round a delta scope too The no-fix-commits fallback was conditioned on "after a clean fan-out", but a round whose findings were ALL rejected also produces no fix commits and is still followed by a mandatory round, so it had no defined scope anywhere: the headline clause was empty and the carve-out did not apply. The condition is now "a round that produced no fix commits", with both causes named, in all four places the rule appears. Also pinned --repo on the title-and-body fetch so a prompt composed from the bullets works when the branch is not checked out locally, matching the template. --- .claude/skills/webjs-start-work/SKILL.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 1b7d0a524..1f72efb7f 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -324,7 +324,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-read the same surface N times; this shape reads it once, in parallel, then re-reads only what changed. - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. -- **Every later round is delta-scoped: ONE reviewer on what the last round changed**, fed the fix commits' diff plus the paragraphs they touched (or, after a clean fan-out with no fix commits, the riskiest section of the original diff), never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. +- **Every later round is delta-scoped: ONE reviewer on what the last round changed**, fed the fix commits' diff plus the paragraphs they touched (or, when a round produced NO fix commits, a clean fan-out or findings that were all rejected, the riskiest section of the original diff), never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. Each round must: @@ -348,7 +348,7 @@ Each round must: Pass a prompt that: - Names the PR number and branch. - - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the PR title and body (`gh pr view --json title,body`) and the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched; the delta pass after a clean fan-out, where no fix commits exist, scopes to the riskiest section of the original diff instead. + - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the PR title and body (`gh pr view --repo webjsdev/webjs --json title,body`) and the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched; a delta pass following a round with no fix commits (a clean fan-out, or findings that were all rejected) scopes to the riskiest section of the original diff instead. - Instructs it to look for: bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, stylistic problems against `AGENTS.md` and `CONVENTIONS.md` (root and per-package). - Asks for a numbered list with `file:line` references. Problems only, no suggestions. - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop." plus a no-padding instruction. Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. @@ -363,7 +363,7 @@ Each round must: Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) -3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched. Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. +3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched (when every finding was rejected and nothing changed, scope to the riskiest section instead, per the round-shape rules). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. 4. **If the round reports `CLEAN`** (a fan-out is clean only when EVERY lens reported it), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. @@ -394,7 +394,7 @@ If you cannot honestly say "last round clean", you cannot say "ready to merge". ### Subagent prompt template ``` -Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). +Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review. Use `gh pr diff ` and `gh pr view ` for the diff and metadata, and read any file at its PR-branch state with `gh api repos///contents/?ref= --jq .content | base64 -d`. All of those read from GitHub, so they work whether or not the branch exists locally, which matters because a PR you were asked to review may not be checked out here at all. If the branch does happen to be the one checked out, reading files in place is fine too. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. @@ -404,7 +404,7 @@ You start with no prior context on this PR. Steps: 2. Run `gh pr view --repo webjsdev/webjs --json title,body` to see what the author claims it does. 3. Read every file the diff touches in its current state (not just the diff hunks) so you see edits in context. 4. Read root AGENTS.md, the per-package AGENTS.md for each touched package, and CONVENTIONS.md if a scaffolded template was touched. -5. Specifically check: . +5. Specifically check: . Report findings as a numbered list with file:line references. Problems only. No suggestions, no nits about style if the rule isn't enforceable. If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop. Do not pad with "looks good overall" or summaries. From 65f5cc99cdd83cee23f91fdf211e5be03115a6d5 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:44:31 +0530 Subject: [PATCH 16/43] docs: trigger step 3's fallback on the mechanism, not two of its causes Findings can be fixed, rejected, or filed, and a round of only rejected and filed findings produces no fix commits either. Step 3's fallback fired only on "every finding rejected", so the filed case executed the main clause against an empty diff. The trigger is now the mechanism itself, the round produced no fix commits, which cannot be under-inclusive whatever dispositions compose it. --- .claude/skills/webjs-start-work/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 1f72efb7f..673eb543d 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -363,7 +363,7 @@ Each round must: Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) -3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched (when every finding was rejected and nothing changed, scope to the riskiest section instead, per the round-shape rules). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. +3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched (when the round produced no fix commits because every finding was rejected or filed, scope to the riskiest section instead, per the round-shape rules). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. 4. **If the round reports `CLEAN`** (a fan-out is clean only when EVERY lens reported it), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. From 1b6bd8c9ee0fe774470ac8bc3ad1c78d75f715c2 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 20:46:25 +0530 Subject: [PATCH 17/43] docs: state the no-fix-commits condition mechanism-only, everywhere The previous fix put "rejected or filed" in one statement while the three sibling glosses kept the older two-cause enumeration, recreating the drift it was fixing. Cause lists rot every time a disposition is added, so all four statements now name only the mechanism, a round that produced no fix commits, which stays correct whatever composes it. --- .claude/skills/webjs-start-work/SKILL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 673eb543d..b60914ab7 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -324,7 +324,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-read the same surface N times; this shape reads it once, in parallel, then re-reads only what changed. - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. -- **Every later round is delta-scoped: ONE reviewer on what the last round changed**, fed the fix commits' diff plus the paragraphs they touched (or, when a round produced NO fix commits, a clean fan-out or findings that were all rejected, the riskiest section of the original diff), never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. +- **Every later round is delta-scoped: ONE reviewer on what the last round changed**, fed the fix commits' diff plus the paragraphs they touched (or, when a round produced NO fix commits, the riskiest section of the original diff), never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. Each round must: @@ -348,7 +348,7 @@ Each round must: Pass a prompt that: - Names the PR number and branch. - - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the PR title and body (`gh pr view --repo webjsdev/webjs --json title,body`) and the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched; a delta pass following a round with no fix commits (a clean fan-out, or findings that were all rejected) scopes to the riskiest section of the original diff instead. + - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the PR title and body (`gh pr view --repo webjsdev/webjs --json title,body`) and the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched; a delta pass following a round with no fix commits scopes to the riskiest section of the original diff instead. - Instructs it to look for: bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, stylistic problems against `AGENTS.md` and `CONVENTIONS.md` (root and per-package). - Asks for a numbered list with `file:line` references. Problems only, no suggestions. - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop." plus a no-padding instruction. Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. @@ -363,7 +363,7 @@ Each round must: Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) -3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched (when the round produced no fix commits because every finding was rejected or filed, scope to the riskiest section instead, per the round-shape rules). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. +3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched (when the round produced no fix commits, scope to the riskiest section instead, per the round-shape rules). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. 4. **If the round reports `CLEAN`** (a fan-out is clean only when EVERY lens reported it), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. @@ -394,7 +394,7 @@ If you cannot honestly say "last round clean", you cannot say "ready to merge". ### Subagent prompt template ``` -Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). +Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review. Use `gh pr diff ` and `gh pr view ` for the diff and metadata, and read any file at its PR-branch state with `gh api repos///contents/?ref= --jq .content | base64 -d`. All of those read from GitHub, so they work whether or not the branch exists locally, which matters because a PR you were asked to review may not be checked out here at all. If the branch does happen to be the one checked out, reading files in place is fine too. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. From ecf2c424a2868f31ec6edc1083f808ac4cd4ac13 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 21:27:37 +0530 Subject: [PATCH 18/43] docs: delta rounds narrow the question, never the evidence "Fed the fix commits' diff plus the paragraphs they touched" read as the reviewer seeing only that, which for a multi-file change would miss a fix's ripple effects somewhere else, the user's exact concern. What made the delta rounds on this PR effective was the opposite split: the fix as the question, the full changed files as evidence, and a mandate to grep every symbol or concept the fix touches across the whole PR surface. That is now what all three statements say. The costly thing a delta round drops is re-auditing the unchanged surface from scratch, the all-pairs reasoning that makes broad rounds slow, not context. --- .claude/skills/webjs-start-work/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index b60914ab7..9e6b894d6 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -324,7 +324,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-read the same surface N times; this shape reads it once, in parallel, then re-reads only what changed. - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. -- **Every later round is delta-scoped: ONE reviewer on what the last round changed**, fed the fix commits' diff plus the paragraphs they touched (or, when a round produced NO fix commits, the riskiest section of the original diff), never the whole surface again. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. +- **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. Each round must: @@ -348,7 +348,7 @@ Each round must: Pass a prompt that: - Names the PR number and branch. - - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the PR title and body (`gh pr view --repo webjsdev/webjs --json title,body`) and the rule files, never prior PR comments or reviews, per the starve rule above. A delta round scopes this to the fix commits' diff and the paragraphs they touched; a delta pass following a round with no fix commits scopes to the riskiest section of the original diff instead. + - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the PR title and body (`gh pr view --repo webjsdev/webjs --json title,body`) and the rule files, never prior PR comments or reviews, per the starve rule above. A delta round keeps the same evidence but makes the fix commits' diff its question, with a blast-radius instruction per the round-shape rules; a delta pass following a round with no fix commits takes the riskiest section of the original diff as its question instead. - Instructs it to look for: bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, stylistic problems against `AGENTS.md` and `CONVENTIONS.md` (root and per-package). - Asks for a numbered list with `file:line` references. Problems only, no suggestions. - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop." plus a no-padding instruction. Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. @@ -394,7 +394,7 @@ If you cannot honestly say "last round clean", you cannot say "ready to merge". ### Subagent prompt template ``` -Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). +Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review. Use `gh pr diff ` and `gh pr view ` for the diff and metadata, and read any file at its PR-branch state with `gh api repos///contents/?ref= --jq .content | base64 -d`. All of those read from GitHub, so they work whether or not the branch exists locally, which matters because a PR you were asked to review may not be checked out here at all. If the branch does happen to be the one checked out, reading files in place is fine too. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. From bbee3535d580bb90d2a386ff34b298968ff8e004 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 21:29:30 +0530 Subject: [PATCH 19/43] docs: convert step 3 and the intro to the question-evidence model The question-never-evidence rewrite reached three statements of the delta rule and missed the fourth, step 3 of the loop, the one an agent executes literally, which still narrowed the evidence and cited the round-shape rules while saying the opposite of them. The section intro also still promised that later rounds re-read only what changed, priming evidence-scoping two bullets above the rule that forbids it. Both now carry the same model: audit once, then narrow the question while holding the full changed files as evidence. --- .claude/skills/webjs-start-work/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 9e6b894d6..de9025e13 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -321,7 +321,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **No full test suite inside the loop.** After a fix, run only the specific test file(s) covering the line you just changed (`node --test `), which is also what the commit rule needs (a logical unit commits when its own tests pass). Do NOT run the e2e suite, the full Node suite, the browser suite, the Bun matrix, or the four-app dogfood boot check between rounds. Those run ONCE, after the last clean round, and the Definition of done still requires every layer the change touches. This is a timing rule, not permission to skip a layer. - **Never wait on CI between rounds.** Pushing is fire-and-forget: push, then start the next round immediately. No `gh pr checks --watch` mid-loop. Read CI once, after the last clean round. A round that finds something means another push anyway, which supersedes the run you would have been watching, and merge is gated on green CI regardless, so waiting mid-loop buys nothing. -**Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-read the same surface N times; this shape reads it once, in parallel, then re-reads only what changed. +**Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-AUDIT the same surface N times; this shape audits it once, in parallel, then each later round asks only about what changed (while still holding the full changed files as evidence). - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. - **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. @@ -363,7 +363,7 @@ Each round must: Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) -3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer on the fix commits' diff plus the paragraphs they touched (when the round produced no fix commits, scope to the riskiest section instead, per the round-shape rules). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. +3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer whose question is the fix commits' diff, with the full changed files as evidence and the blast-radius grep the round-shape rules mandate (when the round produced no fix commits, the question is the riskiest section instead). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. 4. **If the round reports `CLEAN`** (a fan-out is clean only when EVERY lens reported it), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. From 97dd65fd4bb9306834ffa9f99d0d94d33f1b60cf Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 21:30:31 +0530 Subject: [PATCH 20/43] docs: scale review depth with risk, richest for shipped source WebJs is a framework, so a bug in packages/*/src ships into every end-user app. The round-shape rules now say so: shipped source gets the full shape at its richest (correctness and security lenses, a lens on whether the tests would fail if the change were reverted, a bias toward re-running the fan-out when a fix spans files), while a docs or skill PR may run leaner. The fast-loop rules cut waste, never rigor, and for shipped source the tie breaks toward more review, not less. --- .claude/skills/webjs-start-work/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index de9025e13..6d3b36051 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -326,6 +326,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. - **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. +- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (four lenses with correctness and security among them, a lens asking whether the tests would actually fail if the change were reverted, and a bias toward re-running the fan-out when a fix spans files). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. Each round must: From 06f56b64bcdbc7afbfcdd4a098e89a605bd573ba Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 21:33:24 +0530 Subject: [PATCH 21/43] docs: single-source the lens list and the fan-out re-run trigger The risk bullet restated two rules and immediately drifted from both: its lens set could not coexist with the fan-out's definitional list, and its spans-files re-run bias contradicted the two "only" statements of the re-run trigger. The lens list is now stated as swappable defaults, the trigger lives in one place (the delta bullet, with the shipped-source lowering inline), and the other statements defer instead of restating. Also converted the last surviving scope-phrased statement of the riskiest-section pass to question form. --- .claude/skills/webjs-start-work/SKILL.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 6d3b36051..4607ef6ee 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -323,10 +323,10 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-AUDIT the same surface N times; this shape audits it once, in parallel, then each later round asks only about what changed (while still holding the full changed files as evidence). -- **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass (invariant 11, config validity, cross-references, sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. -- **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff. +- **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens picked for THIS PR (the defaults, swap any for a better-fitting one: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass covering invariant 11, config validity, cross-references, and sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. +- **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough). - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. -- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (four lenses with correctness and security among them, a lens asking whether the tests would actually fail if the change were reverted, and a bias toward re-running the fan-out when a fix spans files). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. +- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. Each round must: @@ -364,11 +364,11 @@ Each round must: Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) -3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer whose question is the fix commits' diff, with the full changed files as evidence and the blast-radius grep the round-shape rules mandate (when the round produced no fix commits, the question is the riskiest section instead). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only in the one case the round-shape rules allow, a fix that rewrote something well outside the original diff. +3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer whose question is the fix commits' diff, with the full changed files as evidence and the blast-radius grep the round-shape rules mandate (when the round produced no fix commits, the question is the riskiest section instead). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only when the round-shape rules call for it. 4. **If the round reports `CLEAN`** (a fan-out is clean only when EVERY lens reported it), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. -The minimum is TWO rounds: the fan-out, then at least one delta verify. A clean fan-out is rare and usually means the lenses were too shallow; before believing it, run one delta-shaped pass over the riskiest section anyway. +The minimum is TWO rounds: the fan-out, then at least one delta verify. A clean fan-out is rare and usually means the lenses were too shallow; before believing it, run one delta-shaped pass with the riskiest section as its question anyway. **The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. The ONE case that ends the loop without a clean round is a reviewer that cannot be produced at all, meaning it cannot be spawned or never returns an actual review despite the re-spawns (see the liveness rules in step 1), and that ends it as BLOCKED, never as done. From 3d428599f7d90e37363b63be1d623c66b48a7cdd Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 21:53:56 +0530 Subject: [PATCH 22/43] docs: adversarial refutation, model diversity, and a deep-review tier Three review-quality upgrades the loop lacked. A shipped-source finding whose fix would be expensive or behavior-changing now gets a refuter before anyone acts on it, because a confident false positive buys a wrong fix plus the delta rounds to unwind it. At least one fan-out lens runs on a different model, since same-family reviewers share the same blind spots. And the riskiest surfaces get a committed deep-review workflow (.claude/workflows/deep-review.js): six finder lenses in parallel, one cross-model, then an adaptive adversarial jury per finding (three refuters for critical, two for major, one for minor, majority kills), returning only what survives refutation, with rejected findings carrying their refuters' reasons. It is the local, no-billing analog of the hosted ultra review, and its confirmed findings feed the normal loop. Syntax validated under the workflow runtime's own wrapping, since a bare module check rejects the top-level return the runtime allows. --- .claude/skills/webjs-start-work/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 4607ef6ee..64745847e 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -323,10 +323,10 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-AUDIT the same surface N times; this shape audits it once, in parallel, then each later round asks only about what changed (while still holding the full changed files as evidence). -- **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens picked for THIS PR (the defaults, swap any for a better-fitting one: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass covering invariant 11, config validity, cross-references, and sentinel grammar). Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. +- **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens picked for THIS PR (the defaults, swap any for a better-fitting one: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass covering invariant 11, config validity, cross-references, and sentinel grammar). Run at least one lens on a DIFFERENT model (the Agent tool's `model` parameter), so the lenses do not all share one model family's blind spots. Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. - **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough). - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. -- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. +- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. For the very riskiest surfaces (the serializer, the client router, CSRF or auth, SSR or action dispatch), replace the fan-out with the committed deep-review workflow (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`): six lenses including a different-model one, then an adversarial jury per finding, reporting only what survives refutation. Its confirmed findings feed this same loop (fix, post as one review object, delta round). Each round must: @@ -355,7 +355,7 @@ Each round must: - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop." plus a no-padding instruction. Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. - Tells it to say `BLOCKED` plus what it is missing if it CANNOT review (no diff, no access, wrong repo), instead of falling through to `CLEAN`. Without this the two sanctioned outputs are a finding list or `CLEAN`, so a reviewer that never saw the diff has found nothing genuinely wrong and reports itself clean, which is precisely the non-review the liveness rules must catch. -2. **For each finding the subagent reports**, do exactly ONE of these three. There is no fourth option, and "mention it and move on" is not allowed: +2. **For each finding the subagent reports**, do exactly ONE of these three. There is no fourth option, and "mention it and move on" is not allowed. One gate first, for shipped source only: a finding whose fix would be expensive or behavior-changing gets an adversarial REFUTER before you act, a fresh subagent handed the same evidence and told to DISPROVE the claim (does the scenario reproduce in the code as written, is the behavior actually intended, is it already guarded or tested somewhere the finder did not look). A finding the refuter kills is a rejection with the refuter's reason; the rest proceed. This costs one cheap spawn and saves the expensive failure, a confident false positive that buys a wrong "fix" plus the delta rounds to unwind it. Trivial or obviously-real findings skip the gate. - **Fix it** on the branch (commit + push to update the PR), OR - **Reject it** explicitly with a one-sentence reason, stated in your reply to the user and recorded on the finding's thread (`rejected because `, per the posting rules; not in the PR body). Rejection has to be defensible (e.g. "the agent flagged X as a security issue but X runs server-side only and never reaches user input"). False positives are real; reject them on the merits, don't just hand-wave. OR - **File it** as a tracked issue when it is genuine but out of scope for THIS PR (a pre-existing bug, an unrelated dependency/hygiene problem, a separate feature). "Out of scope" is NOT a reason to drop a finding. INVOKE the `webjs-file-issue` skill rather than hand-rolling `gh issue create`: it gates on grounding the body in the real codebase first, which is exactly what an out-of-scope review finding needs and exactly what a hurried two-command version omits. Capture the issue number. Verify it landed (`gh project item-list 1 --owner webjsdev --limit 20000`, the board is past 200 items so the default cap of 30 reports a false negative). A finding you call out-of-scope without an issue number is an unfiled finding, which is the exact mistake this clause exists to prevent. If unsure whether something is in-scope, default to fixing it in this PR; only file-and-defer when it is clearly separable and fixing it here would mean scope creep. From 3a6519da6528a128b214fb8aa3af9574badb1cfd Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 21:58:30 +0530 Subject: [PATCH 23/43] docs: actually commit deep-review, generic and safely spawned The previous commit pointed the skill at .claude/workflows/deep-review.js and shipped no such file: .gitignore's .claude/* allowlist has no workflows entry, so git add -A silently skipped it, and the delta round caught the skill sending the riskiest PRs to a workflow that cannot run. The allowlist now unignores .claude/workflows/ and the file is tracked. The workflow is also repo-generic now, per the user: no framework naming, args accept a PR number, owner/repo#N, or { pr, repo }, and when no repo is given each agent detects it from its own cwd via gh. The jury's fail-open stance is documented inline (an empty jury or a tied vote leaves the finding confirmed, never silently dropped). The refuter gate in the skill inherits step 1's spawn rules explicitly (worktree isolation, read-only git, per-spawn repo-health check, re-spawn on failure), and a refuter that cannot be produced means the finding proceeds ungated, since the gate is an optimization and must fail toward treating the finding as real. --- .claude/skills/webjs-start-work/SKILL.md | 2 +- .claude/workflows/deep-review.js | 143 +++++++++++++++++++++++ .gitignore | 2 + 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 .claude/workflows/deep-review.js diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 64745847e..8c4830307 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -355,7 +355,7 @@ Each round must: - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop." plus a no-padding instruction. Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. - Tells it to say `BLOCKED` plus what it is missing if it CANNOT review (no diff, no access, wrong repo), instead of falling through to `CLEAN`. Without this the two sanctioned outputs are a finding list or `CLEAN`, so a reviewer that never saw the diff has found nothing genuinely wrong and reports itself clean, which is precisely the non-review the liveness rules must catch. -2. **For each finding the subagent reports**, do exactly ONE of these three. There is no fourth option, and "mention it and move on" is not allowed. One gate first, for shipped source only: a finding whose fix would be expensive or behavior-changing gets an adversarial REFUTER before you act, a fresh subagent handed the same evidence and told to DISPROVE the claim (does the scenario reproduce in the code as written, is the behavior actually intended, is it already guarded or tested somewhere the finder did not look). A finding the refuter kills is a rejection with the refuter's reason; the rest proceed. This costs one cheap spawn and saves the expensive failure, a confident false positive that buys a wrong "fix" plus the delta rounds to unwind it. Trivial or obviously-real findings skip the gate. +2. **For each finding the subagent reports**, do exactly ONE of these three. There is no fourth option, and "mention it and move on" is not allowed. One gate first, for shipped source only: a finding whose fix would be expensive or behavior-changing gets an adversarial REFUTER before you act, a fresh subagent handed the same evidence and told to DISPROVE the claim (does the scenario reproduce in the code as written, is the behavior actually intended, is it already guarded or tested somewhere the finder did not look). A finding the refuter kills is a rejection with the refuter's reason; the rest proceed. The refuter is a reviewer spawn like any other: step 1's working-tree-safety rules (`isolation: "worktree"`, read-only git, the per-spawn repo-health check) and liveness rules (re-spawn a failed one) apply to it in full, and if a refuter cannot be produced, act on the finding ungated, since the gate is an optimization and must fail toward treating the finding as real. This costs one cheap spawn and saves the expensive failure, a confident false positive that buys a wrong "fix" plus the delta rounds to unwind it. Trivial or obviously-real findings skip the gate. - **Fix it** on the branch (commit + push to update the PR), OR - **Reject it** explicitly with a one-sentence reason, stated in your reply to the user and recorded on the finding's thread (`rejected because `, per the posting rules; not in the PR body). Rejection has to be defensible (e.g. "the agent flagged X as a security issue but X runs server-side only and never reaches user input"). False positives are real; reject them on the merits, don't just hand-wave. OR - **File it** as a tracked issue when it is genuine but out of scope for THIS PR (a pre-existing bug, an unrelated dependency/hygiene problem, a separate feature). "Out of scope" is NOT a reason to drop a finding. INVOKE the `webjs-file-issue` skill rather than hand-rolling `gh issue create`: it gates on grounding the body in the real codebase first, which is exactly what an out-of-scope review finding needs and exactly what a hurried two-command version omits. Capture the issue number. Verify it landed (`gh project item-list 1 --owner webjsdev --limit 20000`, the board is past 200 items so the default cap of 30 reports a false negative). A finding you call out-of-scope without an issue number is an unfiled finding, which is the exact mistake this clause exists to prevent. If unsure whether something is in-scope, default to fixing it in this PR; only file-and-defer when it is clearly separable and fixing it here would mean scope creep. diff --git a/.claude/workflows/deep-review.js b/.claude/workflows/deep-review.js new file mode 100644 index 000000000..8f718b791 --- /dev/null +++ b/.claude/workflows/deep-review.js @@ -0,0 +1,143 @@ +export const meta = { + name: 'deep-review', + description: 'Deep multi-agent PR review: parallel finder lenses, then adversarial verification, reporting only confirmed findings', + whenToUse: 'High-risk PRs where a single review pass is not enough. Works on any repo. Pass the PR number as args, or { pr, repo } to target another repository.', + phases: [ + { title: 'Find', detail: 'six lenses over the PR in parallel, one on a different model' }, + { title: 'Verify', detail: 'adversarial refuters per finding, majority rules' }, + ], +} + +// args: a PR number ("123" or 123), an "owner/repo#123" string, or { pr, repo }. +// When no repo is given, every agent detects it from its own cwd via gh. +let pr = null +let repo = null +if (typeof args === 'number') pr = String(args) +else if (typeof args === 'string' && args.trim()) { + const s = args.trim() + if (s.includes('#')) { const [r, n] = s.split('#'); repo = r.trim(); pr = n.trim() } + else pr = s +} else if (args && typeof args === 'object' && args.pr) { + pr = String(args.pr) + repo = args.repo ? String(args.repo) : null +} +if (!pr) throw new Error('Pass the PR number as args, e.g. Workflow({ name: "deep-review", args: "123" }), or "owner/repo#123", or { pr, repo }') + +const REPO_FLAG = repo ? ` --repo ${repo}` : '' +const REPO_NOTE = repo + ? `The repository is ${repo}; pass --repo ${repo} to every gh call.` + : 'Detect the repository once with `gh repo view --json nameWithOwner -q .nameWithOwner` from your working directory and use it for every gh call.' + +const FINDINGS = { + type: 'object', + required: ['findings'], + properties: { + findings: { + type: 'array', + items: { + type: 'object', + required: ['file', 'line', 'title', 'detail', 'severity'], + properties: { + file: { type: 'string', description: 'repo-relative path' }, + line: { type: 'number', description: '1-indexed line at the PR head' }, + title: { type: 'string', description: 'one-sentence statement of the defect' }, + detail: { type: 'string', description: 'concrete failure scenario: inputs/state leading to wrong behavior' }, + severity: { type: 'string', enum: ['critical', 'major', 'minor'] }, + }, + }, + }, + }, +} + +const VERDICT = { + type: 'object', + required: ['refuted', 'reason'], + properties: { + refuted: { type: 'boolean' }, + reason: { type: 'string', description: 'one or two sentences: why the finding is wrong, or why it survives' }, + }, +} + +// Shared preamble for every agent. Git worktrees of one repo share a single +// .git directory, so a git write from any agent can reach the primary checkout. +const SAFETY = `HARD CONSTRAINT: you may be running against a repository another session is actively using, and git worktrees share ONE .git directory. You are READ-ONLY on git: never run checkout, switch, reset, restore, stash, pull, ref-moving fetch, merge, rebase, clean, branch -f, or worktree. Read-only inspection only (log, show, diff without changing state, status, blame). + +${REPO_NOTE} + +Fetch your evidence from GitHub so it works regardless of the local checkout: +- Diff: gh pr diff ${pr}${REPO_FLAG} +- Claims: gh pr view ${pr}${REPO_FLAG} --json title,body +- Head branch name: gh pr view ${pr}${REPO_FLAG} --json headRefName +- Any file at head: gh api "repos///contents/?ref=" --jq .content | base64 -d +Do NOT fetch PR comments or prior reviews. If the repo carries contributor rules (AGENTS.md, CONTRIBUTING.md, CONVENTIONS.md, or similar at the root or per-package), read the relevant ones and judge against them.` + +const LENSES = [ + { key: 'correctness', prompt: 'Correctness of the change itself: logic errors, inverted conditions, off-by-ones, broken control flow, wrong API usage, error paths that swallow or misreport. Trace each changed function end to end.' }, + { key: 'security', prompt: 'Security: injection, authz/authn gaps, CSRF surface changes, secrets or server-only code reaching the client, open redirects, unsafe deserialization, trust-boundary violations. Weight anything on an authentication, serialization, or request-dispatch path most heavily.' }, + { key: 'blast-radius', prompt: 'Ripple effects: for every symbol, export, config key, or rule the diff touches, grep the WHOLE repo at head for its other users and check each still holds. A small change that breaks a distant caller is your only quarry.' }, + { key: 'tests', prompt: 'Test adequacy: would the PR\'s tests FAIL if each functional change were reverted (counterfactual)? Name any changed behavior with no failing-test proof, any test asserting the mock rather than the behavior, and any test layer the repo\'s contributor rules demand that this change touches but does not cover.' }, + { key: 'invariants-docs', prompt: 'Invariants and doc drift: check the diff against every invariant and convention the repo\'s contributor rules state, and check every doc surface that describes the changed behavior still tells the truth at head.' }, + { key: 'fresh-eyes', prompt: 'Broad second-opinion pass: read the diff cold and report anything genuinely wrong, with no assigned angle. Prefer depth on the riskiest hunk over breadth.', model: 'fable' }, +] + +phase('Find') +log(`deep-review of PR ${pr}${repo ? ` in ${repo}` : ''}: ${LENSES.length} lenses in parallel`) + +const found = await parallel(LENSES.map((l) => () => + agent( + `${SAFETY}\n\nYou are ONE review lens over PR ${pr}. Your single charter:\n${l.prompt}\n\nReport only genuine problems with concrete failure scenarios. No style nits, no suggestions, no padding. Return an empty findings array if you find nothing real.`, + { label: `find:${l.key}`, phase: 'Find', schema: FINDINGS, ...(l.model ? { model: l.model } : {}) }, + ))) + +// Barrier is deliberate: dedup needs every finder's output before the +// expensive verification stage spends refuters on duplicates. +const all = found.filter(Boolean).flatMap((r) => r.findings) +const byKey = new Map() +const rank = { critical: 0, major: 1, minor: 2 } +for (const f of all) { + const key = `${f.file}:${f.line}` + const prev = byKey.get(key) + if (!prev || rank[f.severity] < rank[prev.severity]) byKey.set(key, f) +} +const deduped = [...byKey.values()].sort((a, b) => rank[a.severity] - rank[b.severity]) +const CAP = 12 +const toVerify = deduped.slice(0, CAP) +if (deduped.length > CAP) log(`capping verification at ${CAP} of ${deduped.length} deduped findings (dropped ${deduped.length - CAP} lowest-severity; re-run after fixes to catch them)`) +log(`${all.length} raw findings, ${deduped.length} after dedup, verifying ${toVerify.length}`) + +if (toVerify.length === 0) return { pr, repo, confirmed: [], rejected: [], raw: all.length, note: 'no findings survived dedup; treat as a clean deep pass' } + +phase('Verify') +// Adaptive adversarial jury: 3 refuters for critical, 2 for major, 1 for minor. +// A finding dies when a strict MAJORITY of its jury refutes it, so a 1-1 split +// on a major finding survives (fail-open toward treating it as real). +const juries = { critical: 3, major: 2, minor: 1 } + +const verified = await parallel(toVerify.map((f) => () => + parallel(Array.from({ length: juries[f.severity] }, (_, i) => () => + agent( + `${SAFETY}\n\nYou are an adversarial verifier. A reviewer claims this defect in PR ${pr}:\n\nFILE: ${f.file}:${f.line}\nCLAIM: ${f.title}\nSCENARIO: ${f.detail}\n\nTry to REFUTE it. Read the actual code at head, trace the scenario, and decide whether the defect is real. Angle ${i + 1}: ${i === 0 ? 'does the claimed scenario actually reproduce in the code as written?' : i === 1 ? 'is the behavior actually correct or intended, making the claim a false positive?' : 'is this already guarded, tested, or handled somewhere the finder did not look?'} If you cannot confirm the defect is real, refuted=true.`, + { label: `refute:${f.file.split('/').pop()}:${f.line}`, phase: 'Verify', schema: VERDICT }, + ))) + .then((votes) => { + const cast = votes.filter(Boolean) + const refutes = cast.filter((v) => v.refuted).length + // An empty jury (every refuter died) leaves the finding CONFIRMED: + // fail-open toward the finding being real, never silently dropped. + const dead = cast.length > 0 && refutes * 2 > cast.length + return { ...f, confirmed: !dead, jury: cast.length, refutes, reasons: cast.map((v) => v.reason) } + }))) + +const results = verified.filter(Boolean) +const confirmed = results.filter((r) => r.confirmed) +const rejected = results.filter((r) => !r.confirmed) +log(`confirmed ${confirmed.length}, refuted ${rejected.length}`) + +return { + pr, + repo, + confirmed, + rejected, + stats: { raw: all.length, deduped: deduped.length, verified: toVerify.length, lenses: LENSES.length }, + note: 'Confirmed findings feed the normal review loop: fix, post as one review object, then a delta round. Rejected ones carry their refuters\' reasons for the audit trail.', +} diff --git a/.gitignore b/.gitignore index 0d9584459..72437ab73 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,8 @@ Thumbs.db !.claude/hooks/** !.claude/skills/ !.claude/skills/** +!.claude/workflows/ +!.claude/workflows/** # test artifacts coverage/ From a19e17016d0e9acfe2fdef4853945fdf56a34abf Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 22:03:22 +0530 Subject: [PATCH 24/43] docs: pin two distinct models and fix the workflow's rough edges Pinning one lens to fable guaranteed nothing once the session itself runs fable: the pin coincided with the default and every lens ran the same model, making the different-model claim false exactly where the workflow was written to run. Two lenses now pin two distinct models (fable and opus), so at least one always differs from whatever the orchestrating session runs. Also from the delta round: the repo-given SAFETY note told agents to pass --repo to every gh call, but gh api has no --repo flag and errors on it, so the one command needed for reading files at head failed as instructed; the note now splits gh pr from gh api and the fetch line carries the repo in its URL path. And the clean-pass return now has the same shape as the findings return, so a caller reading stats never gets undefined on one path. --- .claude/skills/webjs-start-work/SKILL.md | 2 +- .claude/workflows/deep-review.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 8c4830307..5debedd40 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -326,7 +326,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens picked for THIS PR (the defaults, swap any for a better-fitting one: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass covering invariant 11, config validity, cross-references, and sentinel grammar). Run at least one lens on a DIFFERENT model (the Agent tool's `model` parameter), so the lenses do not all share one model family's blind spots. Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. - **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough). - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. -- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. For the very riskiest surfaces (the serializer, the client router, CSRF or auth, SSR or action dispatch), replace the fan-out with the committed deep-review workflow (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`): six lenses including a different-model one, then an adversarial jury per finding, reporting only what survives refutation. Its confirmed findings feed this same loop (fix, post as one review object, delta round). +- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. For the very riskiest surfaces (the serializer, the client router, CSRF or auth, SSR or action dispatch), replace the fan-out with the committed deep-review workflow (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`): six lenses with two pinned to distinct models (so at least one always differs from the running session's), then an adversarial jury per finding, reporting only what survives refutation. Its confirmed findings feed this same loop (fix, post as one review object, delta round). Each round must: diff --git a/.claude/workflows/deep-review.js b/.claude/workflows/deep-review.js index 8f718b791..7066b1a3c 100644 --- a/.claude/workflows/deep-review.js +++ b/.claude/workflows/deep-review.js @@ -3,7 +3,7 @@ export const meta = { description: 'Deep multi-agent PR review: parallel finder lenses, then adversarial verification, reporting only confirmed findings', whenToUse: 'High-risk PRs where a single review pass is not enough. Works on any repo. Pass the PR number as args, or { pr, repo } to target another repository.', phases: [ - { title: 'Find', detail: 'six lenses over the PR in parallel, one on a different model' }, + { title: 'Find', detail: 'six lenses over the PR in parallel, two pinned to distinct models' }, { title: 'Verify', detail: 'adversarial refuters per finding, majority rules' }, ], } @@ -25,8 +25,9 @@ if (!pr) throw new Error('Pass the PR number as args, e.g. Workflow({ name: "dee const REPO_FLAG = repo ? ` --repo ${repo}` : '' const REPO_NOTE = repo - ? `The repository is ${repo}; pass --repo ${repo} to every gh call.` - : 'Detect the repository once with `gh repo view --json nameWithOwner -q .nameWithOwner` from your working directory and use it for every gh call.' + ? `The repository is ${repo}. Pass --repo ${repo} to gh pr commands; gh api has no --repo flag, so there the repo rides in the URL path (repos/${repo}/...).` + : 'Detect the repository once with `gh repo view --json nameWithOwner -q .nameWithOwner` from your working directory; pass it as --repo to gh pr commands and put it in the URL path of gh api calls (gh api has no --repo flag).' +const REPO_PATH = repo || '/' const FINDINGS = { type: 'object', @@ -68,17 +69,20 @@ Fetch your evidence from GitHub so it works regardless of the local checkout: - Diff: gh pr diff ${pr}${REPO_FLAG} - Claims: gh pr view ${pr}${REPO_FLAG} --json title,body - Head branch name: gh pr view ${pr}${REPO_FLAG} --json headRefName -- Any file at head: gh api "repos///contents/?ref=" --jq .content | base64 -d +- Any file at head: gh api "repos/${REPO_PATH}/contents/?ref=" --jq .content | base64 -d Do NOT fetch PR comments or prior reviews. If the repo carries contributor rules (AGENTS.md, CONTRIBUTING.md, CONVENTIONS.md, or similar at the root or per-package), read the relevant ones and judge against them.` const LENSES = [ { key: 'correctness', prompt: 'Correctness of the change itself: logic errors, inverted conditions, off-by-ones, broken control flow, wrong API usage, error paths that swallow or misreport. Trace each changed function end to end.' }, { key: 'security', prompt: 'Security: injection, authz/authn gaps, CSRF surface changes, secrets or server-only code reaching the client, open redirects, unsafe deserialization, trust-boundary violations. Weight anything on an authentication, serialization, or request-dispatch path most heavily.' }, { key: 'blast-radius', prompt: 'Ripple effects: for every symbol, export, config key, or rule the diff touches, grep the WHOLE repo at head for its other users and check each still holds. A small change that breaks a distant caller is your only quarry.' }, - { key: 'tests', prompt: 'Test adequacy: would the PR\'s tests FAIL if each functional change were reverted (counterfactual)? Name any changed behavior with no failing-test proof, any test asserting the mock rather than the behavior, and any test layer the repo\'s contributor rules demand that this change touches but does not cover.' }, + { key: 'tests', prompt: 'Test adequacy: would the PR\'s tests FAIL if each functional change were reverted (counterfactual)? Name any changed behavior with no failing-test proof, any test asserting the mock rather than the behavior, and any test layer the repo\'s contributor rules demand that this change touches but does not cover.', model: 'opus' }, { key: 'invariants-docs', prompt: 'Invariants and doc drift: check the diff against every invariant and convention the repo\'s contributor rules state, and check every doc surface that describes the changed behavior still tells the truth at head.' }, { key: 'fresh-eyes', prompt: 'Broad second-opinion pass: read the diff cold and report anything genuinely wrong, with no assigned angle. Prefer depth on the riskiest hunk over breadth.', model: 'fable' }, ] +// Two lenses pin two DISTINCT models (fable and opus) so that whatever model +// the orchestrating session runs, at least one lens differs from it. A single +// pinned model is a no-op whenever it coincides with the session default. phase('Find') log(`deep-review of PR ${pr}${repo ? ` in ${repo}` : ''}: ${LENSES.length} lenses in parallel`) @@ -105,7 +109,7 @@ const toVerify = deduped.slice(0, CAP) if (deduped.length > CAP) log(`capping verification at ${CAP} of ${deduped.length} deduped findings (dropped ${deduped.length - CAP} lowest-severity; re-run after fixes to catch them)`) log(`${all.length} raw findings, ${deduped.length} after dedup, verifying ${toVerify.length}`) -if (toVerify.length === 0) return { pr, repo, confirmed: [], rejected: [], raw: all.length, note: 'no findings survived dedup; treat as a clean deep pass' } +if (toVerify.length === 0) return { pr, repo, confirmed: [], rejected: [], stats: { raw: all.length, deduped: deduped.length, verified: 0, lenses: LENSES.length }, note: 'no findings survived dedup; treat as a clean deep pass' } phase('Verify') // Adaptive adversarial jury: 3 refuters for critical, 2 for major, 1 for minor. From 194db5d2e6957c01f9939a507c9b1cacf781430e Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 22:17:41 +0530 Subject: [PATCH 25/43] docs: pin every deep-review lens, half opus and half fable The session default is opus, so inherit-by-default meant four of six lenses ran the author's own model. All six now pin explicitly: opus for correctness, blast-radius, and tests; fable for security, invariants-docs, and fresh-eyes. Two model families always read the diff, deterministically, whatever the session runs. --- .claude/skills/webjs-start-work/SKILL.md | 2 +- .claude/workflows/deep-review.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 5debedd40..79d2b6a6c 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -326,7 +326,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens picked for THIS PR (the defaults, swap any for a better-fitting one: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass covering invariant 11, config validity, cross-references, and sentinel grammar). Run at least one lens on a DIFFERENT model (the Agent tool's `model` parameter), so the lenses do not all share one model family's blind spots. Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. - **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough). - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. -- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. For the very riskiest surfaces (the serializer, the client router, CSRF or auth, SSR or action dispatch), replace the fan-out with the committed deep-review workflow (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`): six lenses with two pinned to distinct models (so at least one always differs from the running session's), then an adversarial jury per finding, reporting only what survives refutation. Its confirmed findings feed this same loop (fix, post as one review object, delta round). +- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. For the very riskiest surfaces (the serializer, the client router, CSRF or auth, SSR or action dispatch), replace the fan-out with the committed deep-review workflow (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`): six lenses pinned half to opus and half to fable so two model families always read the diff, then an adversarial jury per finding, reporting only what survives refutation. Its confirmed findings feed this same loop (fix, post as one review object, delta round). Each round must: diff --git a/.claude/workflows/deep-review.js b/.claude/workflows/deep-review.js index 7066b1a3c..33eba29ea 100644 --- a/.claude/workflows/deep-review.js +++ b/.claude/workflows/deep-review.js @@ -3,7 +3,7 @@ export const meta = { description: 'Deep multi-agent PR review: parallel finder lenses, then adversarial verification, reporting only confirmed findings', whenToUse: 'High-risk PRs where a single review pass is not enough. Works on any repo. Pass the PR number as args, or { pr, repo } to target another repository.', phases: [ - { title: 'Find', detail: 'six lenses over the PR in parallel, two pinned to distinct models' }, + { title: 'Find', detail: 'six lenses over the PR in parallel, half on opus and half on fable' }, { title: 'Verify', detail: 'adversarial refuters per finding, majority rules' }, ], } @@ -73,16 +73,16 @@ Fetch your evidence from GitHub so it works regardless of the local checkout: Do NOT fetch PR comments or prior reviews. If the repo carries contributor rules (AGENTS.md, CONTRIBUTING.md, CONVENTIONS.md, or similar at the root or per-package), read the relevant ones and judge against them.` const LENSES = [ - { key: 'correctness', prompt: 'Correctness of the change itself: logic errors, inverted conditions, off-by-ones, broken control flow, wrong API usage, error paths that swallow or misreport. Trace each changed function end to end.' }, - { key: 'security', prompt: 'Security: injection, authz/authn gaps, CSRF surface changes, secrets or server-only code reaching the client, open redirects, unsafe deserialization, trust-boundary violations. Weight anything on an authentication, serialization, or request-dispatch path most heavily.' }, - { key: 'blast-radius', prompt: 'Ripple effects: for every symbol, export, config key, or rule the diff touches, grep the WHOLE repo at head for its other users and check each still holds. A small change that breaks a distant caller is your only quarry.' }, + { key: 'correctness', prompt: 'Correctness of the change itself: logic errors, inverted conditions, off-by-ones, broken control flow, wrong API usage, error paths that swallow or misreport. Trace each changed function end to end.', model: 'opus' }, + { key: 'security', prompt: 'Security: injection, authz/authn gaps, CSRF surface changes, secrets or server-only code reaching the client, open redirects, unsafe deserialization, trust-boundary violations. Weight anything on an authentication, serialization, or request-dispatch path most heavily.', model: 'fable' }, + { key: 'blast-radius', prompt: 'Ripple effects: for every symbol, export, config key, or rule the diff touches, grep the WHOLE repo at head for its other users and check each still holds. A small change that breaks a distant caller is your only quarry.', model: 'opus' }, { key: 'tests', prompt: 'Test adequacy: would the PR\'s tests FAIL if each functional change were reverted (counterfactual)? Name any changed behavior with no failing-test proof, any test asserting the mock rather than the behavior, and any test layer the repo\'s contributor rules demand that this change touches but does not cover.', model: 'opus' }, - { key: 'invariants-docs', prompt: 'Invariants and doc drift: check the diff against every invariant and convention the repo\'s contributor rules state, and check every doc surface that describes the changed behavior still tells the truth at head.' }, + { key: 'invariants-docs', prompt: 'Invariants and doc drift: check the diff against every invariant and convention the repo\'s contributor rules state, and check every doc surface that describes the changed behavior still tells the truth at head.', model: 'fable' }, { key: 'fresh-eyes', prompt: 'Broad second-opinion pass: read the diff cold and report anything genuinely wrong, with no assigned angle. Prefer depth on the riskiest hunk over breadth.', model: 'fable' }, ] -// Two lenses pin two DISTINCT models (fable and opus) so that whatever model -// the orchestrating session runs, at least one lens differs from it. A single -// pinned model is a no-op whenever it coincides with the session default. +// Every lens pins its model: half opus, half fable. Two model families +// reading the same diff have different blind spots, and pinning all six makes +// the split deterministic instead of inheriting whatever the session runs. phase('Find') log(`deep-review of PR ${pr}${repo ? ` in ${repo}` : ''}: ${LENSES.length} lenses in parallel`) From 78f919561621b04cb6b73ca8ae9ee75f6878b628 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 22:26:39 +0530 Subject: [PATCH 26/43] docs: let a scout propose dynamic lenses per PR in deep-review Six fixed lenses cover the categories every PR shares, but the angles a specific PR earns depend on what it actually does: concurrency for async coordination, wire compatibility for a serializer change, migration paths for a schema move. A new Scope phase runs one scout that reads the diff, is told the fixed six so it never duplicates them, and proposes zero to six additional lens charters, zero being a fine answer for a PR the fixed set already covers. Dynamic lenses alternate between fable and opus by index, run in the same Find fan-out, and report through the same dedup and jury pipeline. A caller can also pass explicit lenses via args, which skips the scout. Stats now name the dynamic lenses used so a run's shape is auditable from its result. --- .claude/skills/webjs-start-work/SKILL.md | 2 +- .claude/workflows/deep-review.js | 64 ++++++++++++++++++++---- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 79d2b6a6c..5018232dd 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -326,7 +326,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens picked for THIS PR (the defaults, swap any for a better-fitting one: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass covering invariant 11, config validity, cross-references, and sentinel grammar). Run at least one lens on a DIFFERENT model (the Agent tool's `model` parameter), so the lenses do not all share one model family's blind spots. Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. - **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough). - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. -- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. For the very riskiest surfaces (the serializer, the client router, CSRF or auth, SSR or action dispatch), replace the fan-out with the committed deep-review workflow (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`): six lenses pinned half to opus and half to fable so two model families always read the diff, then an adversarial jury per finding, reporting only what survives refutation. Its confirmed findings feed this same loop (fix, post as one review object, delta round). +- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. For the very riskiest surfaces (the serializer, the client router, CSRF or auth, SSR or action dispatch), replace the fan-out with the committed deep-review workflow (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`): a scout reads the diff and proposes up to six dynamic lenses tailored to the PR's nature, which run alongside six fixed ones (all split across opus and fable so two model families always read the diff), then an adversarial jury per finding, reporting only what survives refutation. Its confirmed findings feed this same loop (fix, post as one review object, delta round). Each round must: diff --git a/.claude/workflows/deep-review.js b/.claude/workflows/deep-review.js index 33eba29ea..55af96f3b 100644 --- a/.claude/workflows/deep-review.js +++ b/.claude/workflows/deep-review.js @@ -3,13 +3,16 @@ export const meta = { description: 'Deep multi-agent PR review: parallel finder lenses, then adversarial verification, reporting only confirmed findings', whenToUse: 'High-risk PRs where a single review pass is not enough. Works on any repo. Pass the PR number as args, or { pr, repo } to target another repository.', phases: [ - { title: 'Find', detail: 'six lenses over the PR in parallel, half on opus and half on fable' }, + { title: 'Scope', detail: 'a scout reads the diff and proposes dynamic lenses for this PR' }, + { title: 'Find', detail: 'six fixed lenses plus up to six dynamic ones, in parallel, split across opus and fable' }, { title: 'Verify', detail: 'adversarial refuters per finding, majority rules' }, ], } -// args: a PR number ("123" or 123), an "owner/repo#123" string, or { pr, repo }. +// args: a PR number ("123" or 123), an "owner/repo#123" string, or { pr, repo, lenses }. // When no repo is given, every agent detects it from its own cwd via gh. +// lenses: optional [{ key, prompt }] of extra lenses; when given, the scout is +// skipped and these run as the dynamic set instead. let pr = null let repo = null if (typeof args === 'number') pr = String(args) @@ -21,6 +24,7 @@ else if (typeof args === 'string' && args.trim()) { pr = String(args.pr) repo = args.repo ? String(args.repo) : null } +const givenLenses = (args && typeof args === 'object' && Array.isArray(args.lenses)) ? args.lenses : null if (!pr) throw new Error('Pass the PR number as args, e.g. Workflow({ name: "deep-review", args: "123" }), or "owner/repo#123", or { pr, repo }') const REPO_FLAG = repo ? ` --repo ${repo}` : '' @@ -80,14 +84,56 @@ const LENSES = [ { key: 'invariants-docs', prompt: 'Invariants and doc drift: check the diff against every invariant and convention the repo\'s contributor rules state, and check every doc surface that describes the changed behavior still tells the truth at head.', model: 'fable' }, { key: 'fresh-eyes', prompt: 'Broad second-opinion pass: read the diff cold and report anything genuinely wrong, with no assigned angle. Prefer depth on the riskiest hunk over breadth.', model: 'fable' }, ] -// Every lens pins its model: half opus, half fable. Two model families -// reading the same diff have different blind spots, and pinning all six makes -// the split deterministic instead of inheriting whatever the session runs. +// Every fixed lens pins its model: half opus, half fable. Two model families +// reading the same diff have different blind spots, and pinning makes the +// split deterministic instead of inheriting whatever the session runs. +// Dynamic lenses alternate between the two families by index. + +const LENS_PROPOSALS = { + type: 'object', + required: ['lenses'], + properties: { + lenses: { + type: 'array', + maxItems: 6, + items: { + type: 'object', + required: ['key', 'prompt'], + properties: { + key: { type: 'string', description: 'short kebab-case name for the lens' }, + prompt: { type: 'string', description: 'one-paragraph reviewer charter: what this lens hunts and why THIS PR earns it' }, + }, + }, + }, + }, +} + +phase('Scope') +let dynamic = [] +if (givenLenses) { + dynamic = givenLenses.slice(0, 6).map((l) => ({ key: String(l.key), prompt: String(l.prompt) })) + log(`using ${dynamic.length} caller-provided dynamic lenses, scout skipped`) +} else { + const scout = await agent( + `${SAFETY} + +You are the SCOUT for a deep review of PR ${pr}. Read the diff and the PR body, understand what kind of work this PR actually does, and propose ZERO to six ADDITIONAL review lenses tailored to it. Six fixed lenses already run regardless, so never duplicate their ground: correctness of the changed logic, security, repo-wide ripple effects of touched symbols, test adequacy and counterfactuals, invariant and doc drift, and a broad fresh-eyes pass. + +Propose a lens only when THIS PR's nature earns it. Examples of the kind of thing that earns one: concurrency or race conditions if the diff touches async coordination; serialization compatibility if a wire format changed; migration or data-loss paths if storage schemas moved; API backward compatibility if public signatures changed; performance if a hot path was rewritten; accessibility if UI semantics changed; prompt-injection surfaces if agent-facing prose or tooling changed. Zero is a fine answer for a PR whose nature the fixed six already cover. + +Each proposal is a key plus a one-paragraph reviewer charter written like an order: what to hunt, where in this diff, and what evidence would confirm it.`, + { label: 'scout:lenses', phase: 'Scope', schema: LENS_PROPOSALS }, + ) + dynamic = ((scout && scout.lenses) || []).slice(0, 6) + log(`scout proposed ${dynamic.length} dynamic lens(es)${dynamic.length ? ': ' + dynamic.map((l) => l.key).join(', ') : ''}`) +} +const DYNAMIC = dynamic.map((l, i) => ({ key: `dyn-${l.key}`, prompt: l.prompt, model: i % 2 === 0 ? 'fable' : 'opus' })) +const ALL_LENSES = [...LENSES, ...DYNAMIC] phase('Find') -log(`deep-review of PR ${pr}${repo ? ` in ${repo}` : ''}: ${LENSES.length} lenses in parallel`) +log(`deep-review of PR ${pr}${repo ? ` in ${repo}` : ''}: ${ALL_LENSES.length} lenses in parallel (${LENSES.length} fixed, ${DYNAMIC.length} dynamic)`) -const found = await parallel(LENSES.map((l) => () => +const found = await parallel(ALL_LENSES.map((l) => () => agent( `${SAFETY}\n\nYou are ONE review lens over PR ${pr}. Your single charter:\n${l.prompt}\n\nReport only genuine problems with concrete failure scenarios. No style nits, no suggestions, no padding. Return an empty findings array if you find nothing real.`, { label: `find:${l.key}`, phase: 'Find', schema: FINDINGS, ...(l.model ? { model: l.model } : {}) }, @@ -109,7 +155,7 @@ const toVerify = deduped.slice(0, CAP) if (deduped.length > CAP) log(`capping verification at ${CAP} of ${deduped.length} deduped findings (dropped ${deduped.length - CAP} lowest-severity; re-run after fixes to catch them)`) log(`${all.length} raw findings, ${deduped.length} after dedup, verifying ${toVerify.length}`) -if (toVerify.length === 0) return { pr, repo, confirmed: [], rejected: [], stats: { raw: all.length, deduped: deduped.length, verified: 0, lenses: LENSES.length }, note: 'no findings survived dedup; treat as a clean deep pass' } +if (toVerify.length === 0) return { pr, repo, confirmed: [], rejected: [], stats: { raw: all.length, deduped: deduped.length, verified: 0, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key) }, note: 'no findings survived dedup; treat as a clean deep pass' } phase('Verify') // Adaptive adversarial jury: 3 refuters for critical, 2 for major, 1 for minor. @@ -142,6 +188,6 @@ return { repo, confirmed, rejected, - stats: { raw: all.length, deduped: deduped.length, verified: toVerify.length, lenses: LENSES.length }, + stats: { raw: all.length, deduped: deduped.length, verified: toVerify.length, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key) }, note: 'Confirmed findings feed the normal review loop: fix, post as one review object, then a delta round. Rejected ones carry their refuters\' reasons for the audit trail.', } From 2b8e1001201a7fe97f127b9304fb211060be432d Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 22:32:14 +0530 Subject: [PATCH 27/43] docs: deep-review is round 1 for every task, no risk bifurcation The user directed a uniform rule: every task entering the review loop starts with the deep-review workflow, whatever the work touches. The scout removed the last advantage the ad-hoc fan-out held (per-PR lens tailoring), the workflow's cost self-scales because juries convene per finding, and a uniform rule cannot be misjudged the way a by-risk bifurcation can. The fan-out machinery, the lens-swapping guidance, and the match-depth-to-risk bullet are deleted rather than kept alongside, since duplicated review rules drifting apart is this file's documented disease. The refuter gate survives scoped to delta-round findings, whose reports are the only ones that now arrive without a jury behind them. Two rules added at the user's direction: deep-review re-runs in full when the PR's scope changes after round 1 (new work folded in, a pivot, an issue absorbed mid-review), because a delta round verifies fixes to an audited surface and cannot stand in for the audit of a surface round 1 never saw. And a standalone "review the PR" request always starts with a fresh deep-review, regardless of prior cycles, because the ask itself says the existing trail is not trusted or not current. The trivial-change skip is unchanged: a one-line typo still enters no loop at all. --- .claude/skills/webjs-start-work/SKILL.md | 25 ++++++++++++------------ .claude/workflows/deep-review.js | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 5018232dd..c749f409a 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -321,16 +321,15 @@ The draft PR is already open (step 6), so reviews post to it from the first roun - **No full test suite inside the loop.** After a fix, run only the specific test file(s) covering the line you just changed (`node --test `), which is also what the commit rule needs (a logical unit commits when its own tests pass). Do NOT run the e2e suite, the full Node suite, the browser suite, the Bun matrix, or the four-app dogfood boot check between rounds. Those run ONCE, after the last clean round, and the Definition of done still requires every layer the change touches. This is a timing rule, not permission to skip a layer. - **Never wait on CI between rounds.** Pushing is fire-and-forget: push, then start the next round immediately. No `gh pr checks --watch` mid-loop. Read CI once, after the last clean round. A round that finds something means another push anyway, which supersedes the run you would have been watching, and merge is gated on green CI regardless, so waiting mid-loop buys nothing. -**Shape the rounds for wall clock: one parallel fan-out, then delta-scoped verification.** Sequential broad rounds re-AUDIT the same surface N times; this shape audits it once, in parallel, then each later round asks only about what changed (while still holding the full changed files as evidence). +**Shape the rounds for wall clock: deep-review first, then delta-scoped verification.** Sequential broad rounds re-AUDIT the same surface N times; this shape audits it once, in parallel, then each later round asks only about what changed (while still holding the full changed files as evidence). -- **Round 1 is a fan-out of 3 or 4 NARROW reviewers spawned in parallel** (one message, multiple Agent calls, so they run concurrently), each with a distinct lens picked for THIS PR (the defaults, swap any for a better-fitting one: correctness of the change, internal consistency of the touched sections, does-it-actually-satisfy-the-issue, and a mechanical pass covering invariant 11, config validity, cross-references, and sentinel grammar). Run at least one lens on a DIFFERENT model (the Agent tool's `model` parameter), so the lenses do not all share one model family's blind spots. Narrow parallel lenses cost one round's wall clock and cover more than one broad reviewer, and each stays cold on the others' blind spots. Each lens is its own spawn under the liveness rules below: a failed lens re-spawns individually without voiding the others, the round is complete only when EVERY lens has returned findings or `CLEAN`, the lenses' findings post as ONE aggregated review object, and the whole fan-out counts as ONE round. -- **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape; re-run the fan-out only when a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough). +- **Round 1 is the committed deep-review workflow, for EVERY task that enters the loop** (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`). What it does: a scout reads the diff and proposes up to six dynamic lenses tailored to the PR's nature, which run in parallel alongside six fixed ones (correctness, security, blast radius, test adequacy, invariants and doc drift, fresh eyes; all split across opus and fable so two model families always read the diff), then an adversarial jury refutes each finding, and only what survives comes back, with rejected findings carrying their refuters' reasons. Uniform on purpose: its cost self-scales (juries convene per finding, so a quiet PR is cheap), and a uniform rule cannot be misjudged the way a by-risk bifurcation can, which matters because a `packages/*/src` bug ships into every end-user app. The whole run counts as ONE round, clean only when it confirms nothing, and its confirmed findings feed this same loop (fix, post as one review object, delta round). A deep-review run that dies or errors is a failed spawn under the liveness rules below: re-run it, and never substitute an inline pass. +- **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape. Re-run deep-review in full, not a delta, when the surface round 1 audited is no longer the surface under review: a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough), OR the PR's SCOPE changed after round 1, new work folded in, a user-directed pivot, an issue absorbed mid-review. A delta round verifies fixes to an audited surface; it cannot stand in for the audit of a surface round 1 never saw. - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. -- **Match depth to risk.** WebJs is a framework: a bug in `packages/*/src` ships into every end-user app, so a PR touching shipped source gets the FULL shape at its richest (swap lenses so correctness, security, and whether-the-tests-would-fail-if-reverted are covered, and take the lower fan-out re-run bar the delta bullet gives shipped source). A docs or skill-file PR may run leaner (consistency and issue-satisfaction lenses matter most there). The fast-loop rules cut waste, never rigor: for shipped source, when in doubt, review more, not less. For the very riskiest surfaces (the serializer, the client router, CSRF or auth, SSR or action dispatch), replace the fan-out with the committed deep-review workflow (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`): a scout reads the diff and proposes up to six dynamic lenses tailored to the PR's nature, which run alongside six fixed ones (all split across opus and fable so two model families always read the diff), then an adversarial jury per finding, reporting only what survives refutation. Its confirmed findings feed this same loop (fix, post as one review object, delta round). Each round must: -1. **Spawn fresh general-purpose subagents: the round-1 fan-out, or the single delta verifier** (the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: false`; a fan-out is several such calls in one message). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) +1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. @@ -355,7 +354,7 @@ Each round must: - Contains, verbatim: "If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop." plus a no-padding instruction. Keep `CLEAN` and its "on its own line" exactly as written; the loop tests for that literal sentinel, so a paraphrase in the prompt makes a clean round unrecognisable. - Tells it to say `BLOCKED` plus what it is missing if it CANNOT review (no diff, no access, wrong repo), instead of falling through to `CLEAN`. Without this the two sanctioned outputs are a finding list or `CLEAN`, so a reviewer that never saw the diff has found nothing genuinely wrong and reports itself clean, which is precisely the non-review the liveness rules must catch. -2. **For each finding the subagent reports**, do exactly ONE of these three. There is no fourth option, and "mention it and move on" is not allowed. One gate first, for shipped source only: a finding whose fix would be expensive or behavior-changing gets an adversarial REFUTER before you act, a fresh subagent handed the same evidence and told to DISPROVE the claim (does the scenario reproduce in the code as written, is the behavior actually intended, is it already guarded or tested somewhere the finder did not look). A finding the refuter kills is a rejection with the refuter's reason; the rest proceed. The refuter is a reviewer spawn like any other: step 1's working-tree-safety rules (`isolation: "worktree"`, read-only git, the per-spawn repo-health check) and liveness rules (re-spawn a failed one) apply to it in full, and if a refuter cannot be produced, act on the finding ungated, since the gate is an optimization and must fail toward treating the finding as real. This costs one cheap spawn and saves the expensive failure, a confident false positive that buys a wrong "fix" plus the delta rounds to unwind it. Trivial or obviously-real findings skip the gate. +2. **For each finding the subagent reports**, do exactly ONE of these three. There is no fourth option, and "mention it and move on" is not allowed. One gate first, for a DELTA round's findings on shipped source (round-1 findings skip it, since deep-review's juries already refuted them): a finding whose fix would be expensive or behavior-changing gets an adversarial REFUTER before you act, a fresh subagent handed the same evidence and told to DISPROVE the claim (does the scenario reproduce in the code as written, is the behavior actually intended, is it already guarded or tested somewhere the finder did not look). A finding the refuter kills is a rejection with the refuter's reason; the rest proceed. The refuter is a reviewer spawn like any other: step 1's working-tree-safety rules (`isolation: "worktree"`, read-only git, the per-spawn repo-health check) and liveness rules (re-spawn a failed one) apply to it in full, and if a refuter cannot be produced, act on the finding ungated, since the gate is an optimization and must fail toward treating the finding as real. This costs one cheap spawn and saves the expensive failure, a confident false positive that buys a wrong "fix" plus the delta rounds to unwind it. Trivial or obviously-real findings skip the gate. - **Fix it** on the branch (commit + push to update the PR), OR - **Reject it** explicitly with a one-sentence reason, stated in your reply to the user and recorded on the finding's thread (`rejected because `, per the posting rules; not in the PR body). Rejection has to be defensible (e.g. "the agent flagged X as a security issue but X runs server-side only and never reaches user input"). False positives are real; reject them on the merits, don't just hand-wave. OR - **File it** as a tracked issue when it is genuine but out of scope for THIS PR (a pre-existing bug, an unrelated dependency/hygiene problem, a separate feature). "Out of scope" is NOT a reason to drop a finding. INVOKE the `webjs-file-issue` skill rather than hand-rolling `gh issue create`: it gates on grounding the body in the real codebase first, which is exactly what an out-of-scope review finding needs and exactly what a hurried two-command version omits. Capture the issue number. Verify it landed (`gh project item-list 1 --owner webjsdev --limit 20000`, the board is past 200 items so the default cap of 30 reports a false negative). A finding you call out-of-scope without an issue number is an unfiled finding, which is the exact mistake this clause exists to prevent. If unsure whether something is in-scope, default to fixing it in this PR; only file-and-defer when it is clearly separable and fixing it here would mean scope creep. @@ -364,15 +363,15 @@ Each round must: Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) -3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer whose question is the fix commits' diff, with the full changed files as evidence and the blast-radius grep the round-shape rules mandate (when the round produced no fix commits, the question is the riskiest section instead). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; the fan-out re-runs only when the round-shape rules call for it. +3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer whose question is the fix commits' diff, with the full changed files as evidence and the blast-radius grep the round-shape rules mandate (when the round produced no fix commits, the question is the riskiest section instead). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; deep-review re-runs only when the round-shape rules call for it (a fix well outside the original diff, or a scope change). -4. **If the round reports `CLEAN`** (a fan-out is clean only when EVERY lens reported it), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. +4. **If the round reports `CLEAN`** (a deep-review run is clean only when it confirms nothing), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. -The minimum is TWO rounds: the fan-out, then at least one delta verify. A clean fan-out is rare and usually means the lenses were too shallow; before believing it, run one delta-shaped pass with the riskiest section as its question anyway. +The minimum is TWO rounds: deep-review, then at least one delta verify. A clean deep-review is rare and usually means the lenses were too shallow for this PR; before believing it, run one delta-shaped pass with the riskiest section as its question anyway. **The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. The ONE case that ends the loop without a clean round is a reviewer that cannot be produced at all, meaning it cannot be spawned or never returns an actual review despite the re-spawns (see the liveness rules in step 1), and that ends it as BLOCKED, never as done. -**A standalone "review the PR" request IS the loop, not a one-shot.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop: spawn a fresh round, and if it finds anything, fix/reject/file it and run further rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each SPAWN, including one that failed), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be produced, which you report as blocked and never paper over with an inline pass of your own. +**A standalone "review the PR" request IS the loop, not a one-shot, and it ALWAYS starts with a fresh deep-review run.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop from round 1: a full deep-review, regardless of how many review cycles the PR has already been through, because the ask itself says the existing trail is not trusted or not current. Then, if it finds anything, fix/reject/file it and run delta rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each SPAWN, including one that failed), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be produced, which you report as blocked and never paper over with an inline pass of your own. ### When to skip the loop @@ -384,7 +383,7 @@ After the loop converges and the deferred suites plus the CI read (step 4) are d > PR # is up at . Self-review loop ran rounds; last round clean. Issues found and fixed during the loop: . Out-of-scope findings filed as follow-ups: . Ready to merge. -**Only a round in which fresh subagents actually REVIEWED counts toward ``, and a fan-out, all its lenses together, is ONE round.** A spawn that was declined, errored, was killed, or returned nothing did not produce a round, so it can be neither the clean last round nor part of the total, and an inline pass you did yourself is not a round at all (see the liveness rules in step 1). Neither does a spawn that returned a result without reviewing: a completed call and a real subagent result are NOT the test, because a reviewer that says "I could not fetch the diff" satisfies both and reviewed nothing. The test is that it came back with findings or with the literal `CLEAN`. A loop whose reviewer never reviewed has run ZERO rounds, and the honest report there is that the review is blocked and why, not a count and not "ready to merge". +**Only a round in which fresh subagents actually REVIEWED counts toward ``, and a deep-review run, all its lenses and juries together, is ONE round.** A spawn that was declined, errored, was killed, or returned nothing did not produce a round, so it can be neither the clean last round nor part of the total, and an inline pass you did yourself is not a round at all (see the liveness rules in step 1). Neither does a spawn that returned a result without reviewing: a completed call and a real subagent result are NOT the test, because a reviewer that says "I could not fetch the diff" satisfies both and reviewed nothing. The test is that it came back with findings or with the literal `CLEAN`. A loop whose reviewer never reviewed has run ZERO rounds, and the honest report there is that the review is blocked and why, not a count and not "ready to merge". If you cannot honestly say "last round clean", you cannot say "ready to merge". If a finding is rejected as a false positive, mention it in the report so the user can second-guess the rejection. Every finding the loop surfaced must be accounted for in this report as fixed, rejected-with-reason, or filed-with-issue-number; if you reported an out-of-scope finding to the user but cannot point to its issue number, you have not finished the loop. Every genuine finding must ALSO appear as a comment on the PR (see step 2), so the report and the PR agree. @@ -395,7 +394,7 @@ If you cannot honestly say "last round clean", you cannot say "ready to merge". ### Subagent prompt template ``` -Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). +Review PR # (branch ``) at https://github.com/webjsdev/webjs/pull/ for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations against the project's AGENTS.md and CONVENTIONS.md (root + per-package). HARD CONSTRAINT, read first: you are running against a repository the main session is actively using, and every worktree of it shares ONE `.git` directory, so a git write here reaches the main session's checkout even from an isolated worktree. You are a READ-ONLY reviewer. Do NOT run any command that changes git branch, HEAD, the index, or the working tree: no `git checkout`, `git switch`, `git reset`, `git restore`, `git stash`, `git pull`, `git fetch` that moves refs, `git merge`, `git rebase`, `git clean`, `git branch -f`, or `git worktree`. Any of these silently corrupts the main session's checkout (it moved HEAD off the branch and looked like lost work, and a stray worktree op once flipped the shared repo's `core.bare` to `true`). You do NOT need to switch branches to review. Use `gh pr diff ` and `gh pr view ` for the diff and metadata, and read any file at its PR-branch state with `gh api repos///contents/?ref= --jq .content | base64 -d`. All of those read from GitHub, so they work whether or not the branch exists locally, which matters because a PR you were asked to review may not be checked out here at all. If the branch does happen to be the one checked out, reading files in place is fine too. The only git you may run is read-only inspection (`git log`, `git show`, `git diff` WITHOUT changing state, `git status`, `git blame`). If you think you need to change git state to do the review, you are wrong; report what you found instead. @@ -405,7 +404,7 @@ You start with no prior context on this PR. Steps: 2. Run `gh pr view --repo webjsdev/webjs --json title,body` to see what the author claims it does. 3. Read every file the diff touches in its current state (not just the diff hunks) so you see edits in context. 4. Read root AGENTS.md, the per-package AGENTS.md for each touched package, and CONVENTIONS.md if a scaffolded template was touched. -5. Specifically check: . +5. Specifically check: . Report findings as a numbered list with file:line references. Problems only. No suggestions, no nits about style if the rule isn't enforceable. If you find nothing genuinely wrong, say exactly `CLEAN` on its own line and stop. Do not pad with "looks good overall" or summaries. diff --git a/.claude/workflows/deep-review.js b/.claude/workflows/deep-review.js index 55af96f3b..dfbc2290c 100644 --- a/.claude/workflows/deep-review.js +++ b/.claude/workflows/deep-review.js @@ -1,7 +1,7 @@ export const meta = { name: 'deep-review', description: 'Deep multi-agent PR review: parallel finder lenses, then adversarial verification, reporting only confirmed findings', - whenToUse: 'High-risk PRs where a single review pass is not enough. Works on any repo. Pass the PR number as args, or { pr, repo } to target another repository.', + whenToUse: 'The round-1 review for any PR entering a review loop, or a heavyweight pass on demand. Works on any repo. Pass the PR number as args, "owner/repo#123" for another repository, or { pr, repo, lenses } where explicit lenses skip the scout.', phases: [ { title: 'Scope', detail: 'a scout reads the diff and proposes dynamic lenses for this PR' }, { title: 'Find', detail: 'six fixed lenses plus up to six dynamic ones, in parallel, split across opus and fable' }, From 5c4c19957d942d69b58b7c0415d7836206e98809 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 22:38:34 +0530 Subject: [PATCH 28/43] docs: convert the per-round machinery to a workflow-shaped round 1 The unification made round 1 a Workflow run and left the machinery written for Agent-tool spawns, the same seam the fan-out conversion hit. The delta round enumerated all of it. The liveness rules now name the Workflow run's completion notification as round 1's status signal, and the clean-round tests recognize each reviewer's expected result shape, since a deep-review run returns a structured result and never the literal CLEAN the old test demanded. The foreground mandate and prompt spec are scoped to the delta verifier. Working-tree safety states honestly that deep-review's internal agents run on the prompt defense alone, which is exactly why the repo-health check now fires after workflow runs too. Round semantics for a jury-adjudicated round are defined: confirmed findings are the round's findings, jury-rejected ones return for the audit trail only and force nothing, while a delta finding the author rejects still forces a round because the author's own rejection is unadjudicated in a way a jury's is not. A manual review ask overrides the trivial-change skip: when the user asks for a review, they get one. --- .claude/skills/webjs-start-work/SKILL.md | 24 ++++++++++++------------ .claude/workflows/deep-review.js | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index c749f409a..23b077bba 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -323,30 +323,30 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: deep-review first, then delta-scoped verification.** Sequential broad rounds re-AUDIT the same surface N times; this shape audits it once, in parallel, then each later round asks only about what changed (while still holding the full changed files as evidence). -- **Round 1 is the committed deep-review workflow, for EVERY task that enters the loop** (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`). What it does: a scout reads the diff and proposes up to six dynamic lenses tailored to the PR's nature, which run in parallel alongside six fixed ones (correctness, security, blast radius, test adequacy, invariants and doc drift, fresh eyes; all split across opus and fable so two model families always read the diff), then an adversarial jury refutes each finding, and only what survives comes back, with rejected findings carrying their refuters' reasons. Uniform on purpose: its cost self-scales (juries convene per finding, so a quiet PR is cheap), and a uniform rule cannot be misjudged the way a by-risk bifurcation can, which matters because a `packages/*/src` bug ships into every end-user app. The whole run counts as ONE round, clean only when it confirms nothing, and its confirmed findings feed this same loop (fix, post as one review object, delta round). A deep-review run that dies or errors is a failed spawn under the liveness rules below: re-run it, and never substitute an inline pass. +- **Round 1 is the committed deep-review workflow, for EVERY task that enters the loop** (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`). What it does: a scout reads the diff and proposes up to six dynamic lenses tailored to the PR's nature, which run in parallel alongside six fixed ones (correctness, security, blast radius, test adequacy, invariants and doc drift, fresh eyes; all split across opus and fable so two model families always read the diff), then an adversarial jury refutes each finding. Its CONFIRMED findings are the round's findings, the ones you act on; the jury-REJECTED ones come back too, carrying their refuters' reasons, but only for the audit trail (post them with the round per step 2's record rule; they are already adjudicated, need no action, and do not by themselves force another round). Uniform on purpose: its cost self-scales (juries convene per finding, so a quiet PR is cheap), and a uniform rule cannot be misjudged the way a by-risk bifurcation can, which matters because a `packages/*/src` bug ships into every end-user app. The whole run counts as ONE round, clean only when it confirms nothing, and its confirmed findings feed this same loop (fix, post as one review object, delta round). A deep-review run that dies or errors is a failed spawn under the liveness rules below: re-run it, and never substitute an inline pass. - **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape. Re-run deep-review in full, not a delta, when the surface round 1 audited is no longer the surface under review: a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough), OR the PR's SCOPE changed after round 1, new work folded in, a user-directed pivot, an issue absorbed mid-review. A delta round verifies fixes to an audited surface; it cannot stand in for the audit of a surface round 1 never saw. - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. Each round must: -1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: false`). A fresh subagent has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn it in the FOREGROUND so the round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret. A backgrounded reviewer can be killed or lost while you are left guessing whether to keep waiting, which is the ambiguity the liveness rules below exist to remove. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the Agent call itself, foreground or not.) +1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: false`; round 1 via the Workflow tool, which runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn the DELTA verifier in the FOREGROUND so its round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret, where a backgrounded one can be killed or lost while you are left guessing whether to keep waiting. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the tool call itself, foreground or not, and a Workflow call can be declined the same way.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal, and for the mandated foreground spawn that status IS the Agent call's own result in this turn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For the delta verifier's mandated foreground spawn that status IS the Agent call's own result in this turn; for the round-1 deep-review run it IS the Workflow run's completion notification and the structured result it carries, and a run whose harness status is killed or errored is a dead round-1 exactly like a dead spawn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing when it CANNOT review, which covers the could-not-fetch case; a refusal or an off-topic answer will not carry the sentinel, so do not rely on it alone: any answer that is neither a finding list nor a literal `CLEAN` is the same state, and the absence of findings is NOT a clean round. Treat it exactly like a failed spawn: no round happened, so re-spawn per the re-spawn bullet, feeding it whatever the reviewer said it was missing (the diff, a path, the base branch). + - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing when it CANNOT review, which covers the could-not-fetch case; a refusal or an off-topic answer will not carry the sentinel, so do not rely on it alone: any answer that is not the reviewer's expected result shape is the same state, and the absence of findings is NOT a clean round. The expected shape is per reviewer kind: the delta verifier returns a finding list or the literal `CLEAN`; a deep-review run returns its structured result with `confirmed` and `rejected` lists, and anything else from it (prose, a partial result, nothing) means the round did not happen. Treat it exactly like a failed spawn: no round happened, so re-spawn per the re-spawn bullet, feeding it whatever the reviewer said it was missing (the diff, a path, the base branch). - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. - **Re-spawn it. Do not interrupt the user over a failed spawn.** A spawn that broke mechanically (an internal error, a timeout, a returned-without-reviewing result) is harness noise, so just spawn it again, adjusting the prompt if the reviewer said what it was missing. Keep re-spawning until one takes. A handful of consecutive identical failures means something structural is wrong, so vary the approach (a smaller scope, a different focus, a fresh prompt) rather than firing the same call a tenth time. Recovering costs a few seconds; asking the user costs the whole loop its momentum. **Never** stop mid-loop to report a failed spawn, ask how to proceed, or hand back a half-finished loop. The user asked for a converged review, not a status update, and the loop is not done until a round comes back clean. - **A genuinely unrecoverable reviewer is the only stopping point, and even that is reported at the end.** If re-spawning cannot produce a working reviewer at all, the loop has not converged, so withhold the flip to ready for review and say plainly what happened, once, when you report back. Keep it in the conversation: a spawn that could not run is session tooling, not a fact about the change, so it does NOT go into the PR body or a PR comment (the PR records the review, not the harness). What you must never do is substitute an inline pass to keep the loop moving, which is the failure this whole block exists to prevent. - **Working-tree safety (non-negotiable).** A review subagent runs against the repository THIS session is using, and every worktree of a repo shares ONE `.git` directory, so its git writes reach the main checkout even from an isolated worktree. A reviewer that runs `git checkout` / `switch` / `reset` / `stash` / `rebase` silently moves the main session's HEAD off the branch (it has happened: a reviewer ran `git checkout main` mid-loop and the local checkout regressed; commits were safe only because they were already pushed). Two defenses, apply BOTH: - - Spawn the reviewer with `isolation: "worktree"` so its WORKING TREE is its own throwaway checkout and a stray `git checkout` cannot move the files under this session. It does NOT wall off the repo: the worktree shares this repo's `.git`, so ref and config writes still land on the shared repository. (A read-only reviewer needs no shared state; `gh pr diff` reads from GitHub.) - - Include the read-only git prohibition in the prompt (it is baked into the template below). Belt and suspenders: isolation contains the working-tree half of the damage, the prompt prevents the attempt at the shared half that isolation cannot reach. - - After EACH spawn resolves, before acting on findings, run a one-line repo-health check. Every spawn, not every round: a spawn that was declined, errored, or died did not produce a round but may well have created a worktree first, so it needs the check MORE than a clean return does. Run it and repair if needed (the worktree isolation in the first bullet has itself corrupted the main repo: spawning isolated reviewers flipped the main repo's `core.bare` to `true` and left a locked `.claude/worktrees/agent-*` worktree, after which `git checkout` failed with "this operation must be run in a work tree"). Check ALL of: + **Working-tree safety (non-negotiable).** A review agent runs against the repository THIS session is using, and every worktree of a repo shares ONE `.git` directory, so its git writes reach the main checkout even from an isolated worktree. A reviewer that runs `git checkout` / `switch` / `reset` / `stash` / `rebase` silently moves the main session's HEAD off the branch (it has happened: a reviewer ran `git checkout main` mid-loop and the local checkout regressed; commits were safe only because they were already pushed). Two defenses: + - For every reviewer YOU spawn directly (the delta verifier, a refuter): `isolation: "worktree"`, so its WORKING TREE is its own throwaway checkout and a stray `git checkout` cannot move the files under this session. It does NOT wall off the repo: the worktree shares this repo's `.git`, so ref and config writes still land on the shared repository. (A read-only reviewer needs no shared state; `gh pr diff` reads from GitHub.) + - For ALL reviewers, the read-only git prohibition in the prompt (baked into the delta template below and into deep-review's own agent preambles; the workflow's internal agents run on this defense alone, which is why the check below matters after its runs too). Belt and suspenders where both apply: isolation contains the working-tree half of the damage, the prompt prevents the attempt at the shared half that isolation cannot reach. + - After EACH spawn or workflow run resolves, before acting on findings, run a one-line repo-health check. Every one, not every round: a spawn that was declined, errored, or died did not produce a round but may well have created a worktree first, so it needs the check MORE than a clean return does. Run it and repair if needed (the worktree isolation in the first bullet has itself corrupted the main repo: spawning isolated reviewers flipped the main repo's `core.bare` to `true` and left a locked `.claude/worktrees/agent-*` worktree, after which `git checkout` failed with "this operation must be run in a work tree"). Check ALL of: - `git rev-parse --is-inside-work-tree` is `true` and `git config --get core.bare` is NOT `true`. If the work tree is broken, repair with `git config core.bare false`, then `git worktree prune` (and `git worktree remove -f -f .claude/worktrees/agent-*` for any locked leftover). Do NOT touch the user's OWN unrelated worktrees (anything outside `.claude/worktrees/`). - `git rev-parse --abbrev-ref HEAD` is still the feature branch and `git status` is clean. If HEAD moved (or points at a now-deleted branch / `0000000`), `git checkout -f ` (or `main` post-merge) restores it. The merge / commits are always safe on GitHub regardless; this only repairs the LOCAL repo. Nothing is lost. If you cannot cleanly repair, `git checkout -f main && git pull` to resync, then continue. - Pass a prompt that: + For the DELTA verifier (round 1's prompts live inside the deep-review workflow), pass a prompt that: - Names the PR number and branch. - Tells it to fetch the diff (`gh pr diff --repo webjsdev/webjs`) and read the changed files in context. Those plus the PR title and body (`gh pr view --repo webjsdev/webjs --json title,body`) and the rule files, never prior PR comments or reviews, per the starve rule above. A delta round keeps the same evidence but makes the fix commits' diff its question, with a blast-radius instruction per the round-shape rules; a delta pass following a round with no fix commits takes the riskiest section of the original diff as its question instead. - Instructs it to look for: bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, stylistic problems against `AGENTS.md` and `CONVENTIONS.md` (root and per-package). @@ -363,7 +363,7 @@ Each round must: Post rejected findings and false positives too, so the reasoning is auditable on the PR. A round that reviewed and found nothing posts a short summary review saying so. Do this as part of the loop, not as an afterthought. Build the review JSON with a real serializer, never by interpolating into a shell string: review text is dense with backticks, and an unquoted shell string runs them as command substitution. That is exactly how a review on #1115 lost every code reference and had to be deleted and reposted. (Reminder: em-dashes and the other banned glyphs in AGENTS.md invariant 11 apply to every comment body, since they go through the same tooling.) -3. **If the round found any findings (even rejected ones)**, fix them and run a delta-scoped round: ONE fresh reviewer whose question is the fix commits' diff, with the full changed files as evidence and the blast-radius grep the round-shape rules mandate (when the round produced no fix commits, the question is the riskiest section instead). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; deep-review re-runs only when the round-shape rules call for it (a fix well outside the original diff, or a scope change). +3. **If the round produced any actionable findings (a deep-review run's CONFIRMED list, or anything a delta verifier reports, including findings YOU then reject, since your own rejection is unadjudicated judgment in a way a jury's is not)**, resolve them and run a delta-scoped round: ONE fresh reviewer whose question is the fix commits' diff, with the full changed files as evidence and the blast-radius grep the round-shape rules mandate (when the round produced no fix commits, the question is the riskiest section instead). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; deep-review re-runs only when the round-shape rules call for it (a fix well outside the original diff, or a scope change). 4. **If the round reports `CLEAN`** (a deep-review run is clean only when it confirms nothing), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. @@ -371,7 +371,7 @@ The minimum is TWO rounds: deep-review, then at least one delta verify. A clean **The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. The ONE case that ends the loop without a clean round is a reviewer that cannot be produced at all, meaning it cannot be spawned or never returns an actual review despite the re-spawns (see the liveness rules in step 1), and that ends it as BLOCKED, never as done. -**A standalone "review the PR" request IS the loop, not a one-shot, and it ALWAYS starts with a fresh deep-review run.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop from round 1: a full deep-review, regardless of how many review cycles the PR has already been through, because the ask itself says the existing trail is not trusted or not current. Then, if it finds anything, fix/reject/file it and run delta rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each SPAWN, including one that failed), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be produced, which you report as blocked and never paper over with an inline pass of your own. +**A standalone "review the PR" request IS the loop, not a one-shot, and it ALWAYS starts with a fresh deep-review run.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop from round 1: a full deep-review, regardless of how many review cycles the PR has already been through, because the ask itself says the existing trail is not trusted or not current. An explicit ask also overrides the trivial-change skip below: when the user asks for a review, they get one, however small the diff. Then, if it finds anything, fix/reject/file it and run delta rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each SPAWN, including one that failed), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be produced, which you report as blocked and never paper over with an inline pass of your own. ### When to skip the loop @@ -383,7 +383,7 @@ After the loop converges and the deferred suites plus the CI read (step 4) are d > PR # is up at . Self-review loop ran rounds; last round clean. Issues found and fixed during the loop: . Out-of-scope findings filed as follow-ups: . Ready to merge. -**Only a round in which fresh subagents actually REVIEWED counts toward ``, and a deep-review run, all its lenses and juries together, is ONE round.** A spawn that was declined, errored, was killed, or returned nothing did not produce a round, so it can be neither the clean last round nor part of the total, and an inline pass you did yourself is not a round at all (see the liveness rules in step 1). Neither does a spawn that returned a result without reviewing: a completed call and a real subagent result are NOT the test, because a reviewer that says "I could not fetch the diff" satisfies both and reviewed nothing. The test is that it came back with findings or with the literal `CLEAN`. A loop whose reviewer never reviewed has run ZERO rounds, and the honest report there is that the review is blocked and why, not a count and not "ready to merge". +**Only a round in which fresh subagents actually REVIEWED counts toward ``, and a deep-review run, all its lenses and juries together, is ONE round.** A spawn that was declined, errored, was killed, or returned nothing did not produce a round, so it can be neither the clean last round nor part of the total, and an inline pass you did yourself is not a round at all (see the liveness rules in step 1). Neither does a spawn that returned a result without reviewing: a completed call and a real subagent result are NOT the test, because a reviewer that says "I could not fetch the diff" satisfies both and reviewed nothing. The test is that it came back in its expected result shape: findings or the literal `CLEAN` from a delta verifier, the structured `confirmed`/`rejected` result from a deep-review run. A loop whose reviewer never reviewed has run ZERO rounds, and the honest report there is that the review is blocked and why, not a count and not "ready to merge". If you cannot honestly say "last round clean", you cannot say "ready to merge". If a finding is rejected as a false positive, mention it in the report so the user can second-guess the rejection. Every finding the loop surfaced must be accounted for in this report as fixed, rejected-with-reason, or filed-with-issue-number; if you reported an out-of-scope finding to the user but cannot point to its issue number, you have not finished the loop. Every genuine finding must ALSO appear as a comment on the PR (see step 2), so the report and the PR agree. diff --git a/.claude/workflows/deep-review.js b/.claude/workflows/deep-review.js index dfbc2290c..f2fa324d8 100644 --- a/.claude/workflows/deep-review.js +++ b/.claude/workflows/deep-review.js @@ -1,6 +1,6 @@ export const meta = { name: 'deep-review', - description: 'Deep multi-agent PR review: parallel finder lenses, then adversarial verification, reporting only confirmed findings', + description: 'Deep multi-agent PR review: parallel finder lenses, then adversarial verification; confirmed findings are actionable, jury-rejected ones return for the audit trail', whenToUse: 'The round-1 review for any PR entering a review loop, or a heavyweight pass on demand. Works on any repo. Pass the PR number as args, "owner/repo#123" for another repository, or { pr, repo, lenses } where explicit lenses skip the scout.', phases: [ { title: 'Scope', detail: 'a scout reads the diff and proposes dynamic lenses for this PR' }, From 99971fb13490b2cf90749f8ee025146a40eaee8a Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 22:43:56 +0530 Subject: [PATCH 29/43] docs: finish converting the three spawn-era recaps The liveness tail still wrote off every dead background agent as never the mandated reviewer, which stopped being true the moment round 1 became a background Workflow run; it now distinguishes a stray agent from the round's own reviewer and names the right recovery for each. The standalone-review recap still restated the old both-defenses-always scheme its source no longer says, and the Failure handling bullet still spoke only of re-spawning a subagent. All three now match the workflow-shaped machinery they summarize. --- .claude/skills/webjs-start-work/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 23b077bba..283348b8d 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -331,7 +331,7 @@ Each round must: 1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: false`; round 1 via the Workflow tool, which runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn the DELTA verifier in the FOREGROUND so its round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret, where a backgrounded one can be killed or lost while you are left guessing whether to keep waiting. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the tool call itself, foreground or not, and a Workflow call can be declined the same way.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For the delta verifier's mandated foreground spawn that status IS the Agent call's own result in this turn; for the round-1 deep-review run it IS the Workflow run's completion notification and the structured result it carries, and a run whose harness status is killed or errored is a dead round-1 exactly like a dead spawn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead BACKGROUND agent is not by itself a blocked loop: it was never the mandated reviewer, so write it off and spawn the foreground reviewer this step calls for. Only that spawn failing blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For the delta verifier's mandated foreground spawn that status IS the Agent call's own result in this turn; for the round-1 deep-review run it IS the Workflow run's completion notification and the structured result it carries, and a run whose harness status is killed or errored is a dead round-1 exactly like a dead spawn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the foreground delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing when it CANNOT review, which covers the could-not-fetch case; a refusal or an off-topic answer will not carry the sentinel, so do not rely on it alone: any answer that is not the reviewer's expected result shape is the same state, and the absence of findings is NOT a clean round. The expected shape is per reviewer kind: the delta verifier returns a finding list or the literal `CLEAN`; a deep-review run returns its structured result with `confirmed` and `rejected` lists, and anything else from it (prose, a partial result, nothing) means the round did not happen. Treat it exactly like a failed spawn: no round happened, so re-spawn per the re-spawn bullet, feeding it whatever the reviewer said it was missing (the diff, a path, the base branch). - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. @@ -371,7 +371,7 @@ The minimum is TWO rounds: deep-review, then at least one delta verify. A clean **The LAST round must be clean, always. A fix is never the end of the loop.** The moment a round surfaces a genuine finding and you fix it (or reject it), you have NOT finished: the fix changed the branch, so it needs its own round. Run another round on the new HEAD and keep going until one round finds zero issues. Do NOT report "fixed it" or "ready to merge" after a round that found something, even if the fix is obviously correct and tests pass. The recurring failure this prevents: review finds X, you fix X and immediately report done, having never re-reviewed the fix. If you ever notice you are about to report a result where the most recent review round found a finding, stop and run another round first. The ONE case that ends the loop without a clean round is a reviewer that cannot be produced at all, meaning it cannot be spawned or never returns an actual review despite the re-spawns (see the liveness rules in step 1), and that ends it as BLOCKED, never as done. -**A standalone "review the PR" request IS the loop, not a one-shot, and it ALWAYS starts with a fresh deep-review run.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop from round 1: a full deep-review, regardless of how many review cycles the PR has already been through, because the ask itself says the existing trail is not trusted or not current. An explicit ask also overrides the trivial-change skip below: when the user asks for a review, they get one, however small the diff. Then, if it finds anything, fix/reject/file it and run delta rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (spawn with `isolation: "worktree"`, keep the reviewer read-only on git, run the repo-health check after each SPAWN, including one that failed), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be produced, which you report as blocked and never paper over with an inline pass of your own. +**A standalone "review the PR" request IS the loop, not a one-shot, and it ALWAYS starts with a fresh deep-review run.** When the user asks you to review an existing PR (separately from the post-`gh pr create` flow), you re-enter this exact loop from round 1: a full deep-review, regardless of how many review cycles the PR has already been through, because the ask itself says the existing trail is not trusted or not current. An explicit ask also overrides the trivial-change skip below: when the user asks for a review, they get one, however small the diff. Then, if it finds anything, fix/reject/file it and run delta rounds until the last one is clean, BEFORE you report back. "I reviewed it and fixed one thing" is not an acceptable stopping point; "I reviewed it in a loop and the last round was clean" is. The user should never have to ask "was the last review clean?" because you should not have stopped until it was. Arriving here skips step 1's preamble, so read it before spawning, BOTH halves. The working-tree-safety block still applies (worktree isolation for reviewers you spawn directly, the read-only git prohibition for every reviewer including deep-review's internal agents, and the repo-health check after each spawn or workflow run, including failed ones), and so do the liveness rules: the sole reason to stop short of clean is a reviewer that cannot be produced, which you report as blocked and never paper over with an inline pass of your own. ### When to skip the loop @@ -464,5 +464,5 @@ A merge updates `main` and npm, but the four in-repo apps deployed to Railway (` - If `git status` is dirty: stop and ask the user to stash, commit, or abandon their pending work before creating the new branch. Never silently lose changes. - If the issue is already in `In progress` (someone else's work, or a prior branch left open): report this and ask the user whether to continue on the existing branch, branch off a fresh main, or pick a different issue. - If the local checkout regressed mid-loop (HEAD on `main` or a base commit, the feature branch's work seemingly "gone"): a review subagent mutated shared git state. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. -- If the review subagent cannot be spawned (declined at the permission prompt, an internal error, a spawn that dies, or one that returns having reviewed nothing): the round did not happen. Re-spawn it, varying the approach after a few identical failures, and do NOT stop mid-loop to report the failure or ask how to proceed: recovering costs seconds and interrupting costs the loop its momentum. Only a reviewer that cannot be produced at all blocks the loop, which is its one exit that is not a clean round; then withhold the flip to ready for review and say once, at the end, that the review did not run and why. Keep that out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. +- If the round's reviewer cannot be produced (a deep-review run or a delta spawn that is declined at the permission prompt, errors, dies, or returns without its expected result shape): the round did not happen. Re-run it (re-invoke the workflow, re-spawn the verifier), varying the approach after a few identical failures, and do NOT stop mid-loop to report the failure or ask how to proceed: recovering costs seconds and interrupting costs the loop its momentum. Only a reviewer that cannot be produced at all blocks the loop, which is its one exit that is not a clean round; then withhold the flip to ready for review and say once, at the end, that the review did not run and why. Keep that out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. - If the `gh project item-edit` call fails (auth scope, missing field): report the failure clearly and offer to do the move manually via the web UI. The branch creation still stands. From 5c98045ebd9fd7899a54a162fea6f703fdc46fc8 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 22:49:49 +0530 Subject: [PATCH 30/43] docs: async reviewer spawns with a transcript watchdog The foreground mandate rebuilt the exact blind wait this PR exists to kill: a synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up, and one delta round that should cost minutes ate 20 that way, unwatchable from inside the blocked call. Reviewers now spawn in the background with a watchdog on the task's transcript file, firing on two minutes of stalled mtime or ten minutes total. The peek it triggers reads harness evidence only, task status and whether the transcript is still growing; the words in a transcript remain an agent's own prose and count for nothing, so the #1158 rule survives intact. A stalled reviewer is stopped and re-spawned, a progressing one is left alone, and a hang past the watchdog joins the failure taxonomy. --- .claude/skills/webjs-start-work/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 283348b8d..606d4bccf 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -329,9 +329,9 @@ The draft PR is already open (step 6), so reviews post to it from the first roun Each round must: -1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: false`; round 1 via the Workflow tool, which runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. Spawn the DELTA verifier in the FOREGROUND so its round resolves inside one turn: a synchronous call hands back either the review or the failure, with no in-flight state left to interpret, where a backgrounded one can be killed or lost while you are left guessing whether to keep waiting. (Backgrounding does not protect you from a declined spawn either. A refusal lands on the tool call itself, foreground or not, and a Workflow call can be declined the same way.) +1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file: fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For the delta verifier's mandated foreground spawn that status IS the Agent call's own result in this turn; for the round-1 deep-review run it IS the Workflow run's completion notification and the structured result it carries, and a run whose harness status is killed or errored is a dead round-1 exactly like a dead spawn. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the foreground delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For a spawned reviewer that status is its completion notification and result, with the watchdog's transcript-activity probe as the mid-flight signal; for the round-1 deep-review run it IS the Workflow run's completion notification and the structured result it carries, and a run whose harness status is killed or errored is a dead round exactly like a dead spawn. Transcript-file activity (mtime, growth) is harness evidence and may be probed; the WORDS in a transcript are an agent's own prose and still count for nothing. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the foreground delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing when it CANNOT review, which covers the could-not-fetch case; a refusal or an off-topic answer will not carry the sentinel, so do not rely on it alone: any answer that is not the reviewer's expected result shape is the same state, and the absence of findings is NOT a clean round. The expected shape is per reviewer kind: the delta verifier returns a finding list or the literal `CLEAN`; a deep-review run returns its structured result with `confirmed` and `rejected` lists, and anything else from it (prose, a partial result, nothing) means the round did not happen. Treat it exactly like a failed spawn: no round happened, so re-spawn per the re-spawn bullet, feeding it whatever the reviewer said it was missing (the diff, a path, the base branch). - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. @@ -464,5 +464,5 @@ A merge updates `main` and npm, but the four in-repo apps deployed to Railway (` - If `git status` is dirty: stop and ask the user to stash, commit, or abandon their pending work before creating the new branch. Never silently lose changes. - If the issue is already in `In progress` (someone else's work, or a prior branch left open): report this and ask the user whether to continue on the existing branch, branch off a fresh main, or pick a different issue. - If the local checkout regressed mid-loop (HEAD on `main` or a base commit, the feature branch's work seemingly "gone"): a review subagent mutated shared git state. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. -- If the round's reviewer cannot be produced (a deep-review run or a delta spawn that is declined at the permission prompt, errors, dies, or returns without its expected result shape): the round did not happen. Re-run it (re-invoke the workflow, re-spawn the verifier), varying the approach after a few identical failures, and do NOT stop mid-loop to report the failure or ask how to proceed: recovering costs seconds and interrupting costs the loop its momentum. Only a reviewer that cannot be produced at all blocks the loop, which is its one exit that is not a clean round; then withhold the flip to ready for review and say once, at the end, that the review did not run and why. Keep that out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. +- If the round's reviewer cannot be produced (a deep-review run or a delta spawn that is declined at the permission prompt, errors, dies, hangs past its watchdog with a stalled transcript, or returns without its expected result shape): the round did not happen. Re-run it (re-invoke the workflow, re-spawn the verifier), varying the approach after a few identical failures, and do NOT stop mid-loop to report the failure or ask how to proceed: recovering costs seconds and interrupting costs the loop its momentum. Only a reviewer that cannot be produced at all blocks the loop, which is its one exit that is not a clean round; then withhold the flip to ready for review and say once, at the end, that the review did not run and why. Keep that out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. - If the `gh project item-edit` call fails (auth scope, missing field): report the failure clearly and offer to do the move manually via the web UI. The branch creation still stands. From 360bb5d12a1e3636a826d74d27bf2f7e1c13bef1 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 22:50:27 +0530 Subject: [PATCH 31/43] docs: watchdog checks the reviewer transcript every 30 seconds --- .claude/skills/webjs-start-work/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 606d4bccf..6beff443b 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -329,7 +329,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun Each round must: -1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file: fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) +1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file, checking every 30 seconds: fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For a spawned reviewer that status is its completion notification and result, with the watchdog's transcript-activity probe as the mid-flight signal; for the round-1 deep-review run it IS the Workflow run's completion notification and the structured result it carries, and a run whose harness status is killed or errored is a dead round exactly like a dead spawn. Transcript-file activity (mtime, growth) is harness evidence and may be probed; the WORDS in a transcript are an agent's own prose and still count for nothing. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the foreground delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. From f9a4cba00c14323b6f60f8350d1d029a9b0a9289 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 22:56:34 +0530 Subject: [PATCH 32/43] docs: one watchdog for both reviewer kinds, with a locatable path The async flip left one clause still calling the delta verifier foreground, inside the non-negotiable block, which was the one sentence that could rebuild the blind wait. The liveness split also gave the round-1 workflow run no mid-flight signal at all, a blind wait for the longest-running reviewer kind, while the failure taxonomy assumed it had one. Both reviewer kinds now share the same pair of signals: the harness notification on completion, the watchdog's output-file activity probe mid-flight, TaskStop on either task id. And the watchdog's path is now specified rather than implied: taken from the spawn's own tool result or task notification, never guessed, since a guess once aimed a watchdog at its own output file; with no path available it degrades to a hard timer on the same thresholds. --- .claude/skills/webjs-start-work/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 6beff443b..e7d5e7eea 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -329,9 +329,9 @@ The draft PR is already open (step 6), so reviews post to it from the first roun Each round must: -1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file, checking every 30 seconds: fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) +1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file, checking every 30 seconds. Take the path from the spawn's own tool result or task notification, NEVER a guess (a watchdog once aimed at the newest file in the tasks directory and watched its own output; if the harness returns no path, as for a named teammate, the watchdog degrades to a hard timer on the same thresholds). Fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For a spawned reviewer that status is its completion notification and result, with the watchdog's transcript-activity probe as the mid-flight signal; for the round-1 deep-review run it IS the Workflow run's completion notification and the structured result it carries, and a run whose harness status is killed or errored is a dead round exactly like a dead spawn. Transcript-file activity (mtime, growth) is harness evidence and may be probed; the WORDS in a transcript are an agent's own prose and still count for nothing. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the foreground delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For BOTH reviewer kinds, an Agent spawn and a Workflow run alike, the completion signal is the harness notification and the result it carries, and the mid-flight signal is the watchdog's activity probe on the task's output file (`TaskStop` works on either task id), so neither kind is ever a blind wait. A run whose harness status is killed or errored is a dead round exactly like a dead spawn. Transcript-file activity (mtime, growth) is harness evidence and may be probed; the WORDS in a transcript are an agent's own prose and still count for nothing. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing when it CANNOT review, which covers the could-not-fetch case; a refusal or an off-topic answer will not carry the sentinel, so do not rely on it alone: any answer that is not the reviewer's expected result shape is the same state, and the absence of findings is NOT a clean round. The expected shape is per reviewer kind: the delta verifier returns a finding list or the literal `CLEAN`; a deep-review run returns its structured result with `confirmed` and `rejected` lists, and anything else from it (prose, a partial result, nothing) means the round did not happen. Treat it exactly like a failed spawn: no round happened, so re-spawn per the re-spawn bullet, feeding it whatever the reviewer said it was missing (the diff, a path, the base branch). - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. From 8dffa7f3df366f66e637d204572bb56fd717fa78 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:01:41 +0530 Subject: [PATCH 33/43] docs: use async and multi-agent features across the four heavy skills An audit of all nine skills (filed as #1161, #1162, #1163, all closed by this PR) found four whose dominant cost is independent work run serially, and five that are correctly sequential (a board query, a write-up, a post gain nothing from agents). start-work: the deferred suites and the CI read launch as one batch of background tasks after the last clean round, with every result collected before anything is reported, so the wall clock is the slowest suite rather than the sum, and the WHEN-not-WHETHER rule is explicitly untouched. Also pinned: reviewers are one-shot, never persistent teammates, because accumulated context is the blind-spot sharing the fresh-reviewer rule exists to kill. file-issue: multi-area grounding fans out as parallel read-only Explore agents, one per concern, with the cold-start test unchanged as the gate. doc-sync and scaffold-sync: surface sweeps fan out as read-only Explore agents while edits stay in the main session, since parallel writers on overlapping docs collide; scaffold verifications run as parallel background boots, preferring the portless in-process harness so runs cannot collide, with distinct --port values when a real listen is unavoidable. --- .claude/skills/webjs-doc-sync/SKILL.md | 4 ++++ .claude/skills/webjs-file-issue/SKILL.md | 2 ++ .claude/skills/webjs-scaffold-sync/SKILL.md | 4 ++++ .claude/skills/webjs-start-work/SKILL.md | 4 ++-- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.claude/skills/webjs-doc-sync/SKILL.md b/.claude/skills/webjs-doc-sync/SKILL.md index 50d9079ac..4ee17f062 100644 --- a/.claude/skills/webjs-doc-sync/SKILL.md +++ b/.claude/skills/webjs-doc-sync/SKILL.md @@ -84,6 +84,8 @@ applies, then update or consciously skip each. ## Per-change sync procedure +The surface checks below are independent reads, so run them fanned out: one read-only `Explore` agent per doc surface, spawned in ONE message, each answering "does this surface describe the changed behavior, and does it still tell the truth at head?". The agents REPORT; the edits stay in the main session, because parallel writers on overlapping docs collide. A single-surface change needs no fan-out. + 1. Identify the change's IDENTIFYING TOKENS: the export name, CLI flag, config key, file-convention string, or feature phrase a doc would mention. 2. Grep those tokens across every surface to see where the feature is (or should @@ -103,6 +105,8 @@ applies, then update or consciously skip each. ## Audit-mode procedure (sweep shipped work for drift) +An audit is the fan-out's best case: every surface of the map gets its own read-only `Explore` agent, all spawned in one message, so the sweep costs the slowest surface rather than the sum. Reads fan out, writes stay here. + Use this to find existing gaps (for example, across the Done items on the project board): diff --git a/.claude/skills/webjs-file-issue/SKILL.md b/.claude/skills/webjs-file-issue/SKILL.md index acf4babc9..18f9bfd54 100644 --- a/.claude/skills/webjs-file-issue/SKILL.md +++ b/.claude/skills/webjs-file-issue/SKILL.md @@ -39,6 +39,8 @@ If the user's description is very thin (e.g. "track adding dark mode as a todo") A few greps now save the implementing agent a cold-start investigation later, and the difference is visible in the filed issue. + **For an issue touching more than one area, fan the grounding out instead of hunting serially.** The four collections above are independent reads, so spawn parallel READ-ONLY `Explore` agents in ONE message, one per concern (where-to-edit paths; landmines via `git log` and prior-issue search; invariants from AGENTS.md; test and doc surfaces), then synthesize their returns into the Implementation-notes section yourself. Explore agents cannot write, which is the point: nothing edits the repo mid-filing. Inline grounding stays fine for a small single-area issue. Either way the gate is unchanged: a body that fails the cold-start test is unready, however it was researched. + **Cold-start test, apply it to your own draft before filing.** Reread the body as if you had never seen this conversation. Could you start work from it alone, without asking a single clarifying question and without first having to find where the relevant code lives? If not, it is not ready. Two concrete failure signs: the body names an area ("the router", "the prefetch logic") rather than a path, or it describes the symptom without saying where the fix goes. The user should never have to ask for this. If they do, the skill was not followed. diff --git a/.claude/skills/webjs-scaffold-sync/SKILL.md b/.claude/skills/webjs-scaffold-sync/SKILL.md index 75ccbe39a..7f53642e8 100644 --- a/.claude/skills/webjs-scaffold-sync/SKILL.md +++ b/.claude/skills/webjs-scaffold-sync/SKILL.md @@ -178,6 +178,8 @@ it applies, then update or consciously skip each. ## Per-change sync procedure +Run the mandatory generate + boot + check verifications in PARALLEL when more than one template is affected: each verification is its own background Bash task, launched in one batch, with EVERY task's result collected before the sync is declared done (a forgotten background boot is a silently unverified template). Prefer the in-process harness (`createRequestHandler` + `Request` objects, the dogfood boot-check pattern in the start-work skill): it binds no port, so parallel runs cannot collide. When a real `webjs dev` listen is unavoidable, give each boot a distinct `--port`. + 1. Identify the change's IDENTIFYING TOKENS: the demo route (`app/features/`, `app/api/features/`), the template name, the generated file path, the convention phrase, or the scoping phrase (e.g. "full-stack only"). @@ -213,6 +215,8 @@ it applies, then update or consciously skip each. ## Audit-mode procedure (sweep the scaffold for drift) +Fan the sweep out like the doc-sync audit: one read-only `Explore` agent per surface of the map, spawned in one message; the agents report and the edits stay in the main session. + 1. List the shipped scaffold changes (new demos, new template, scoping changes, changed generated files). 2. For each, pull its tokens and run the surface grep above. diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index e7d5e7eea..4d2608c38 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -329,7 +329,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun Each round must: -1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file, checking every 30 seconds. Take the path from the spawn's own tool result or task notification, NEVER a guess (a watchdog once aimed at the newest file in the tasks directory and watched its own output; if the harness returns no path, as for a named teammate, the watchdog degrades to a hard timer on the same thresholds). Fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) +1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. That is also why reviewers are ONE-SHOT, never persistent teammates: a teammate that keeps context across rounds re-derives its own prior conclusions, which is the blind-spot sharing this rule exists to kill. Teammates fit collaborating work; reviewing is the one job where accumulated context is the failure mode. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file, checking every 30 seconds. Take the path from the spawn's own tool result or task notification, NEVER a guess (a watchdog once aimed at the newest file in the tasks directory and watched its own output; if the harness returns no path, as for a named teammate, the watchdog degrades to a hard timer on the same thresholds). Fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For BOTH reviewer kinds, an Agent spawn and a Workflow run alike, the completion signal is the harness notification and the result it carries, and the mid-flight signal is the watchdog's activity probe on the task's output file (`TaskStop` works on either task id), so neither kind is ever a blind wait. A run whose harness status is killed or errored is a dead round exactly like a dead spawn. Transcript-file activity (mtime, growth) is harness evidence and may be probed; the WORDS in a transcript are an agent's own prose and still count for nothing. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. @@ -365,7 +365,7 @@ Each round must: 3. **If the round produced any actionable findings (a deep-review run's CONFIRMED list, or anything a delta verifier reports, including findings YOU then reject, since your own rejection is unadjudicated judgment in a way a jury's is not)**, resolve them and run a delta-scoped round: ONE fresh reviewer whose question is the fix commits' diff, with the full changed files as evidence and the blast-radius grep the round-shape rules mandate (when the round produced no fix commits, the question is the riskiest section instead). Fresh matters: on #1159, three consecutive rounds each found problems INTRODUCED by the previous round's fix, which is exactly what a re-used reviewer, already invested in that fix, is worst at seeing. If the delta round finds something, fix and repeat the delta shape; deep-review re-runs only when the round-shape rules call for it (a fix well outside the original diff, or a scope change). -4. **If the round reports `CLEAN`** (a deep-review run is clean only when it confirms nothing), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. +4. **If the round reports `CLEAN`** (a deep-review run is clean only when it confirms nothing), the loop is done, with one exception: a clean round 1 still gets the delta-shaped pass the minimum-two-rounds rule requires. Once done, NOW run everything the loop deferred: the full suites for every layer the change touches (e2e, Node, browser, Bun matrix, the four-app dogfood boot check, per the Definition of done), and only now read CI. Run them in PARALLEL: launch each suite as its own background Bash task in one batch, plus the CI read as a background watch (an `until` loop that exits when no check is pending), then collect EVERY launched task's result before reporting anything. The set is independent work, so its wall clock is the slowest suite rather than the sum, and a background task you forget to collect is a silently skipped layer, so the report waits for all of them. This changes only the shape; the WHEN-not-WHETHER rule above still decides what runs. The minimum is TWO rounds: deep-review, then at least one delta verify. A clean deep-review is rare and usually means the lenses were too shallow for this PR; before believing it, run one delta-shaped pass with the riskiest section as its question anyway. From d088e68273f9b18c5405c300933d280f52f11f0d Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:08:23 +0530 Subject: [PATCH 34/43] docs: the watchdog's mtime probe is meaningless for isolated agents A worktree-isolated agent's output file in the tasks directory is a static stub whose mtime never moves, so probing it reports a stall for every healthy isolated reviewer, and two were killed mid-review on exactly that misread before the CLEAN verdict arrived from the second. For isolated reviewers the watchdog now degrades to the hard timer alone, and only the hard cap or a killed/errored task status justifies a kill. --- .claude/skills/webjs-start-work/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 4d2608c38..0a999462c 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -329,7 +329,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun Each round must: -1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. That is also why reviewers are ONE-SHOT, never persistent teammates: a teammate that keeps context across rounds re-derives its own prior conclusions, which is the blind-spot sharing this rule exists to kill. Teammates fit collaborating work; reviewing is the one job where accumulated context is the failure mode. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file, checking every 30 seconds. Take the path from the spawn's own tool result or task notification, NEVER a guess (a watchdog once aimed at the newest file in the tasks directory and watched its own output; if the harness returns no path, as for a named teammate, the watchdog degrades to a hard timer on the same thresholds). Fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) +1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. That is also why reviewers are ONE-SHOT, never persistent teammates: a teammate that keeps context across rounds re-derives its own prior conclusions, which is the blind-spot sharing this rule exists to kill. Teammates fit collaborating work; reviewing is the one job where accumulated context is the failure mode. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file, checking every 30 seconds. Take the path from the spawn's own tool result or task notification, NEVER a guess (a watchdog once aimed at the newest file in the tasks directory and watched its own output). KNOW when the probe is meaningless: a worktree-isolated agent's output file is a static ~150-byte stub whose mtime never moves, so for isolated reviewers, and whenever the harness returns no usable path, the watchdog degrades to the hard timer alone, and a non-growing stub is NOT evidence of death (two healthy isolated reviewers were once killed on exactly that misread). Kill on the hard cap or a killed/errored task status, nothing softer. Fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For BOTH reviewer kinds, an Agent spawn and a Workflow run alike, the completion signal is the harness notification and the result it carries, and the mid-flight signal is the watchdog's activity probe on the task's output file (`TaskStop` works on either task id), so neither kind is ever a blind wait. A run whose harness status is killed or errored is a dead round exactly like a dead spawn. Transcript-file activity (mtime, growth) is harness evidence and may be probed; the WORDS in a transcript are an agent's own prose and still count for nothing. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. From 99e188ae0cdf52cd11ad4aaec42c5c6480b1bfac Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:10:10 +0530 Subject: [PATCH 35/43] docs: parallel investigation and verification in the last two skills Closes #1164: research-record investigations fan out one read-only Explore agent per option or angle, with the writeup and its judgment staying with the caller, and the where-the-record-lands rules untouched. Closes #1165: blog-write delegates the de-dup sweep to one Explore agent returning per-post summaries, and runs the dogfood verification's independent parts as one batch of background tasks, Node and Bun boots in parallel with both still mandatory, every result collected before a post is called verified, and browser-dependent probes kept in the main session. --- .claude/skills/webjs-blog-write/SKILL.md | 4 ++-- .claude/skills/webjs-research-record/SKILL.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.claude/skills/webjs-blog-write/SKILL.md b/.claude/skills/webjs-blog-write/SKILL.md index 16d97c961..225eadc25 100644 --- a/.claude/skills/webjs-blog-write/SKILL.md +++ b/.claude/skills/webjs-blog-write/SKILL.md @@ -90,8 +90,8 @@ The test before you ship: read the draft aloud and ask whether the author would ## Process: from topic to a published, true post 1. **Pick an SEO topic backed by shipped work.** Mine the git history, the merged PRs, the per-package changelog, and the closed issues for a real, shipped capability that a developer would search for and that no existing post already covers. High-intent angles (migration from another framework, a concrete how-to) rank best. -2. **De-duplicate against EVERY existing post, directly AND indirectly.** Read the titles and skim the bodies of all `blog/*.md`. A new post must not restate a published one, even partially. If two candidate topics overlap, either merge them or sharpen each to a distinct angle and cross-link rather than repeat. Two posts that share a mechanism should each own a different facet and reference the other, not re-explain it. -3. **For a feature post, verify every factual claim by dogfooding before you publish.** This is not optional for a post that describes runtime behaviour. Scaffold a real app (`webjs create`), boot it (on Node AND Bun where the claim is runtime-sensitive), and confirm each claim with a real probe: `curl` for HTTP and header behaviour, a browser for hydration, streaming, client-router, and a11y. If a claim turns out false, FIX the post to state the truth, and file a framework issue via `webjs-file-issue` if the framework itself is wrong. A blog post that states a behaviour the framework does not have is the failure this step exists to prevent. Do not report a post done off unverified claims. +2. **De-duplicate against EVERY existing post, directly AND indirectly.** Delegate the sweep to one read-only `Explore` agent over `blog/*.md` returning per-post topic-and-angle summaries (the corpus is small, so the win is keeping your context clean rather than parallelism), then judge overlap yourself against its return. A new post must not restate a published one, even partially. If two candidate topics overlap, either merge them or sharpen each to a distinct angle and cross-link rather than repeat. Two posts that share a mechanism should each own a different facet and reference the other, not re-explain it. +3. **For a feature post, verify every factual claim by dogfooding before you publish.** This is not optional for a post that describes runtime behaviour. Scaffold a real app (`webjs create`) and run the verification's independent parts as ONE batch of background Bash tasks: the Node boot and the Bun boot in parallel (BOTH stay mandatory where a claim is runtime-sensitive, one never satisfies the other), with the per-claim `curl` probes against whichever boot they need, and EVERY task's result collected before the post is called verified, since a forgotten background probe is an unverified claim shipping. Browser-dependent probes (hydration, streaming, client-router, a11y) stay in the main session, where the browser tooling lives. Confirm each claim with a real probe: `curl` for HTTP and header behaviour, a browser for hydration, streaming, client-router, and a11y. If a claim turns out false, FIX the post to state the truth, and file a framework issue via `webjs-file-issue` if the framework itself is wrong. A blog post that states a behaviour the framework does not have is the failure this step exists to prevent. Do not report a post done off unverified claims. 4. **Write the post** in the voice from Step 0, obeying the hard rules. 5. **Place the file** at repo-root `blog/.md`. Do not put it under `website/`. Confirm the front matter has `title` and `date` (the index drops files missing either). 6. **Self-check before committing.** Grep your own draft: `grep -nE '#[0-9]{3,4}' blog/.md` must return nothing, and scan for process tells ("dogfood", "self-review", "verified by"). Confirm no em-dash or banned pause punctuation. Read the whole post once as a reader who has never seen the codebase. diff --git a/.claude/skills/webjs-research-record/SKILL.md b/.claude/skills/webjs-research-record/SKILL.md index 0af99580b..d7a784703 100644 --- a/.claude/skills/webjs-research-record/SKILL.md +++ b/.claude/skills/webjs-research-record/SKILL.md @@ -26,6 +26,10 @@ The framework's reference docs (the skill at `.agents/skills/webjs/`) exist ONLY A research record has **no code diff**. A PR is a "merge this diff" object, so a research PR needs an empty-commit hack and leaves a dangling branch behind (see the now-deleted branch from #559). An issue is the native home for a writeup with threaded discussion. It is filterable by the `research` label, and it fits the project model where the GitHub Project board backed by issues is the source of truth. Earlier records used closed PRs (#546, #553); the convention going forward is a labeled issue. Do not convert the old PRs unless asked. +## Investigate in parallel + +A comparison or options analysis is multi-angle work, and each angle is an independent read: one option's codebase evidence, the prior art, the project's own constraints in AGENTS.md, what the ecosystem does. For a multi-option or multi-angle investigation, spawn one read-only `Explore` agent per option or angle in ONE message, each returning evidence with concrete file paths and citations, then synthesize yourself. Reads fan out; the writeup and its judgment stay with you, because the record's voice and conclusions are the caller's, not an agent vote. A single-question spike needs no fan-out. Nothing here changes WHERE the record lands (the closed `research`-labeled issue below). + ## Lifecycle: backlog item then findings in the SAME issue Research often starts as a planned **backlog item**, an OPEN `research`-labeled issue ("research whether X") sitting in the board's Todo column. When an agent actually does the research, the findings go into **that same issue**, NOT a new PR: From 5efc510a25ec4a5f5ae707a7acc5f4f4716bd998 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:14:29 +0530 Subject: [PATCH 36/43] docs: two watchdog modes, decided by what the output file is The stub caveat was one sentence patched into a paragraph whose every other sentence still described the transcript model: the mtime-fire clause fired on every healthy isolated reviewer, the stalled-gets- stopped and tail-peek sentences contradicted the nothing-softer kill rule, the 30-second cadence was decorative for the mandated shape, the liveness paragraph still promised a mid-flight probe for both kinds, and failure handling kept a stalled transcript as a kill trigger. The watchdog now has two explicit modes: transcript mode (the file grows, mtime probe, stall fires, TaskStop and re-spawn) and stub mode (the permanent healthy state of every isolated reviewer, hard timer alone, kill only on the cap or a killed/errored status), and the liveness promise is corrected from never-a-blind-wait to never-an-UNBOUNDED wait, which is the guarantee that was actually true all along. --- .claude/skills/webjs-start-work/SKILL.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 0a999462c..e945aa20b 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -329,9 +329,12 @@ The draft PR is already open (step 6), so reviews post to it from the first roun Each round must: -1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. That is also why reviewers are ONE-SHOT, never persistent teammates: a teammate that keeps context across rounds re-derives its own prior conclusions, which is the blind-spot sharing this rule exists to kill. Teammates fit collaborating work; reviewing is the one job where accumulated context is the failure mode. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's transcript file, checking every 30 seconds. Take the path from the spawn's own tool result or task notification, NEVER a guess (a watchdog once aimed at the newest file in the tasks directory and watched its own output). KNOW when the probe is meaningless: a worktree-isolated agent's output file is a static ~150-byte stub whose mtime never moves, so for isolated reviewers, and whenever the harness returns no usable path, the watchdog degrades to the hard timer alone, and a non-growing stub is NOT evidence of death (two healthy isolated reviewers were once killed on exactly that misread). Kill on the hard cap or a killed/errored task status, nothing softer. Fire when the file's mtime goes about 2 minutes without changing, or the run passes about 10 minutes total. When it fires, PEEK at harness evidence only: the task's status, and whether the transcript is still growing (a `tail` of its last couple of KB shows activity; it is NEVER read as a prose claim of liveness). A stalled reviewer gets stopped (`TaskStop`) and re-spawned per the re-spawn bullet; a progressing one gets left alone and the watchdog re-armed. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) +1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. That is also why reviewers are ONE-SHOT, never persistent teammates: a teammate that keeps context across rounds re-derives its own prior conclusions, which is the blind-spot sharing this rule exists to kill. Teammates fit collaborating work; reviewing is the one job where accumulated context is the failure mode. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's output file. Take the path from the spawn's own tool result or task notification, NEVER a guess (a watchdog once aimed at the newest file in the tasks directory and watched its own output). The watchdog has TWO MODES, decided by what that file actually is: + - **Transcript mode**, when the file demonstrably grows: probe its mtime every 30 seconds and fire on about 2 minutes of stall or 10 minutes total. On a fire, peek at harness evidence only (the task's status, whether the file is still growing; the words in any `tail` are NEVER read as a prose claim of liveness), stop a stalled reviewer (`TaskStop`) and re-spawn it per the re-spawn bullet, leave a progressing one alone. + - **Stub mode**, when the file is a static stub whose mtime never moves, which is the PERMANENT HEALTHY state of every worktree-isolated reviewer, and therefore the mode for every reviewer you spawn directly (two healthy isolated ones were once killed on exactly that misread): there is NO mid-flight activity signal, so the watchdog is the 10-minute hard timer alone, the harness's completion or killed notification is the only earlier news, and the wait is blind but BOUNDED, which is the point. Kill only on the hard cap or a killed/errored task status, nothing softer. + When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) - **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For BOTH reviewer kinds, an Agent spawn and a Workflow run alike, the completion signal is the harness notification and the result it carries, and the mid-flight signal is the watchdog's activity probe on the task's output file (`TaskStop` works on either task id), so neither kind is ever a blind wait. A run whose harness status is killed or errored is a dead round exactly like a dead spawn. Transcript-file activity (mtime, growth) is harness evidence and may be probed; the WORDS in a transcript are an agent's own prose and still count for nothing. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. + **Reviewer liveness (non-negotiable).** The harness status is the ONLY liveness signal. For BOTH reviewer kinds, an Agent spawn and a Workflow run alike, the completion signal is the harness notification and the result it carries, and the mid-flight signal is the watchdog in whichever mode the task's output file supports, an activity probe on a real transcript or the bare hard timer on a stub (`TaskStop` works on either task id), so neither kind is ever an UNBOUNDED wait. A run whose harness status is killed or errored is a dead round exactly like a dead spawn. Transcript-file activity (mtime, growth) is harness evidence where the file is a real transcript, and no signal at all where it is a static stub; the WORDS in a transcript are an agent's own prose and count for nothing in either mode. A spawn that was declined at the permission prompt, returned an internal error, or came back with an empty result means NOTHING is running and no review happened. Never read an agent's own prose as evidence it is alive: a background reviewer once signed off with "Yes, it's running. I have the diff and I'm partway through the careful pass" while its harness status read `killed`, and taking that at face value is exactly what turns a dead round into an unbounded wait. If a background reviewer is in play anyway (a `/code-review` agent, or a named agent from an earlier turn), its status is the harness completion notification for that task, which arrives on its own and carries the agent's real result, never what the agent last said, and a `killed` or errored status is terminal. Do not poll it, do not wait on it, do not re-read its message hoping for a different answer. `TaskList` is the shared to-do list, NOT a registry of running subagents, so an empty `TaskList` is evidence of nothing here. A dead STRAY background agent (a `/code-review` agent, a named agent from an earlier turn) is not by itself a blocked loop: it was never the round's reviewer, so write it off and run the round's real reviewer, the deep-review workflow for round 1 or the delta verifier after it. Only the round's own reviewer being unproducible blocks the loop. The rules below follow: recognise the failure, re-spawn it per the re-spawn bullet, and only a reviewer that cannot be produced at all blocks the loop. - **A failed spawn means the round did not happen.** Do not count it as a round, do not report it as a round, and do not let it advance the loop toward "last round clean". A failed spawn is not something to stop and report on its own, though: re-spawn it per the re-spawn bullet. Do still run the repo-health check below: a spawn that dies after its isolated worktree was created is the likeliest leaker of all, so that check keys off the SPAWN, not off a round completing. - **A reviewer that returns without reviewing is also a failed round.** A subagent can come back successfully and still have reviewed nothing ("I could not fetch the diff", a refusal, an answer to some other question, a summary of the PR body with no reading of the code). The prompt tells it to report `BLOCKED` plus what it is missing when it CANNOT review, which covers the could-not-fetch case; a refusal or an off-topic answer will not carry the sentinel, so do not rely on it alone: any answer that is not the reviewer's expected result shape is the same state, and the absence of findings is NOT a clean round. The expected shape is per reviewer kind: the delta verifier returns a finding list or the literal `CLEAN`; a deep-review run returns its structured result with `confirmed` and `rejected` lists, and anything else from it (prose, a partial result, nothing) means the round did not happen. Treat it exactly like a failed spawn: no round happened, so re-spawn per the re-spawn bullet, feeding it whatever the reviewer said it was missing (the diff, a path, the base branch). - **NEVER downgrade to an inline self-review.** The fresh subagent is required, not preferred, because reviewing your own work re-derives the same assumptions that produced the bug. That downgrade has already shipped three real bugs into a PR that two inline rounds had called clean (an unguarded `.action=${fn}` property path, a test-less branch the suite would not have missed, and a factually wrong commit message), every one of them caught by the first genuine subagent round that followed. An inline pass is strictly worse than no review, because it looks like one. @@ -464,5 +467,5 @@ A merge updates `main` and npm, but the four in-repo apps deployed to Railway (` - If `git status` is dirty: stop and ask the user to stash, commit, or abandon their pending work before creating the new branch. Never silently lose changes. - If the issue is already in `In progress` (someone else's work, or a prior branch left open): report this and ask the user whether to continue on the existing branch, branch off a fresh main, or pick a different issue. - If the local checkout regressed mid-loop (HEAD on `main` or a base commit, the feature branch's work seemingly "gone"): a review subagent mutated shared git state. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. -- If the round's reviewer cannot be produced (a deep-review run or a delta spawn that is declined at the permission prompt, errors, dies, hangs past its watchdog with a stalled transcript, or returns without its expected result shape): the round did not happen. Re-run it (re-invoke the workflow, re-spawn the verifier), varying the approach after a few identical failures, and do NOT stop mid-loop to report the failure or ask how to proceed: recovering costs seconds and interrupting costs the loop its momentum. Only a reviewer that cannot be produced at all blocks the loop, which is its one exit that is not a clean round; then withhold the flip to ready for review and say once, at the end, that the review did not run and why. Keep that out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. +- If the round's reviewer cannot be produced (a deep-review run or a delta spawn that is declined at the permission prompt, errors, dies, hangs past its watchdog, or returns without its expected result shape): the round did not happen. Re-run it (re-invoke the workflow, re-spawn the verifier), varying the approach after a few identical failures, and do NOT stop mid-loop to report the failure or ask how to proceed: recovering costs seconds and interrupting costs the loop its momentum. Only a reviewer that cannot be produced at all blocks the loop, which is its one exit that is not a clean round; then withhold the flip to ready for review and say once, at the end, that the review did not run and why. Keep that out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. - If the `gh project item-edit` call fails (auth scope, missing field): report the failure clearly and offer to do the move manually via the web UI. The branch creation still stands. From 71ade5a89c0211d2703f44ffca5583885515c444 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:16:48 +0530 Subject: [PATCH 37/43] docs: the headline promises bounded, not blind-free, like the rules under it --- .claude/skills/webjs-start-work/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index e945aa20b..65365be35 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -329,7 +329,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun Each round must: -1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. That is also why reviewers are ONE-SHOT, never persistent teammates: a teammate that keeps context across rounds re-derives its own prior conclusions, which is the blind-spot sharing this rule exists to kill. Teammates fit collaborating work; reviewing is the one job where accumulated context is the failure mode. **Async plus a watchdog, never a blind wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's output file. Take the path from the spawn's own tool result or task notification, NEVER a guess (a watchdog once aimed at the newest file in the tasks directory and watched its own output). The watchdog has TWO MODES, decided by what that file actually is: +1. **Run the round's reviewer: the round-1 deep-review workflow, or the single delta verifier** (the delta verifier via the Agent tool with `subagent_type: "general-purpose"` and `run_in_background: true`; round 1 via the Workflow tool, which also runs in the background and reports through its completion notification). A fresh reviewer has no prior context on the decisions you made, so it's less likely to share your blind spots. That is also why reviewers are ONE-SHOT, never persistent teammates: a teammate that keeps context across rounds re-derives its own prior conclusions, which is the blind-spot sharing this rule exists to kill. Teammates fit collaborating work; reviewing is the one job where accumulated context is the failure mode. **Async plus a watchdog, never an UNBOUNDED wait in either direction.** A synchronous spawn blocks the whole turn, so a hung reviewer is indistinguishable from a working one for as long as the harness takes to give up (a delta round that should cost 1 to 5 minutes once ate 20 this way, unwatchable from inside the blocked call). So spawn the reviewer in the background AND, immediately, start a background watchdog on the task's output file. Take the path from the spawn's own tool result or task notification, NEVER a guess (a watchdog once aimed at the newest file in the tasks directory and watched its own output). The watchdog has TWO MODES, decided by what that file actually is: - **Transcript mode**, when the file demonstrably grows: probe its mtime every 30 seconds and fire on about 2 minutes of stall or 10 minutes total. On a fire, peek at harness evidence only (the task's status, whether the file is still growing; the words in any `tail` are NEVER read as a prose claim of liveness), stop a stalled reviewer (`TaskStop`) and re-spawn it per the re-spawn bullet, leave a progressing one alone. - **Stub mode**, when the file is a static stub whose mtime never moves, which is the PERMANENT HEALTHY state of every worktree-isolated reviewer, and therefore the mode for every reviewer you spawn directly (two healthy isolated ones were once killed on exactly that misread): there is NO mid-flight activity signal, so the watchdog is the 10-minute hard timer alone, the harness's completion or killed notification is the only earlier news, and the wait is blind but BOUNDED, which is the point. Kill only on the hard cap or a killed/errored task status, nothing softer. When the reviewer completes first, stop the watchdog. (Backgrounding does not protect you from a declined spawn: a refusal lands on the tool call itself, and a Workflow call can be declined the same way.) From 80b484d946dc613ecd0c236fdc73433add30aeba Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:26:10 +0530 Subject: [PATCH 38/43] docs: hard-cap deep-review at 24 agents per run The run's worst case was ~41 agents (scout, twelve finders, full juries), which is a token bill nobody chose per review. A whole-run budget now caps it at 24 by default, overridable via maxAgents and clamped 8 to 60, degrading gracefully: the six fixed lenses always run, dynamic lenses trim to what remains after reserving four jury slots, juries allocate severity-first from the leftover, and a finding the budget cannot verify is returned in an unverified list with a log line, never silently dropped, per the no-silent-caps rule. Both return paths carry the unverified list and the effective cap in stats so a run's shape is auditable from its result. --- .claude/skills/webjs-start-work/SKILL.md | 2 +- .claude/workflows/deep-review.js | 41 +++++++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 65365be35..8a52368bf 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -323,7 +323,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: deep-review first, then delta-scoped verification.** Sequential broad rounds re-AUDIT the same surface N times; this shape audits it once, in parallel, then each later round asks only about what changed (while still holding the full changed files as evidence). -- **Round 1 is the committed deep-review workflow, for EVERY task that enters the loop** (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`). What it does: a scout reads the diff and proposes up to six dynamic lenses tailored to the PR's nature, which run in parallel alongside six fixed ones (correctness, security, blast radius, test adequacy, invariants and doc drift, fresh eyes; all split across opus and fable so two model families always read the diff), then an adversarial jury refutes each finding. Its CONFIRMED findings are the round's findings, the ones you act on; the jury-REJECTED ones come back too, carrying their refuters' reasons, but only for the audit trail (post them with the round per step 2's record rule; they are already adjudicated, need no action, and do not by themselves force another round). Uniform on purpose: its cost self-scales (juries convene per finding, so a quiet PR is cheap), and a uniform rule cannot be misjudged the way a by-risk bifurcation can, which matters because a `packages/*/src` bug ships into every end-user app. The whole run counts as ONE round, clean only when it confirms nothing, and its confirmed findings feed this same loop (fix, post as one review object, delta round). A deep-review run that dies or errors is a failed spawn under the liveness rules below: re-run it, and never substitute an inline pass. +- **Round 1 is the committed deep-review workflow, for EVERY task that enters the loop** (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`). What it does: a scout reads the diff and proposes up to six dynamic lenses tailored to the PR's nature, which run in parallel alongside six fixed ones (correctness, security, blast radius, test adequacy, invariants and doc drift, fresh eyes; all split across opus and fable so two model families always read the diff), then an adversarial jury refutes each finding. The whole run is HARD-CAPPED at 24 agents by default (`{ pr, maxAgents }` overrides, clamped 8 to 60): dynamic lenses trim first, jury sizes shrink next, and a finding the budget cannot verify comes back in an `unverified` list, never silently dropped, so raise the cap or re-run when that list is non-empty. Its CONFIRMED findings are the round's findings, the ones you act on; the jury-REJECTED ones come back too, carrying their refuters' reasons, but only for the audit trail (post them with the round per step 2's record rule; they are already adjudicated, need no action, and do not by themselves force another round). Uniform on purpose: its cost self-scales (juries convene per finding, so a quiet PR is cheap), and a uniform rule cannot be misjudged the way a by-risk bifurcation can, which matters because a `packages/*/src` bug ships into every end-user app. The whole run counts as ONE round, clean only when it confirms nothing, and its confirmed findings feed this same loop (fix, post as one review object, delta round). A deep-review run that dies or errors is a failed spawn under the liveness rules below: re-run it, and never substitute an inline pass. - **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape. Re-run deep-review in full, not a delta, when the surface round 1 audited is no longer the surface under review: a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough), OR the PR's SCOPE changed after round 1, new work folded in, a user-directed pivot, an issue absorbed mid-review. A delta round verifies fixes to an audited surface; it cannot stand in for the audit of a surface round 1 never saw. - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. diff --git a/.claude/workflows/deep-review.js b/.claude/workflows/deep-review.js index f2fa324d8..f64de3550 100644 --- a/.claude/workflows/deep-review.js +++ b/.claude/workflows/deep-review.js @@ -1,7 +1,7 @@ export const meta = { name: 'deep-review', description: 'Deep multi-agent PR review: parallel finder lenses, then adversarial verification; confirmed findings are actionable, jury-rejected ones return for the audit trail', - whenToUse: 'The round-1 review for any PR entering a review loop, or a heavyweight pass on demand. Works on any repo. Pass the PR number as args, "owner/repo#123" for another repository, or { pr, repo, lenses } where explicit lenses skip the scout.', + whenToUse: 'The round-1 review for any PR entering a review loop, or a heavyweight pass on demand. Works on any repo. Pass the PR number as args, "owner/repo#123" for another repository, or { pr, repo, lenses, maxAgents } where explicit lenses skip the scout and maxAgents caps the whole run (default 24).', phases: [ { title: 'Scope', detail: 'a scout reads the diff and proposes dynamic lenses for this PR' }, { title: 'Find', detail: 'six fixed lenses plus up to six dynamic ones, in parallel, split across opus and fable' }, @@ -25,6 +25,12 @@ else if (typeof args === 'string' && args.trim()) { repo = args.repo ? String(args.repo) : null } const givenLenses = (args && typeof args === 'object' && Array.isArray(args.lenses)) ? args.lenses : null +// Hard agent budget for the WHOLE run (scout + finders + jurors). Degrades +// gracefully: dynamic lenses are trimmed first, jury sizes shrink next, and a +// finding the budget cannot verify is returned marked unverified, never +// silently dropped. +const rawMax = (args && typeof args === 'object' && Number.isFinite(Number(args.maxAgents))) ? Number(args.maxAgents) : 24 +const MAX_AGENTS = Math.min(60, Math.max(8, Math.floor(rawMax))) if (!pr) throw new Error('Pass the PR number as args, e.g. Workflow({ name: "deep-review", args: "123" }), or "owner/repo#123", or { pr, repo }') const REPO_FLAG = repo ? ` --repo ${repo}` : '' @@ -109,11 +115,13 @@ const LENS_PROPOSALS = { } phase('Scope') +let spent = 0 let dynamic = [] if (givenLenses) { dynamic = givenLenses.slice(0, 6).map((l) => ({ key: String(l.key), prompt: String(l.prompt) })) log(`using ${dynamic.length} caller-provided dynamic lenses, scout skipped`) } else { + spent += 1 const scout = await agent( `${SAFETY} @@ -127,6 +135,13 @@ Each proposal is a key plus a one-paragraph reviewer charter written like an ord dynamic = ((scout && scout.lenses) || []).slice(0, 6) log(`scout proposed ${dynamic.length} dynamic lens(es)${dynamic.length ? ': ' + dynamic.map((l) => l.key).join(', ') : ''}`) } +// Budget: fixed lenses always run; dynamic ones fit in what remains after +// reserving at least 4 jury slots for the verify phase. +const dynamicAllowed = Math.max(0, MAX_AGENTS - spent - LENSES.length - 4) +if (dynamic.length > dynamicAllowed) { + log(`agent budget ${MAX_AGENTS}: trimming dynamic lenses ${dynamic.length} to ${dynamicAllowed}`) + dynamic = dynamic.slice(0, dynamicAllowed) +} const DYNAMIC = dynamic.map((l, i) => ({ key: `dyn-${l.key}`, prompt: l.prompt, model: i % 2 === 0 ? 'fable' : 'opus' })) const ALL_LENSES = [...LENSES, ...DYNAMIC] @@ -155,7 +170,7 @@ const toVerify = deduped.slice(0, CAP) if (deduped.length > CAP) log(`capping verification at ${CAP} of ${deduped.length} deduped findings (dropped ${deduped.length - CAP} lowest-severity; re-run after fixes to catch them)`) log(`${all.length} raw findings, ${deduped.length} after dedup, verifying ${toVerify.length}`) -if (toVerify.length === 0) return { pr, repo, confirmed: [], rejected: [], stats: { raw: all.length, deduped: deduped.length, verified: 0, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key) }, note: 'no findings survived dedup; treat as a clean deep pass' } +if (toVerify.length === 0) return { pr, repo, confirmed: [], rejected: [], unverified: [], stats: { raw: all.length, deduped: deduped.length, verified: 0, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key), maxAgents: MAX_AGENTS }, note: 'no findings survived dedup; treat as a clean deep pass' } phase('Verify') // Adaptive adversarial jury: 3 refuters for critical, 2 for major, 1 for minor. @@ -163,8 +178,23 @@ phase('Verify') // on a major finding survives (fail-open toward treating it as real). const juries = { critical: 3, major: 2, minor: 1 } -const verified = await parallel(toVerify.map((f) => () => - parallel(Array.from({ length: juries[f.severity] }, (_, i) => () => +// Allocate jurors within the remaining budget, severity-first (toVerify is +// already severity-sorted). A finding allocated zero jurors is returned +// UNVERIFIED rather than silently dropped. +spent += ALL_LENSES.length +let juryBudget = Math.max(0, MAX_AGENTS - spent) +const allocated = [] +const unverified = [] +for (const f of toVerify) { + const take = Math.min(juries[f.severity], juryBudget) + if (take === 0) { unverified.push(f); continue } + juryBudget -= take + allocated.push({ f, take }) +} +if (unverified.length) log(`agent budget ${MAX_AGENTS}: ${unverified.length} finding(s) returned unverified (no jury slots left); raise maxAgents or re-run after fixes`) + +const verified = await parallel(allocated.map(({ f, take }) => () => + parallel(Array.from({ length: take }, (_, i) => () => agent( `${SAFETY}\n\nYou are an adversarial verifier. A reviewer claims this defect in PR ${pr}:\n\nFILE: ${f.file}:${f.line}\nCLAIM: ${f.title}\nSCENARIO: ${f.detail}\n\nTry to REFUTE it. Read the actual code at head, trace the scenario, and decide whether the defect is real. Angle ${i + 1}: ${i === 0 ? 'does the claimed scenario actually reproduce in the code as written?' : i === 1 ? 'is the behavior actually correct or intended, making the claim a false positive?' : 'is this already guarded, tested, or handled somewhere the finder did not look?'} If you cannot confirm the defect is real, refuted=true.`, { label: `refute:${f.file.split('/').pop()}:${f.line}`, phase: 'Verify', schema: VERDICT }, @@ -188,6 +218,7 @@ return { repo, confirmed, rejected, - stats: { raw: all.length, deduped: deduped.length, verified: toVerify.length, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key) }, + unverified, + stats: { raw: all.length, deduped: deduped.length, verified: allocated.length, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key), maxAgents: MAX_AGENTS }, note: 'Confirmed findings feed the normal review loop: fix, post as one review object, then a delta round. Rejected ones carry their refuters\' reasons for the audit trail.', } From cd4c6853ee68c1575d4891fa727fa729f7e08c32 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:28:57 +0530 Subject: [PATCH 39/43] docs: the args comment and error message name every accepted key --- .claude/workflows/deep-review.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/workflows/deep-review.js b/.claude/workflows/deep-review.js index f64de3550..738744cbc 100644 --- a/.claude/workflows/deep-review.js +++ b/.claude/workflows/deep-review.js @@ -9,7 +9,7 @@ export const meta = { ], } -// args: a PR number ("123" or 123), an "owner/repo#123" string, or { pr, repo, lenses }. +// args: a PR number ("123" or 123), an "owner/repo#123" string, or { pr, repo, lenses, maxAgents }. // When no repo is given, every agent detects it from its own cwd via gh. // lenses: optional [{ key, prompt }] of extra lenses; when given, the scout is // skipped and these run as the dynamic set instead. @@ -31,7 +31,7 @@ const givenLenses = (args && typeof args === 'object' && Array.isArray(args.lens // silently dropped. const rawMax = (args && typeof args === 'object' && Number.isFinite(Number(args.maxAgents))) ? Number(args.maxAgents) : 24 const MAX_AGENTS = Math.min(60, Math.max(8, Math.floor(rawMax))) -if (!pr) throw new Error('Pass the PR number as args, e.g. Workflow({ name: "deep-review", args: "123" }), or "owner/repo#123", or { pr, repo }') +if (!pr) throw new Error('Pass the PR number as args, e.g. Workflow({ name: "deep-review", args: "123" }), or "owner/repo#123", or { pr, repo, lenses, maxAgents }') const REPO_FLAG = repo ? ` --repo ${repo}` : '' const REPO_NOTE = repo From 0869a293569b395b87e45d4ee73a193513f5c607 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:44:25 +0530 Subject: [PATCH 40/43] docs: one task per git worktree, always, hook-enforced Closes #1166. The worktree rule was conditional on concurrency, with a lone-agent plain-branch carve-out whose premise is unverifiable: an agent cannot know another session will not start mid-task, and this repo runs several a night. The start-work skill's steps 3 and 4 now cut a worktree from origin/main unconditionally (a dirty primary neither blocks nor gets fixed, since it is never edited at all), AGENTS.md states the rule without the carve-out, and the instagram skill roots its own worktree at the primary checkout so it cannot nest under a task worktree. Enforcement is real now rather than fictional: AGENTS.md pointed at .claude/hooks/guard-branch-context.sh, which does not exist in the repo. The new require-worktree-for-edits.sh hook blocks (exit 2) any tracked-file edit in a repo's PRIMARY checkout, judged against the FILE's directory rather than the session cwd, since the harness resets cwd to the primary while edits legitimately target worktree files by absolute path. Untracked and gitignored files stay editable, primary detection is the git-dir/git-common-dir equality invariant, and WEBJS_NO_WORKTREE_GATE=1 is the escape hatch, all covered by seven tests including the counterfactual block case. --- .claude/hooks/require-worktree-for-edits.sh | 41 ++++++++ .claude/settings.json | 11 ++- .claude/skills/webjs-instagram-post/SKILL.md | 4 +- .claude/skills/webjs-start-work/SKILL.md | 16 +--- AGENTS.md | 10 +- .../hooks/require-worktree-for-edits.test.mjs | 96 +++++++++++++++++++ 6 files changed, 159 insertions(+), 19 deletions(-) create mode 100755 .claude/hooks/require-worktree-for-edits.sh create mode 100644 test/hooks/require-worktree-for-edits.test.mjs diff --git a/.claude/hooks/require-worktree-for-edits.sh b/.claude/hooks/require-worktree-for-edits.sh new file mode 100755 index 000000000..e1e0bff7b --- /dev/null +++ b/.claude/hooks/require-worktree-for-edits.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# PreToolUse hook (Write|Edit|MultiEdit|NotebookEdit): BLOCK editing a TRACKED +# file that resides in a repo's PRIMARY checkout. Task work always happens in +# a linked worktree (AGENTS.md: one task per git worktree, ALWAYS); the +# primary checkout stays an untouched mirror of main. Untracked and gitignored +# files (session state, scratch, settings.local.json) stay editable. +# The predicate runs against the FILE's directory, never the session cwd: the +# harness resets cwd to the primary checkout between commands while edits +# legitimately target worktree files by absolute path. +# Contract: exit 0 = allow, exit 2 = block (message on stderr). +# Escape hatch: WEBJS_NO_WORKTREE_GATE=1. + +if [ "${WEBJS_NO_WORKTREE_GATE:-0}" = "1" ]; then exit 0; fi +if ! command -v jq >/dev/null 2>&1; then exit 0; fi + +input=$(cat) +fp=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty') +if [ -z "$fp" ]; then exit 0; fi + +dir=$(dirname "$fp") +if [ ! -d "$dir" ]; then exit 0; fi +if ! git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi + +gd=$(git -C "$dir" rev-parse --git-dir 2>/dev/null) || exit 0 +gcd=$(git -C "$dir" rev-parse --git-common-dir 2>/dev/null) || exit 0 +# Linked worktree: --git-dir carries /worktrees/ and differs from the +# common dir. Only the primary checkout has them equal. +if [ "$gd" != "$gcd" ]; then exit 0; fi + +# Untracked or gitignored files are session-local and stay editable. +if ! git -C "$dir" ls-files --error-unmatch -- "$fp" >/dev/null 2>&1; then exit 0; fi + +top=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null || echo "this repo") +{ + echo "BLOCKED: '$fp' is a tracked file in the PRIMARY checkout ($top)." + echo "Task work always happens in a linked worktree (AGENTS.md: one task per git worktree, ALWAYS):" + echo " git worktree add -b / ../- origin/main" + echo "Work there by absolute path; the primary checkout stays on main untouched." + echo "Escape hatch for a deliberate exception: WEBJS_NO_WORKTREE_GATE=1." +} >&2 +exit 2 diff --git a/.claude/settings.json b/.claude/settings.json index b3bb3ff9e..d268dc468 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -16,6 +16,15 @@ } ], "PreToolUse": [ + { + "matcher": "Write|Edit|MultiEdit|NotebookEdit", + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/require-worktree-for-edits.sh" + } + ] + }, { "matcher": "Write|Edit|MultiEdit", "hooks": [ @@ -81,4 +90,4 @@ } ] } -} +} \ No newline at end of file diff --git a/.claude/skills/webjs-instagram-post/SKILL.md b/.claude/skills/webjs-instagram-post/SKILL.md index d83f697f2..9c11d7dbd 100644 --- a/.claude/skills/webjs-instagram-post/SKILL.md +++ b/.claude/skills/webjs-instagram-post/SKILL.md @@ -111,10 +111,10 @@ only has to exist for that one call, then it is deleted. ```sh SLUG=works-without-javascript # match the post topic BR=chore/ig-social-$SLUG -git worktree add -b "$BR" ../webjs-ig-social origin/main +git -C "$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')" worktree add -b "$BR" ../webjs-ig-social origin/main # rooted at the PRIMARY checkout, so it never nests under a task worktree mkdir -p ../webjs-ig-social/website/public/social cp "$OUT" ../webjs-ig-social/website/public/social/$SLUG.jpg -( cd ../webjs-ig-social +( cd "$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')/../webjs-ig-social" git add website/public/social/$SLUG.jpg git commit -q -m "chore: add $SLUG Instagram social card asset" git push -q -u origin "$BR" ) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 8a52368bf..70afb6c9c 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -63,22 +63,16 @@ The user's request typically names an issue by number (e.g. `#112`) or by descri If not present, add it: `gh project item-add 1 --owner webjsdev --url https://github.com/webjsdev/webjs/issues/`. -3. **Verify we're on `main` with a clean tree and pull latest.** If the working tree is dirty or already on a feature branch, STOP and ask the user how to proceed. Do not silently abandon their work. +3. **Fetch, and leave the primary checkout alone.** `git fetch origin`. The task's worktree cuts from `origin/main`, so a dirty or mid-something primary checkout neither blocks starting nor gets "fixed"; it is never edited at all (enforced by `.claude/hooks/require-worktree-for-edits.sh`, which blocks tracked-file edits in a primary checkout). - ```sh - git status --porcelain # must be empty - git branch --show-current # must be main - git pull origin main - ``` - -4. **Create the feature branch AND push it to origin immediately.** Pick the prefix from the issue labels: `enhancement` to `feat/`, `bug` to `fix/`, `documentation` to `docs/`, otherwise `chore/`. Build the slug from the issue title (lowercase, kebab-case, max 30 chars, drop conjunctions). The empty branch goes to GitHub right away so the work survives any local-machine failure even before the first commit. +4. **Create the task's WORKTREE and push its branch immediately.** One task, one worktree, ALWAYS; there is no lone-agent plain-branch path, because "no other agent is active" is unverifiable mid-task. Pick the prefix from the issue labels: `enhancement` to `feat/`, `bug` to `fix/`, `documentation` to `docs/`, otherwise `chore/`. Build the slug from the issue title (lowercase, kebab-case, max 30 chars, drop conjunctions). The push happens right away so the work survives any local-machine failure even before the first commit. ```sh - git checkout -b / - git push -u origin / + git worktree add -b / ../- origin/main + git -C ../- push -u origin / ``` - After this step, ALSO push after every subsequent commit (`git push` is cheap and is the safety net against losing work). Do not batch multiple commits before pushing. + ALL work for the task happens inside that worktree, by absolute path when the session's cwd resets. A fresh worktree has NO `node_modules`; see AGENTS.md for the symlink remedy (#954). Cleanup after merge is automatic (`cleanup-merged-worktree.sh`). After this step, ALSO push after every subsequent commit (`git push` is cheap and is the safety net against losing work). Do not batch multiple commits before pushing. 5. **Move the project card from Todo to In progress.** Resolve the four IDs and call `item-edit`: diff --git a/AGENTS.md b/AGENTS.md index d39d9333c..9f1162743 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,15 +37,15 @@ itself): commands, repo-health git config, changelog flow, dev error overlay. ### Before starting ANY work: verify and sync the branch -1. `git branch --show-current`. If on `main` / `master`, **STOP** and `git checkout -b feature/`. +1. Work NEVER happens in the primary checkout. Cut the task's worktree first: `git worktree add -b / ../- origin/main`, and do ALL work there. 2. Verify the branch matches the task. Don't mix unrelated work. 3. Sync with parent: `git fetch origin && git log HEAD..origin/main --oneline`. If there are upstream commits, `git rebase origin/main` first. -Claude Code enforces step 1 via `.claude/hooks/guard-branch-context.sh`. Other agents check manually. +Claude Code enforces step 1 via `.claude/hooks/require-worktree-for-edits.sh` (blocks tracked-file edits in a primary checkout; test `test/hooks/require-worktree-for-edits.test.mjs`, escape hatch `WEBJS_NO_WORKTREE_GATE=1`). Other agents check manually. -### One task per git worktree when agents run concurrently +### One task per git worktree, ALWAYS -WebJs is worked by MULTIPLE agents at once. If more than one agent (or more than one in-flight task) shares ONE working checkout, they collide: a `git checkout` in one moves `HEAD` under the other, so the next commit lands on the WRONG branch. This has happened (a `chore: release` commit landed on an unrelated `feat/` branch, with a contaminated changelog). So give each task its own worktree: +WebJs is worked by MULTIPLE agents at once, and "no other agent is active right now" is unverifiable mid-task (another session can start any minute), so the worktree rule is unconditional. If more than one agent (or more than one in-flight task) shares ONE working checkout, they collide: a `git checkout` in one moves `HEAD` under the other, so the next commit lands on the WRONG branch. This has happened (a `chore: release` commit landed on an unrelated `feat/` branch, with a contaminated changelog). So give each task its own worktree: ```sh git worktree add -b / ../- origin/main @@ -55,7 +55,7 @@ cd ../- # do ALL work for the task here **A fresh worktree has NO `node_modules`** (git worktrees do not copy it), so running an app from one (`webjs dev` / `webjs start`, the test runner, a scaffolded app) fails to resolve `@webjsdev/*` until you install or link it. `webjs doctor` warns for this exact case (#954) and `webjs dev` / `webjs start` print the cause + remedy instead of a raw `ERR_MODULE_NOT_FOUND`. Fix by installing in the worktree (`npm install`) or symlinking the primary checkout's modules (`ln -s ..//node_modules node_modules`). When only a subset of `@webjsdev/*` packages was edited, link those from the worktree and the rest from the primary checkout so a built `dist/` (e.g. `@webjsdev/core`) still resolves. -Git enforces one-branch-per-worktree, so separate worktrees make the collision impossible. Before any commit in a shared checkout, confirm `git branch --show-current` is still the branch you created; if it moved, you are colliding, switch to a worktree. A lone agent in a clean checkout may still use a plain branch. The repo's `.hooks/pre-commit` additionally BLOCKS a published-library (`core`/`server`/`cli`/`mcp`/`ui`/`intellisense`) version bump on any non-`chore/release-*` branch, the canonical wrong-branch-release symptom. +Git enforces one-branch-per-worktree, so separate worktrees make the collision impossible. There is NO lone-agent exception: every task cuts a worktree, and the primary checkout stays an untouched mirror of main (tracked-file edits there are hook-blocked). The repo's `.hooks/pre-commit` additionally BLOCKS a published-library (`core`/`server`/`cli`/`mcp`/`ui`/`intellisense`) version bump on any non-`chore/release-*` branch, the canonical wrong-branch-release symptom. **Cleanup is automatic after a merge.** The `.claude/hooks/cleanup-merged-worktree.sh` PostToolUse hook fires after any `gh pr merge` and removes each linked worktree whose branch is merged AND whose tree is clean, so a merged branch's worktree never leaks (accumulated stale worktrees are exactly what it prevents). It is conservative: it KEEPS anything with uncommitted changes, an unmerged branch, or the worktree you ran the merge from (you cannot remove your current directory, so `cd` out and `git worktree remove` it yourself), and never touches the primary checkout. Disable with `WEBJS_NO_WORKTREE_CLEANUP=1`. Test: `test/hooks/cleanup-merged-worktree.test.mjs`. diff --git a/test/hooks/require-worktree-for-edits.test.mjs b/test/hooks/require-worktree-for-edits.test.mjs new file mode 100644 index 000000000..7ed892193 --- /dev/null +++ b/test/hooks/require-worktree-for-edits.test.mjs @@ -0,0 +1,96 @@ +// The require-worktree-for-edits hook blocks tracked-file edits in a repo's +// PRIMARY checkout and allows everything else: the same file in a linked +// worktree, untracked files, gitignored files, non-repo paths, and the +// WEBJS_NO_WORKTREE_GATE=1 escape hatch. AGENTS.md: one task per git +// worktree, ALWAYS. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync, execSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'; +import { join, dirname, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; +import { fileURLToPath } from 'node:url'; + +const HOOK = resolve( + dirname(fileURLToPath(import.meta.url)), + '../../.claude/hooks/require-worktree-for-edits.sh' +); + +function runHook(filePath, env = {}) { + return spawnSync('bash', [HOOK], { + input: JSON.stringify({ tool_input: { file_path: filePath } }), + env: { ...process.env, ...env }, + encoding: 'utf8', + }); +} + +function makeRepo() { + const dir = mkdtempSync(join(tmpdir(), 'wt-gate-')); + const g = (cmd) => execSync(`git ${cmd}`, { cwd: dir, stdio: 'pipe' }); + g('init -q -b main'); + g('config user.email t@t'); + g('config user.name t'); + writeFileSync(join(dir, 'tracked.txt'), 'hello\n'); + writeFileSync(join(dir, '.gitignore'), 'ignored.txt\n'); + g('add .'); + g('commit -q -m init'); + return dir; +} + +test('tracked file in the PRIMARY checkout is blocked (counterfactual: the gate fires)', () => { + const repo = makeRepo(); + try { + const r = runHook(join(repo, 'tracked.txt')); + assert.equal(r.status, 2, `expected block, got ${r.status}: ${r.stderr}`); + assert.match(r.stderr, /PRIMARY checkout/); + assert.match(r.stderr, /git worktree add/); + } finally { rmSync(repo, { recursive: true, force: true }); } +}); + +test('the same tracked file in a LINKED worktree is allowed', () => { + const repo = makeRepo(); + const wt = `${repo}-wt`; + try { + execSync(`git worktree add -q -b feat/x ${wt}`, { cwd: repo, stdio: 'pipe' }); + const r = runHook(join(wt, 'tracked.txt')); + assert.equal(r.status, 0, r.stderr); + } finally { + execSync(`git worktree remove --force ${wt}`, { cwd: repo, stdio: 'pipe' }); + rmSync(repo, { recursive: true, force: true }); + } +}); + +test('an untracked file in the primary checkout is allowed', () => { + const repo = makeRepo(); + try { + writeFileSync(join(repo, 'scratch-note.md'), 'x'); + assert.equal(runHook(join(repo, 'scratch-note.md')).status, 0); + } finally { rmSync(repo, { recursive: true, force: true }); } +}); + +test('a gitignored file in the primary checkout is allowed', () => { + const repo = makeRepo(); + try { + writeFileSync(join(repo, 'ignored.txt'), 'x'); + assert.equal(runHook(join(repo, 'ignored.txt')).status, 0); + } finally { rmSync(repo, { recursive: true, force: true }); } +}); + +test('a path outside any git repo is allowed', () => { + const dir = mkdtempSync(join(tmpdir(), 'wt-norepo-')); + try { + writeFileSync(join(dir, 'f.txt'), 'x'); + assert.equal(runHook(join(dir, 'f.txt')).status, 0); + } finally { rmSync(dir, { recursive: true, force: true }); } +}); + +test('WEBJS_NO_WORKTREE_GATE=1 bypasses the block', () => { + const repo = makeRepo(); + try { + assert.equal(runHook(join(repo, 'tracked.txt'), { WEBJS_NO_WORKTREE_GATE: '1' }).status, 0); + } finally { rmSync(repo, { recursive: true, force: true }); } +}); + +test('a nonexistent-directory path is allowed (new file in a new dir elsewhere)', () => { + assert.equal(runHook('/nonexistent-dir-xyz/f.txt').status, 0); +}); From 13d709fa833834f900873156a3ca82f013122b8d Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:53:06 +0530 Subject: [PATCH 41/43] fix: the worktree gate now guards subdirectories, not just the root The review ran the hook itself and caught it failing open for every tracked file in a SUBDIRECTORY of the primary checkout: invoked via -C on a subdir, git prints --git-dir absolute and --git-common-dir relative (../../.git), so the raw string compare misclassified the primary as a linked worktree, and the gate guarded only root-level files while the tests stayed green by testing exactly those. Both paths now resolve through --path-format=absolute, the test repo carries a nested tracked file whose block case fails against the old hook, and the escape-hatch env var is pinned off in non-bypass tests so an inherited value cannot flip the assertions. Also from the round: the instagram recipe roots every path (mkdir, cp, cd, teardown) at one captured PRIMARY variable instead of half at the primary and half at the session cwd; the start-work skill's three surviving checkout-in-place passages now describe the worktree model, where HEAD-on-main is the primary's healthy state and the task worktree is the only holder of its branch; and AGENTS.md's autonomous-mode line cuts a worktree from origin/main instead of branching in place with the retired feature/ prefix. --- .claude/hooks/require-worktree-for-edits.sh | 10 ++++++++-- .claude/settings.json | 2 +- .claude/skills/webjs-instagram-post/SKILL.md | 12 +++++++----- .claude/skills/webjs-start-work/SKILL.md | 6 +++--- AGENTS.md | 2 +- test/hooks/require-worktree-for-edits.test.mjs | 16 +++++++++++++++- 6 files changed, 35 insertions(+), 13 deletions(-) diff --git a/.claude/hooks/require-worktree-for-edits.sh b/.claude/hooks/require-worktree-for-edits.sh index e1e0bff7b..941e0a5b1 100755 --- a/.claude/hooks/require-worktree-for-edits.sh +++ b/.claude/hooks/require-worktree-for-edits.sh @@ -21,8 +21,14 @@ dir=$(dirname "$fp") if [ ! -d "$dir" ]; then exit 0; fi if ! git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi -gd=$(git -C "$dir" rev-parse --git-dir 2>/dev/null) || exit 0 -gcd=$(git -C "$dir" rev-parse --git-common-dir 2>/dev/null) || exit 0 +# Both paths ABSOLUTE, or the comparison lies: invoked via -C on a SUBDIR of +# the primary, git prints --git-dir absolute and --git-common-dir relative +# (../../.git), so a raw string compare misclassifies the primary as a linked +# worktree and the gate fails open for the whole source tree (caught in +# review; the tests only covered root-level files). +gd=$(git -C "$dir" rev-parse --path-format=absolute --git-dir 2>/dev/null) || exit 0 +gcd=$(git -C "$dir" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || exit 0 +if [ -z "$gd" ] || [ -z "$gcd" ]; then exit 0; fi # Linked worktree: --git-dir carries /worktrees/ and differs from the # common dir. Only the primary checkout has them equal. if [ "$gd" != "$gcd" ]; then exit 0; fi diff --git a/.claude/settings.json b/.claude/settings.json index d268dc468..7a0487212 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -90,4 +90,4 @@ } ] } -} \ No newline at end of file +} diff --git a/.claude/skills/webjs-instagram-post/SKILL.md b/.claude/skills/webjs-instagram-post/SKILL.md index 9c11d7dbd..40cca01ff 100644 --- a/.claude/skills/webjs-instagram-post/SKILL.md +++ b/.claude/skills/webjs-instagram-post/SKILL.md @@ -111,10 +111,12 @@ only has to exist for that one call, then it is deleted. ```sh SLUG=works-without-javascript # match the post topic BR=chore/ig-social-$SLUG -git -C "$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')" worktree add -b "$BR" ../webjs-ig-social origin/main # rooted at the PRIMARY checkout, so it never nests under a task worktree -mkdir -p ../webjs-ig-social/website/public/social -cp "$OUT" ../webjs-ig-social/website/public/social/$SLUG.jpg -( cd "$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')/../webjs-ig-social" +PRIMARY=$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}') # every path below roots here, so nothing nests under a task worktree or lands beside the session cwd +git -C "$PRIMARY" worktree add -b "$BR" ../webjs-ig-social origin/main +IG="$PRIMARY/../webjs-ig-social" +mkdir -p "$IG/website/public/social" +cp "$OUT" "$IG/website/public/social/$SLUG.jpg" +( cd "$IG" git add website/public/social/$SLUG.jpg git commit -q -m "chore: add $SLUG Instagram social card asset" git push -q -u origin "$BR" ) @@ -126,7 +128,7 @@ IMG="https://raw.githubusercontent.com/webjsdev/webjs/$BR/website/public/social/ After the post is published in Step 4, tear the branch down so nothing lingers: ```sh -git worktree remove ../webjs-ig-social --force +git -C "$PRIMARY" worktree remove ../webjs-ig-social --force # PRIMARY from the setup block above git branch -D "$BR" git push origin --delete "$BR" ``` diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 70afb6c9c..cb47b795a 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -340,7 +340,7 @@ Each round must: - For ALL reviewers, the read-only git prohibition in the prompt (baked into the delta template below and into deep-review's own agent preambles; the workflow's internal agents run on this defense alone, which is why the check below matters after its runs too). Belt and suspenders where both apply: isolation contains the working-tree half of the damage, the prompt prevents the attempt at the shared half that isolation cannot reach. - After EACH spawn or workflow run resolves, before acting on findings, run a one-line repo-health check. Every one, not every round: a spawn that was declined, errored, or died did not produce a round but may well have created a worktree first, so it needs the check MORE than a clean return does. Run it and repair if needed (the worktree isolation in the first bullet has itself corrupted the main repo: spawning isolated reviewers flipped the main repo's `core.bare` to `true` and left a locked `.claude/worktrees/agent-*` worktree, after which `git checkout` failed with "this operation must be run in a work tree"). Check ALL of: - `git rev-parse --is-inside-work-tree` is `true` and `git config --get core.bare` is NOT `true`. If the work tree is broken, repair with `git config core.bare false`, then `git worktree prune` (and `git worktree remove -f -f .claude/worktrees/agent-*` for any locked leftover). Do NOT touch the user's OWN unrelated worktrees (anything outside `.claude/worktrees/`). - - `git rev-parse --abbrev-ref HEAD` is still the feature branch and `git status` is clean. If HEAD moved (or points at a now-deleted branch / `0000000`), `git checkout -f ` (or `main` post-merge) restores it. + - In the TASK's worktree, `git rev-parse --abbrev-ref HEAD` is still the feature branch and `git status` is clean (`git -C ...`; the worktree itself holds the branch, so a checkout there cannot collide). In the PRIMARY checkout the healthy HEAD is `main`; a primary sitting on anything else means something mutated it, so put it back with `git checkout -f main`. Never try `git checkout -f ` in the primary: the task worktree holds that branch and git refuses. The merge / commits are always safe on GitHub regardless; this only repairs the LOCAL repo. Nothing is lost. If you cannot cleanly repair, `git checkout -f main && git pull` to resync, then continue. For the DELTA verifier (round 1's prompts live inside the deep-review workflow), pass a prompt that: @@ -458,8 +458,8 @@ A merge updates `main` and npm, but the four in-repo apps deployed to Railway (` ## Failure handling -- If `git status` is dirty: stop and ask the user to stash, commit, or abandon their pending work before creating the new branch. Never silently lose changes. +- If the TASK worktree's `git status` is dirty at start (a prior session died mid-work in it): stop and ask the user to commit, stash, or abandon that work. Never silently lose changes. A dirty PRIMARY checkout is not a blocker and not yours to fix; the worktree cuts from `origin/main` regardless. - If the issue is already in `In progress` (someone else's work, or a prior branch left open): report this and ask the user whether to continue on the existing branch, branch off a fresh main, or pick a different issue. -- If the local checkout regressed mid-loop (HEAD on `main` or a base commit, the feature branch's work seemingly "gone"): a review subagent mutated shared git state. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. +- If the TASK worktree regressed mid-loop (its HEAD detached or off the feature branch, work seemingly "gone"): a review subagent mutated shared git state. In the PRIMARY, HEAD on `main` is the healthy state, not a regression. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. - If the round's reviewer cannot be produced (a deep-review run or a delta spawn that is declined at the permission prompt, errors, dies, hangs past its watchdog, or returns without its expected result shape): the round did not happen. Re-run it (re-invoke the workflow, re-spawn the verifier), varying the approach after a few identical failures, and do NOT stop mid-loop to report the failure or ask how to proceed: recovering costs seconds and interrupting costs the loop its momentum. Only a reviewer that cannot be produced at all blocks the loop, which is its one exit that is not a clean round; then withhold the flip to ready for review and say once, at the end, that the review did not run and why. Keep that out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. - If the `gh project item-edit` call fails (auth scope, missing field): report the failure clearly and offer to do the move manually via the web UI. The branch creation still stands. diff --git a/AGENTS.md b/AGENTS.md index 9f1162743..faaf9649e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,7 +65,7 @@ A Skill is model-invoked, so it fires only when the model judges a match. The `. ### Autonomous mode (sandbox / bypass permissions) -When interactive approval is disabled, never block on questions. Auto-decide: on `main`, auto-create `feature/`; auto-rebase if the parent moved; auto-merge when ready; **delete** feature/fix branches after merge but **keep** long-lived ones (dev, staging, release/*); auto-generate meaningful commit messages; fix failing tests / convention violations rather than asking. Autonomous mode is MORE disciplined, not less, with the same quality bar. +When interactive approval is disabled, never block on questions. Auto-decide: cut the task's worktree from `origin/main` (auto-create `/` per the label scheme); auto-rebase if the parent moved; auto-merge when ready; **delete** feature/fix branches after merge but **keep** long-lived ones (dev, staging, release/*); auto-generate meaningful commit messages; fix failing tests / convention violations rather than asking. Autonomous mode is MORE disciplined, not less, with the same quality bar. ### Code workflow (mandatory) diff --git a/test/hooks/require-worktree-for-edits.test.mjs b/test/hooks/require-worktree-for-edits.test.mjs index 7ed892193..f15481740 100644 --- a/test/hooks/require-worktree-for-edits.test.mjs +++ b/test/hooks/require-worktree-for-edits.test.mjs @@ -19,7 +19,9 @@ const HOOK = resolve( function runHook(filePath, env = {}) { return spawnSync('bash', [HOOK], { input: JSON.stringify({ tool_input: { file_path: filePath } }), - env: { ...process.env, ...env }, + // Pin the escape hatch off so an inherited WEBJS_NO_WORKTREE_GATE=1 in the + // invoking environment cannot silently flip the block assertions. + env: { ...process.env, WEBJS_NO_WORKTREE_GATE: '0', ...env }, encoding: 'utf8', }); } @@ -31,6 +33,8 @@ function makeRepo() { g('config user.email t@t'); g('config user.name t'); writeFileSync(join(dir, 'tracked.txt'), 'hello\n'); + mkdirSync(join(dir, 'sub', 'deep'), { recursive: true }); + writeFileSync(join(dir, 'sub', 'deep', 'nested.txt'), 'hello\n'); writeFileSync(join(dir, '.gitignore'), 'ignored.txt\n'); g('add .'); g('commit -q -m init'); @@ -47,6 +51,14 @@ test('tracked file in the PRIMARY checkout is blocked (counterfactual: the gate } finally { rmSync(repo, { recursive: true, force: true }); } }); +test('a tracked file in a SUBDIRECTORY of the primary is blocked (git prints git-dir absolute and common-dir relative there, the fail-open the review caught)', () => { + const repo = makeRepo(); + try { + const r = runHook(join(repo, 'sub', 'deep', 'nested.txt')); + assert.equal(r.status, 2, `expected block, got ${r.status}: ${r.stderr}`); + } finally { rmSync(repo, { recursive: true, force: true }); } +}); + test('the same tracked file in a LINKED worktree is allowed', () => { const repo = makeRepo(); const wt = `${repo}-wt`; @@ -54,6 +66,8 @@ test('the same tracked file in a LINKED worktree is allowed', () => { execSync(`git worktree add -q -b feat/x ${wt}`, { cwd: repo, stdio: 'pipe' }); const r = runHook(join(wt, 'tracked.txt')); assert.equal(r.status, 0, r.stderr); + const rSub = runHook(join(wt, 'sub', 'deep', 'nested.txt')); + assert.equal(rSub.status, 0, rSub.stderr); } finally { execSync(`git worktree remove --force ${wt}`, { cwd: repo, stdio: 'pipe' }); rmSync(repo, { recursive: true, force: true }); From 43edc480f3f22fcc767f71a59b9f2e13979eaed8 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 23:59:42 +0530 Subject: [PATCH 42/43] docs: teardown re-derives its variables, recovery anchors its checkout The instagram teardown's rooting was illusory: Step 4's user confirmation sits between setup and teardown, shell state does not survive across tool calls, and git -C "" silently degrades to cwd-relative, so the teardown was exactly the unrooted form the prior round flagged. It now re-derives PRIMARY and BR at the top of its own block, and the derivation swaps awk for sed since awk splits a path containing spaces. The start-work recovery command anchors its checkout to the task worktree: run bare from the primary it would succeed and park the PRIMARY on the feature branch, because a detached worktree no longer holds the branch and the git-refuses safety net does not apply. --- .claude/skills/webjs-instagram-post/SKILL.md | 9 +++++++-- .claude/skills/webjs-start-work/SKILL.md | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.claude/skills/webjs-instagram-post/SKILL.md b/.claude/skills/webjs-instagram-post/SKILL.md index 40cca01ff..712300287 100644 --- a/.claude/skills/webjs-instagram-post/SKILL.md +++ b/.claude/skills/webjs-instagram-post/SKILL.md @@ -111,7 +111,7 @@ only has to exist for that one call, then it is deleted. ```sh SLUG=works-without-javascript # match the post topic BR=chore/ig-social-$SLUG -PRIMARY=$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}') # every path below roots here, so nothing nests under a task worktree or lands beside the session cwd +PRIMARY=$(git worktree list --porcelain | sed -n 's/^worktree //p' | head -1) # every path below roots here, so nothing nests under a task worktree or lands beside the session cwd (sed, not awk: awk splits a path containing spaces) git -C "$PRIMARY" worktree add -b "$BR" ../webjs-ig-social origin/main IG="$PRIMARY/../webjs-ig-social" mkdir -p "$IG/website/public/social" @@ -128,7 +128,12 @@ IMG="https://raw.githubusercontent.com/webjsdev/webjs/$BR/website/public/social/ After the post is published in Step 4, tear the branch down so nothing lingers: ```sh -git -C "$PRIMARY" worktree remove ../webjs-ig-social --force # PRIMARY from the setup block above +# Shell state does NOT survive across tool calls, and Step 4's user +# confirmation sits between setup and here, so re-derive BOTH variables +# (an empty $PRIMARY silently degrades `git -C ""` to cwd-relative). +PRIMARY=${PRIMARY:-$(git worktree list --porcelain | sed -n 's/^worktree //p' | head -1)} +BR=${BR:-chore/ig-social-} # fill the slug if the variable is gone +git -C "$PRIMARY" worktree remove ../webjs-ig-social --force git branch -D "$BR" git push origin --delete "$BR" ``` diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index cb47b795a..a1cea0f8b 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -460,6 +460,6 @@ A merge updates `main` and npm, but the four in-repo apps deployed to Railway (` - If the TASK worktree's `git status` is dirty at start (a prior session died mid-work in it): stop and ask the user to commit, stash, or abandon that work. Never silently lose changes. A dirty PRIMARY checkout is not a blocker and not yours to fix; the worktree cuts from `origin/main` regardless. - If the issue is already in `In progress` (someone else's work, or a prior branch left open): report this and ask the user whether to continue on the existing branch, branch off a fresh main, or pick a different issue. -- If the TASK worktree regressed mid-loop (its HEAD detached or off the feature branch, work seemingly "gone"): a review subagent mutated shared git state. In the PRIMARY, HEAD on `main` is the healthy state, not a regression. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git checkout `; confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. +- If the TASK worktree regressed mid-loop (its HEAD detached or off the feature branch, work seemingly "gone"): a review subagent mutated shared git state. In the PRIMARY, HEAD on `main` is the healthy state, not a regression. Do NOT panic or redo work. The local feature-branch ref and `origin/` still point at the latest commit (every logical unit was pushed). Recover with `git -C checkout ` (anchored: run bare from the primary it would succeed and park the PRIMARY on the feature branch, since a detached worktree no longer holds it); confirm with `git log --oneline origin/main..HEAD` and `git status` clean. The PR on GitHub was never affected (the GitHub-reading reviewer still saw correct content), so no re-push or force-push is needed. - If the round's reviewer cannot be produced (a deep-review run or a delta spawn that is declined at the permission prompt, errors, dies, hangs past its watchdog, or returns without its expected result shape): the round did not happen. Re-run it (re-invoke the workflow, re-spawn the verifier), varying the approach after a few identical failures, and do NOT stop mid-loop to report the failure or ask how to proceed: recovering costs seconds and interrupting costs the loop its momentum. Only a reviewer that cannot be produced at all blocks the loop, which is its one exit that is not a clean round; then withhold the flip to ready for review and say once, at the end, that the review did not run and why. Keep that out of the PR body and PR comments, since a spawn that could not run is session tooling rather than a fact about the change. Do NOT review it yourself inline and count that as the round; an inline pass is what let three real bugs through a supposedly clean PR. The branch, the commits, and the card all stay exactly as they are. Full rules in step 1 of the self-review loop. - If the `gh project item-edit` call fails (auth scope, missing field): report the failure clearly and offer to do the move manually via the web UI. The branch creation still stands. From 104f3922f12ccae4b284320bd9b20b0edf92ccef Mon Sep 17 00:00:00 2001 From: Vivek Date: Wed, 29 Jul 2026 00:18:19 +0530 Subject: [PATCH 43/43] docs: cap fable-pinned reviewers at three per deep-review run Dynamic lenses alternated fable and opus by index, so a full run could pin six reviewers to fable. A MAX_FABLE budget now caps the pinned fable count across the whole run: the three fixed fable lenses (security, invariants-docs, fresh-eyes) spend it, and every dynamic lens takes opus. Model diversity is unchanged, since both families still always read the diff, and stats report the effective fable count so a run's split is auditable from its result. --- .claude/skills/webjs-start-work/SKILL.md | 2 +- .claude/workflows/deep-review.js | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index a1cea0f8b..bd3f78deb 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -317,7 +317,7 @@ The draft PR is already open (step 6), so reviews post to it from the first roun **Shape the rounds for wall clock: deep-review first, then delta-scoped verification.** Sequential broad rounds re-AUDIT the same surface N times; this shape audits it once, in parallel, then each later round asks only about what changed (while still holding the full changed files as evidence). -- **Round 1 is the committed deep-review workflow, for EVERY task that enters the loop** (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`). What it does: a scout reads the diff and proposes up to six dynamic lenses tailored to the PR's nature, which run in parallel alongside six fixed ones (correctness, security, blast radius, test adequacy, invariants and doc drift, fresh eyes; all split across opus and fable so two model families always read the diff), then an adversarial jury refutes each finding. The whole run is HARD-CAPPED at 24 agents by default (`{ pr, maxAgents }` overrides, clamped 8 to 60): dynamic lenses trim first, jury sizes shrink next, and a finding the budget cannot verify comes back in an `unverified` list, never silently dropped, so raise the cap or re-run when that list is non-empty. Its CONFIRMED findings are the round's findings, the ones you act on; the jury-REJECTED ones come back too, carrying their refuters' reasons, but only for the audit trail (post them with the round per step 2's record rule; they are already adjudicated, need no action, and do not by themselves force another round). Uniform on purpose: its cost self-scales (juries convene per finding, so a quiet PR is cheap), and a uniform rule cannot be misjudged the way a by-risk bifurcation can, which matters because a `packages/*/src` bug ships into every end-user app. The whole run counts as ONE round, clean only when it confirms nothing, and its confirmed findings feed this same loop (fix, post as one review object, delta round). A deep-review run that dies or errors is a failed spawn under the liveness rules below: re-run it, and never substitute an inline pass. +- **Round 1 is the committed deep-review workflow, for EVERY task that enters the loop** (`.claude/workflows/deep-review.js`, run via the Workflow tool as `{ name: "deep-review", args: "" }`). What it does: a scout reads the diff and proposes up to six dynamic lenses tailored to the PR's nature, which run in parallel alongside six fixed ones (correctness, security, blast radius, test adequacy, invariants and doc drift, fresh eyes; at most THREE reviewers per run are pinned to fable and the rest to opus, so two model families always read the diff at a fixed fable cost), then an adversarial jury refutes each finding. The whole run is HARD-CAPPED at 24 agents by default (`{ pr, maxAgents }` overrides, clamped 8 to 60): dynamic lenses trim first, jury sizes shrink next, and a finding the budget cannot verify comes back in an `unverified` list, never silently dropped, so raise the cap or re-run when that list is non-empty. Its CONFIRMED findings are the round's findings, the ones you act on; the jury-REJECTED ones come back too, carrying their refuters' reasons, but only for the audit trail (post them with the round per step 2's record rule; they are already adjudicated, need no action, and do not by themselves force another round). Uniform on purpose: its cost self-scales (juries convene per finding, so a quiet PR is cheap), and a uniform rule cannot be misjudged the way a by-risk bifurcation can, which matters because a `packages/*/src` bug ships into every end-user app. The whole run counts as ONE round, clean only when it confirms nothing, and its confirmed findings feed this same loop (fix, post as one review object, delta round). A deep-review run that dies or errors is a failed spawn under the liveness rules below: re-run it, and never substitute an inline pass. - **Every later round is delta-scoped: ONE reviewer whose QUESTION is what the last round changed** (or, when a round produced NO fix commits, the riskiest section of the original diff). Delta-scoped narrows the question, never the evidence: the reviewer still receives the full changed files and MUST trace the fix's blast radius, grepping every symbol, rule, or concept the fix touches across the whole PR surface and comparing each other occurrence, since a small fix can break something far from its own hunk. What it does not do is re-audit the unchanged surface from scratch, which is the all-pairs reasoning that makes broad rounds slow. If it finds something, fix and repeat the delta shape. Re-run deep-review in full, not a delta, when the surface round 1 audited is no longer the surface under review: a fix rewrote something well outside the original diff (for shipped source, a fix spanning files is reason enough), OR the PR's SCOPE changed after round 1, new work folded in, a user-directed pivot, an issue absorbed mid-review. A delta round verifies fixes to an audited surface; it cannot stand in for the audit of a surface round 1 never saw. - **Starve reviewers of context they do not need.** Each gets the PR diff, the PR title and body (the author's claims, which the review checks against), the touched files, and the rule files it judges against (`AGENTS.md`, `CONVENTIONS.md`), nothing more. NEVER feed them prior PR comments or reviews (measured on #1159: the comment payload alone reached 171 KB by the fifth round and dominated its cost), and never a growing already-handled exclusion list. Dedupe repeat findings yourself: a duplicate costs you a second of reading, while the exclusion list costs every reviewer minutes of ingestion. diff --git a/.claude/workflows/deep-review.js b/.claude/workflows/deep-review.js index 738744cbc..6243d4a49 100644 --- a/.claude/workflows/deep-review.js +++ b/.claude/workflows/deep-review.js @@ -4,7 +4,7 @@ export const meta = { whenToUse: 'The round-1 review for any PR entering a review loop, or a heavyweight pass on demand. Works on any repo. Pass the PR number as args, "owner/repo#123" for another repository, or { pr, repo, lenses, maxAgents } where explicit lenses skip the scout and maxAgents caps the whole run (default 24).', phases: [ { title: 'Scope', detail: 'a scout reads the diff and proposes dynamic lenses for this PR' }, - { title: 'Find', detail: 'six fixed lenses plus up to six dynamic ones, in parallel, split across opus and fable' }, + { title: 'Find', detail: 'six fixed lenses plus up to six dynamic ones, in parallel, at most three pinned to fable and the rest opus' }, { title: 'Verify', detail: 'adversarial refuters per finding, majority rules' }, ], } @@ -93,7 +93,10 @@ const LENSES = [ // Every fixed lens pins its model: half opus, half fable. Two model families // reading the same diff have different blind spots, and pinning makes the // split deterministic instead of inheriting whatever the session runs. -// Dynamic lenses alternate between the two families by index. +// MAX_FABLE caps fable-pinned reviewers across the whole run: the three fixed +// fable lenses spend the budget, so dynamic lenses take opus. Diversity is +// preserved (both families always read the diff) at a fixed fable cost. +const MAX_FABLE = 3 const LENS_PROPOSALS = { type: 'object', @@ -142,7 +145,12 @@ if (dynamic.length > dynamicAllowed) { log(`agent budget ${MAX_AGENTS}: trimming dynamic lenses ${dynamic.length} to ${dynamicAllowed}`) dynamic = dynamic.slice(0, dynamicAllowed) } -const DYNAMIC = dynamic.map((l, i) => ({ key: `dyn-${l.key}`, prompt: l.prompt, model: i % 2 === 0 ? 'fable' : 'opus' })) +let fableUsed = LENSES.filter((l) => l.model === 'fable').length +const DYNAMIC = dynamic.map((l) => { + const model = fableUsed < MAX_FABLE ? 'fable' : 'opus' + if (model === 'fable') fableUsed += 1 + return { key: `dyn-${l.key}`, prompt: l.prompt, model } +}) const ALL_LENSES = [...LENSES, ...DYNAMIC] phase('Find') @@ -170,7 +178,7 @@ const toVerify = deduped.slice(0, CAP) if (deduped.length > CAP) log(`capping verification at ${CAP} of ${deduped.length} deduped findings (dropped ${deduped.length - CAP} lowest-severity; re-run after fixes to catch them)`) log(`${all.length} raw findings, ${deduped.length} after dedup, verifying ${toVerify.length}`) -if (toVerify.length === 0) return { pr, repo, confirmed: [], rejected: [], unverified: [], stats: { raw: all.length, deduped: deduped.length, verified: 0, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key), maxAgents: MAX_AGENTS }, note: 'no findings survived dedup; treat as a clean deep pass' } +if (toVerify.length === 0) return { pr, repo, confirmed: [], rejected: [], unverified: [], stats: { raw: all.length, deduped: deduped.length, verified: 0, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key), maxAgents: MAX_AGENTS, fable: ALL_LENSES.filter((l) => l.model === 'fable').length }, note: 'no findings survived dedup; treat as a clean deep pass' } phase('Verify') // Adaptive adversarial jury: 3 refuters for critical, 2 for major, 1 for minor. @@ -219,6 +227,6 @@ return { confirmed, rejected, unverified, - stats: { raw: all.length, deduped: deduped.length, verified: allocated.length, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key), maxAgents: MAX_AGENTS }, + stats: { raw: all.length, deduped: deduped.length, verified: allocated.length, lenses: ALL_LENSES.length, dynamic: DYNAMIC.map((l) => l.key), maxAgents: MAX_AGENTS, fable: ALL_LENSES.filter((l) => l.model === 'fable').length }, note: 'Confirmed findings feed the normal review loop: fix, post as one review object, then a delta round. Rejected ones carry their refuters\' reasons for the audit trail.', }