From 05a085599aae6f2fb708929e3e35fea97f8b3f48 Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 13:26:20 -0400 Subject: [PATCH 01/13] spec: amend Phase 2 to three workflows; defer the fix loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the operator's ruling at the review cap, in the artifact that is supposed to carry it: - R4 (bounded fix loop) is DEFERRED to its own intent. It needed a third claude[bot] trigger edge where R5 allows two, and it was the one job holding write credentials, running PR-authored code, and reading untrusted PR text at once — that earns a clean design, not patches. - R5 is scoped to this phase's three workflows and its single open bot edge (review-of-agent-PRs). - The Design section no longer builds fix-on-review.yml or the address-review skill, and the exit test proves R1-R3. This amendment REDUCES what the lane does. Widening a spec so an agent can have more power is the amendment this project refuses; recording less delivery is the operator's call, taken at the cap. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 51 +++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 2efc5c5..7e61bcc 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -6,13 +6,23 @@ drafted: 2026-08-26 # Spec: v2 autonomous lane (Phase 2) From the accepted intent: the chain must advance without a live session — merged -intent → spec PR, merged spec → implementation PR, every PR reviewed with a bounded -fix loop; the operator's only actions are the gate merges. +intent → spec PR, merged spec → implementation PR, every PR reviewed; the operator's +only actions are the gate merges. + +**Amended 2026-08-27 (operator ruling at the review cap).** This phase ships three +workflows, not four: the fix loop (R4) is deferred to its own intent. Two reasons, +recorded in `plan.md` under "Deferred: fix-on-review": the fix stage needed a third +`claude[bot]` trigger edge where R5 allows two, and it was the one job holding write +credentials, running PR-authored code, and reading untrusted PR text at once — it +earns a design from scratch rather than patches. Answering review findings stays the +operator's job in a session until that intent lands. This is a scope reduction the +operator ruled on; no rail is weakened by it. ## Requirements -Each requirement is verifiable; R1–R4 are jointly proven by the phase exit test -(one real change flows through the lane with the operator only merging). +Each requirement is verifiable; R1–R3 are jointly proven by the phase exit test +(one real change flows through the lane with the operator only merging). R4 is +deferred with this phase's fix stage — see the amendment note above. - **R1 — spec stage.** An operator merge to main that adds or changes `work//intent.md` triggers a job that opens (or updates) a PR titled @@ -29,14 +39,16 @@ Each requirement is verifiable; R1–R4 are jointly proven by the phase exit tes - **R3 — review stage.** Every same-repo PR gets a review per `REVIEW.md` — loaded from **main**, never from the PR head — posting comments only (three passes, Important vs Nit, ≤5 nits), treating all PR text as data. -- **R4 — bounded fix loop.** Review findings trigger a fix pass that bumps the - `round-N` label **before** acting; at `round-3` it applies `needs-human`, posts - the productive-cap comment (land the converged core, split the remainder), and - stops. Fix pushes re-trigger review; the labels bound the loop. -- **R5 — safety invariants (all four workflows).** One global `claude-quota` +- **R4 — bounded fix loop. DEFERRED** to its own intent (see the amendment note + above). It described: review findings trigger a fix pass that bumps the `round-N` + label before acting; at `round-3` it applies `needs-human`, posts the productive-cap + comment, and stops. That design is superseded — the next intent starts from the + credential split in `plan.md`, not from this text. +- **R5 — safety invariants (all three workflows in this phase).** One global `claude-quota` concurrency group serializes every agent job; explicit actor gates - (`github.actor == operator`, or `allowed_bots: claude[bot]` only on the two - deliberately-opened bot edges: review-of-agent-PRs and fix-on-review-comment); + (`github.actor == operator`, or `allowed_bots: claude[bot]` only on deliberately + opened bot edges — this phase opens exactly one, review-of-agent-PRs; the second + edge R5 allowed served the deferred fix stage); `timeout-minutes` and `--max-turns` on every job; PR-creation steps assert the PR exists and fail loudly; the fix stage never pushes to a PR that already has an approval; stage write-limits stated in the stage skills (mechanical @@ -61,17 +73,16 @@ Each requirement is verifiable; R1–R4 are jointly proven by the phase exit tes At the file/component altitude; order of work belongs to `plan.md`. - **Workflows** (`.github/workflows/`): `spec-on-intent.yml`, - `implement-on-spec.yml`, `review-on-pr.yml`, `fix-on-review.yml` — per the v2 - design doc's skeletons: push-to-main path triggers for the two stage jobs; - `pull_request` (same-repo guard, per-PR `cancel-in-progress`) for review; - `issue_comment` with the reviewer's `YSTACK-REVIEW` marker for the fix pass. - All use `claude_code_oauth_token`, no `github_token` input (app-token events - must cascade), and invoke stage skills as their prompt. + `implement-on-spec.yml`, `review-on-pr.yml` — push-to-main path triggers for + the two stage jobs, and `pull_request` (same-repo guard, per-PR + `cancel-in-progress`) for review. All use `claude_code_oauth_token`, no + `github_token` input (app-token events must cascade), and invoke stage skills + as their prompt. (`fix-on-review.yml` and its `issue_comment` trigger belonged + to the deferred R4 — see the amendment note.) - **Stage skills** (`.claude/skills/`): reuse `spec-draft` and `plan-draft` unchanged; add `implement` (runs plan-draft first, then codes to the plan, - runs verify, opens the impl PR), `review-pr` (applies REVIEW.md), and - `address-review` (adopt-or-push-back per finding; never touches test files - during a fix — hook-enforced in Phase 3). + runs verify, opens the impl PR) and `review-pr` (applies REVIEW.md). + (`address-review` belonged to the deferred R4.) - **Helpers** (`scripts/v2/`, each with a hermetic test in `scripts/test/`): `pending-spec.sh` / `pending-impl.sh` (which slug needs work — the hash-comparison idempotency guards), `round-cap.sh` (read/bump round labels, From fac77d4d17dd453f08b6d623f8a296a9a42465f6 Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 13:34:44 -0400 Subject: [PATCH 02/13] spec: make the amendment self-contained and self-consistent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex on this PR, all three adopted: - the opening claim said the operator's only actions are gate merges, while the amendment gives them review findings to answer; it now says both - the deferral cited a plan section that lands in a later PR, so the rationale could not be audited from this branch alone. The reasons and the credential-split direction are stated here, in the spec - R5 still required "the fix stage never pushes to an approved PR" — a rule about a stage this phase does not build Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 7e61bcc..6b4f4e4 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -6,15 +6,21 @@ drafted: 2026-08-26 # Spec: v2 autonomous lane (Phase 2) From the accepted intent: the chain must advance without a live session — merged -intent → spec PR, merged spec → implementation PR, every PR reviewed; the operator's -only actions are the gate merges. +intent → spec PR, merged spec → implementation PR, every PR reviewed. The operator's +actions are the gate merges, plus answering review findings in a session until the +deferred fix stage lands (see the amendment note). **Amended 2026-08-27 (operator ruling at the review cap).** This phase ships three workflows, not four: the fix loop (R4) is deferred to its own intent. Two reasons, -recorded in `plan.md` under "Deferred: fix-on-review": the fix stage needed a third +stated here so this spec stands on its own: the fix stage needed a third `claude[bot]` trigger edge where R5 allows two, and it was the one job holding write credentials, running PR-authored code, and reading untrusted PR text at once — it -earns a design from scratch rather than patches. Answering review findings stays the +earns a design from scratch rather than patches. + +**Where the next intent starts — split the credential**, so no one job holds all three +powers: the agent that edits the PR's code holds no token and runs nothing from the PR; +CI verifies the result on the pushed branch as it verifies any branch; a deterministic +step holds the app credential and does the writing. Answering review findings stays the operator's job in a session until that intent lands. This is a scope reduction the operator ruled on; no rail is weakened by it. @@ -50,9 +56,10 @@ deferred with this phase's fix stage — see the amendment note above. opened bot edges — this phase opens exactly one, review-of-agent-PRs; the second edge R5 allowed served the deferred fix stage); `timeout-minutes` and `--max-turns` on every job; PR-creation steps assert the - PR exists and fail loudly; the fix stage never pushes to a PR that already has - an approval; stage write-limits stated in the stage skills (mechanical - enforcement arrives with Phase 3 hooks). + PR exists and fail loudly; stage write-limits stated in the stage skills + (mechanical enforcement arrives with Phase 3 hooks). (The "never push to an + approved PR" rule went with the deferred fix stage — it belongs to that intent, + which is the only thing that pushes to an open PR.) - **R6 — plumbing proven first.** Before the four workflows are finalized, a disposable `workflow_dispatch` test proves: the action (app token, no `github_token` input) can push a branch and create a PR via allowlisted From bbbd65e794bd674526c939005732a146bdf7c7f9 Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 13:40:29 -0400 Subject: [PATCH 03/13] spec: R6 names the three workflows this phase finalizes Codex: the probe prerequisite still said 'before the four workflows are finalized', describing a phase this amendment no longer reaches. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 6b4f4e4..3e9f9d3 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -60,7 +60,7 @@ deferred with this phase's fix stage — see the amendment note above. (mechanical enforcement arrives with Phase 3 hooks). (The "never push to an approved PR" rule went with the deferred fix stage — it belongs to that intent, which is the only thing that pushes to an open PR.) -- **R6 — plumbing proven first.** Before the four workflows are finalized, a +- **R6 — plumbing proven first.** Before the three workflows are finalized, a disposable `workflow_dispatch` test proves: the action (app token, no `github_token` input) can push a branch and create a PR via allowlisted `gh pr create`, and that app-created events trigger downstream workflows. From 43a28a7add930881278810efcd6008396348b710 Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 14:20:39 -0400 Subject: [PATCH 04/13] spec: fold four more, including a rule I cut on a false premise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex round 3 on this amendment: - the exit test declared R1-R3 proven while the operator "only merges", but this amendment also makes them answer findings. The run now counts only if the review found something and the operator resolved it — otherwise the smoke test proves the happy path, not the handoff the phase depends on - the deferral pointed the next intent at plan.md for its design, but any plan written for the four-workflow shape is exactly what was superseded; the direction lives in this spec - quota sizing still counted four jobs - I removed the "never push to an approved PR" rule claiming only the fix stage pushes to open PRs. False: R1 and R2 let a re-triggered producer stage update an existing PR. The rule is restored and now covers every stage — an approval means the operator read that diff Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 3e9f9d3..1214660 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -26,9 +26,12 @@ operator ruled on; no rail is weakened by it. ## Requirements -Each requirement is verifiable; R1–R3 are jointly proven by the phase exit test -(one real change flows through the lane with the operator only merging). R4 is -deferred with this phase's fix stage — see the amendment note above. +Each requirement is verifiable; R1–R3 are jointly proven by the phase exit test: +one real change flows through the lane, the operator merging at each gate and +answering whatever the review finds. The run counts only if the review actually +reported something and the operator resolved it in a session — a smoke change so +clean that nothing was found proves the happy path, not the handoff this phase +depends on. R4 is deferred with the fix stage — see the amendment note above. - **R1 — spec stage.** An operator merge to main that adds or changes `work//intent.md` triggers a job that opens (or updates) a PR titled @@ -49,7 +52,8 @@ deferred with this phase's fix stage — see the amendment note above. above). It described: review findings trigger a fix pass that bumps the `round-N` label before acting; at `round-3` it applies `needs-human`, posts the productive-cap comment, and stops. That design is superseded — the next intent starts from the - credential split in `plan.md`, not from this text. + credential split stated in the amendment note above, not from this text and not + from any plan written for the four-workflow shape. - **R5 — safety invariants (all three workflows in this phase).** One global `claude-quota` concurrency group serializes every agent job; explicit actor gates (`github.actor == operator`, or `allowed_bots: claude[bot]` only on deliberately @@ -57,9 +61,11 @@ deferred with this phase's fix stage — see the amendment note above. edge R5 allowed served the deferred fix stage); `timeout-minutes` and `--max-turns` on every job; PR-creation steps assert the PR exists and fail loudly; stage write-limits stated in the stage skills - (mechanical enforcement arrives with Phase 3 hooks). (The "never push to an - approved PR" rule went with the deferred fix stage — it belongs to that intent, - which is the only thing that pushes to an open PR.) + (mechanical enforcement arrives with Phase 3 hooks). **No stage pushes to a PR + the operator has already approved** — R1 and R2 let a re-triggered producer + stage update an existing PR, so this is not only the deferred fix stage's + concern: an approval means the operator read that diff, and a later push would + silently move what they approved. - **R6 — plumbing proven first.** Before the three workflows are finalized, a disposable `workflow_dispatch` test proves: the action (app token, no `github_token` input) can push a branch and create a PR via allowlisted @@ -119,8 +125,8 @@ first; adding Codex is additive and needs no workflow changes). one-concern budget. Recommendation for the plan stage: land as a stacked sequence under one `plan.md` — (a) helpers + tests, (b) workflows + skills + docs — each PR independently green. -3. **Quota:** all four jobs share the operator's subscription window with their - interactive use. Serialization + preflight (R5) mitigate; the sizing question +3. **Quota:** the three jobs share the operator's subscription window with the + operator's own interactive use. Serialization + preflight (R5) mitigate; the sizing question stays open until measured in this phase. 4. **North-star fit:** user-directed intent (operator-approved at G1), so no consensus gate required. The lane serves north star B indirectly — an From 0b76d303fabd885905e4d5c4c5688b838b20800d Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 14:26:28 -0400 Subject: [PATCH 05/13] spec: say what a producer does when the approved PR is stale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit My restored rule created a deadlock: R1/R2 have a re-triggered producer update an existing PR, while the new invariant forbids pushing to an approved one. Resolution: the stage does not push. It labels the PR stale, comments once, and stops — the operator merges, dismisses the approval, or closes it. An approved PR belongs to the operator, so the lane never edits one behind them. Completing the previous fold rather than a new round: the deadlock was introduced by that fold. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 1214660..d75fe9e 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -65,7 +65,12 @@ depends on. R4 is deferred with the fix stage — see the amendment note above. the operator has already approved** — R1 and R2 let a re-triggered producer stage update an existing PR, so this is not only the deferred fix stage's concern: an approval means the operator read that diff, and a later push would - silently move what they approved. + silently move what they approved. When a producer stage would have to update an + already-approved PR — the upstream artifact moved while the PR sat approved and + unmerged — it does not push. It labels the PR `stale`, says so in one comment, + and stops. The operator then merges it, dismisses the approval, or closes it; + the stage runs again once the PR is theirs to rebuild. An approved PR belongs to + the operator, so the lane never edits one behind them. - **R6 — plumbing proven first.** Before the three workflows are finalized, a disposable `workflow_dispatch` test proves: the action (app token, no `github_token` input) can push a branch and create a PR via allowlisted From f89e9240ad3ed93fc9575581ee762c70baa6acf8 Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 14:58:11 -0400 Subject: [PATCH 06/13] spec: a stale PR is rebuilt, never merged; and count the edges honestly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex on the current head, both adopted: - my stale-PR recovery offered merging as a way out. That lands an artifact whose recorded upstream hash no longer matches main, and no stage rebuilds it — merging changes the artifact, not the upstream that moved. The way out is to close it, or dismiss the approval and let the stage rebuild from current upstream. - the amendment said the fix stage needed a third edge while R5 said the second allowed edge already served it. Both are true and the text now says so: R5 allows two, the fix stage needed both plus a third (the dispatch re-running review after a push-back that changes no code) — which is precisely why it was deferred. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index d75fe9e..180487b 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -12,8 +12,10 @@ deferred fix stage lands (see the amendment note). **Amended 2026-08-27 (operator ruling at the review cap).** This phase ships three workflows, not four: the fix loop (R4) is deferred to its own intent. Two reasons, -stated here so this spec stands on its own: the fix stage needed a third -`claude[bot]` trigger edge where R5 allows two, and it was the one job holding write +stated here so this spec stands on its own: R5 allows two `claude[bot]` trigger +edges — review-of-agent-PRs, and the fix-on-review comment. The fix stage needed +both of those **plus** a third, the dispatch that re-runs review after a push-back +that changes no code; and it was the one job holding write credentials, running PR-authored code, and reading untrusted PR text at once — it earns a design from scratch rather than patches. @@ -57,8 +59,9 @@ depends on. R4 is deferred with the fix stage — see the amendment note above. - **R5 — safety invariants (all three workflows in this phase).** One global `claude-quota` concurrency group serializes every agent job; explicit actor gates (`github.actor == operator`, or `allowed_bots: claude[bot]` only on deliberately - opened bot edges — this phase opens exactly one, review-of-agent-PRs; the second - edge R5 allowed served the deferred fix stage); + opened bot edges — this phase opens exactly one, review-of-agent-PRs. R5's second + allowed edge, the fix-on-review comment, went with the deferred stage, and the + third edge that stage would also have needed is precisely why it was deferred); `timeout-minutes` and `--max-turns` on every job; PR-creation steps assert the PR exists and fail loudly; stage write-limits stated in the stage skills (mechanical enforcement arrives with Phase 3 hooks). **No stage pushes to a PR @@ -68,9 +71,13 @@ depends on. R4 is deferred with the fix stage — see the amendment note above. silently move what they approved. When a producer stage would have to update an already-approved PR — the upstream artifact moved while the PR sat approved and unmerged — it does not push. It labels the PR `stale`, says so in one comment, - and stops. The operator then merges it, dismisses the approval, or closes it; - the stage runs again once the PR is theirs to rebuild. An approved PR belongs to - the operator, so the lane never edits one behind them. + and stops. **Merging it is not one of the ways out:** its recorded upstream hash + no longer matches main, so merging would land an artifact the chain reads as + stale and no stage would rebuild it — the merge changes the artifact, not the + upstream that moved. The operator closes the stale PR (or dismisses the approval + and lets the stage rebuild it from the current upstream). An approved PR belongs + to the operator, so the lane never edits one behind them; and a stale one is + rebuilt, never merged. - **R6 — plumbing proven first.** Before the three workflows are finalized, a disposable `workflow_dispatch` test proves: the action (app token, no `github_token` input) can push a branch and create a PR via allowlisted From 058c089151cc89250003268f82832a16c821dc06 Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 15:06:37 -0400 Subject: [PATCH 07/13] spec: give stale-PR recovery a way to restart the stage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex: closing a stale PR or dismissing an approval fires no event, so push-triggered stages never wake and the slug sits stranded — my recovery told the operator to do something the lane could not act on. Both stage workflows now carry workflow_dispatch beside their push trigger, gated to the operator, and the recovery says to use it. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 180487b..a35dcfb 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -74,10 +74,13 @@ depends on. R4 is deferred with the fix stage — see the amendment note above. and stops. **Merging it is not one of the ways out:** its recorded upstream hash no longer matches main, so merging would land an artifact the chain reads as stale and no stage would rebuild it — the merge changes the artifact, not the - upstream that moved. The operator closes the stale PR (or dismisses the approval - and lets the stage rebuild it from the current upstream). An approved PR belongs - to the operator, so the lane never edits one behind them; and a stale one is - rebuilt, never merged. + upstream that moved. The operator closes the stale PR, then **re-runs the stage + by dispatch** — closing a PR or dismissing an approval fires no event, so the + push-triggered stages would otherwise never wake and the slug would sit stranded. + Both stage workflows therefore carry `workflow_dispatch` alongside their push + trigger, gated to the operator like every other dispatch here. An approved PR + belongs to the operator, so the lane never edits one behind them; and a stale one + is rebuilt, never merged. - **R6 — plumbing proven first.** Before the three workflows are finalized, a disposable `workflow_dispatch` test proves: the action (app token, no `github_token` input) can push a branch and create a PR via allowlisted @@ -99,8 +102,9 @@ At the file/component altitude; order of work belongs to `plan.md`. - **Workflows** (`.github/workflows/`): `spec-on-intent.yml`, `implement-on-spec.yml`, `review-on-pr.yml` — push-to-main path triggers for - the two stage jobs, and `pull_request` (same-repo guard, per-PR - `cancel-in-progress`) for review. All use `claude_code_oauth_token`, no + the two stage jobs **plus `workflow_dispatch` on each, so the operator can + restart a stage after clearing a stale PR** (see the approved-PR rule in R5), + and `pull_request` (same-repo guard, per-PR `cancel-in-progress`) for review. All use `claude_code_oauth_token`, no `github_token` input (app-token events must cascade), and invoke stage skills as their prompt. (`fix-on-review.yml` and its `issue_comment` trigger belonged to the deferred R4 — see the amendment note.) From b6563156f3a0a59a5a6832c2d1ee4ef7bf36e729 Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 15:15:20 -0400 Subject: [PATCH 08/13] spec: the operator gate checks both actors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex [Security]: adding workflow_dispatch to the stage workflows re-opened the rerun hole the probe already taught us — github.actor stays the ORIGINAL dispatcher on a re-run, so any write-capable collaborator can re-run an operator's dispatch and spend their subscription. R5 now requires github.triggering_actor to match too, as plumbing-test.yml already does. The lesson existed in the plan and in a live workflow; the spec never carried it, so a new dispatch path inherited the old hole. Rules learned in one artifact have to reach the artifact that governs. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index a35dcfb..cd9a7b4 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -58,7 +58,11 @@ depends on. R4 is deferred with the fix stage — see the amendment note above. from any plan written for the four-workflow shape. - **R5 — safety invariants (all three workflows in this phase).** One global `claude-quota` concurrency group serializes every agent job; explicit actor gates - (`github.actor == operator`, or `allowed_bots: claude[bot]` only on deliberately + (`github.actor == operator` **and `github.triggering_actor == operator`** — on a + re-run `github.actor` stays whoever dispatched originally, so the first check + alone lets any write-capable collaborator re-run an operator's dispatch and spend + their subscription; the probe workflow already carries both checks for exactly + this reason — or `allowed_bots: claude[bot]` only on deliberately opened bot edges — this phase opens exactly one, review-of-agent-PRs. R5's second allowed edge, the fix-on-review comment, went with the deferred stage, and the third edge that stage would also have needed is precisely why it was deferred); From 44966e34a4b8ccd51c93097c235578e658c074bb Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 15:27:01 -0400 Subject: [PATCH 09/13] spec: round-cap is dormant, not part of this phase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex: the Helpers section still prescribed round-cap.sh and its test while nothing in the three workflows consumes round labels — fix-loop machinery left in an active design reads as work to do. It is already merged, so the spec now says it stays and sleeps until the deferred intent wakes it. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index cd9a7b4..804431e 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -118,9 +118,11 @@ At the file/component altitude; order of work belongs to `plan.md`. (`address-review` belonged to the deferred R4.) - **Helpers** (`scripts/v2/`, each with a hermetic test in `scripts/test/`): `pending-spec.sh` / `pending-impl.sh` (which slug needs work — the - hash-comparison idempotency guards), `round-cap.sh` (read/bump round labels, - emit proceed/stop), `quota-preflight.sh` (count recent agent runs, skip over - budget). Deterministic bash, no model calls. + hash-comparison idempotency guards) and `quota-preflight.sh` (count recent + agent runs, stop over budget). Deterministic bash, no model calls. + (`round-cap.sh` is already merged and stays in the repo, but nothing in this + phase calls it: round labels bound the fix loop, which is deferred. It wakes + again with that intent.) - **Model policy:** stage skills carry it — producer stages (`implement`) at the fixed coder ceiling from `config/models.conf`; gate stages (`review-pr`) at high effort, never downgraded (v1 tiering, unchanged). From 98e7b6ce57d5ea673233fcfa189ce5c45d90291e Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 15:55:53 -0400 Subject: [PATCH 10/13] spec: the brake counts push-triggered runs only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex: adding workflow_dispatch to the producer stages broke the invariant quota-preflight.sh depends on — that a counted workflow's runs always mean the agent ran. A dispatch that fails the actor gate still records a skipped run, so a collaborator could spam dispatches until the brake trips and starves the lane. Counting --event push restores the invariant: the ruleset means only an operator merge causes those. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 804431e..9071145 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -82,7 +82,12 @@ depends on. R4 is deferred with the fix stage — see the amendment note above. by dispatch** — closing a PR or dismissing an approval fires no event, so the push-triggered stages would otherwise never wake and the slug would sit stranded. Both stage workflows therefore carry `workflow_dispatch` alongside their push - trigger, gated to the operator like every other dispatch here. An approved PR + trigger, gated to the operator like every other dispatch here. **The runaway + brake counts push-triggered runs only** (`--event push`): a dispatch by anyone + who fails the actor gate still records a run whose job was skipped, and counting + those would let a collaborator spam dispatches until the brake trips and starves + the lane. Push-to-main runs keep the property the brake needs — the ruleset means + only an operator merge causes them, so such a run always means the agent ran. An approved PR belongs to the operator, so the lane never edits one behind them; and a stale one is rebuilt, never merged. - **R6 — plumbing proven first.** Before the three workflows are finalized, a From e5e0916c81d3867bcb07480ed244c61b3fc02919 Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 16:00:28 -0400 Subject: [PATCH 11/13] spec: state the both-actors rule once, for every gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex found the actor/triggering_actor hole for the third time, at a third location: a collaborator can re-run a bot-triggered review, and github.actor stays claude[bot] while only triggering_actor names them. Patching a third edge would have invited a fourth. R5 now states it as one invariant covering every job in the lane — operator gates, dispatches, and the bot edge alike — instead of a note attached to whichever gate was last found wanting. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 9071145..6092f98 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -57,15 +57,23 @@ depends on. R4 is deferred with the fix stage — see the amendment note above. credential split stated in the amendment note above, not from this text and not from any plan written for the four-workflow shape. - **R5 — safety invariants (all three workflows in this phase).** One global `claude-quota` - concurrency group serializes every agent job; explicit actor gates - (`github.actor == operator` **and `github.triggering_actor == operator`** — on a - re-run `github.actor` stays whoever dispatched originally, so the first check - alone lets any write-capable collaborator re-run an operator's dispatch and spend - their subscription; the probe workflow already carries both checks for exactly - this reason — or `allowed_bots: claude[bot]` only on deliberately - opened bot edges — this phase opens exactly one, review-of-agent-PRs. R5's second - allowed edge, the fix-on-review comment, went with the deferred stage, and the - third edge that stage would also have needed is precisely why it was deferred); + concurrency group serializes every agent job. + + **Every gate checks BOTH actors — one rule, no exceptions.** On a re-run + `github.actor` stays whoever (or whatever) started the original run, and the + person who pressed re-run appears only in `github.triggering_actor`. So any gate + written on `github.actor` alone lets a write-capable collaborator re-run someone + else's job — an operator's dispatch, or a bot-triggered review — and spend the + operator's subscription under a passing check. Every job in this lane therefore + requires the triggering actor to be the operator (or, on the bot edge, the bot + itself), alongside whatever `github.actor` or `allowed_bots` allows. The probe + workflow already carries both checks; this rule exists so no new job forgets it. + + Which gates: the two stage jobs and any dispatch are operator-gated; the review + job opens exactly one bot edge, review-of-agent-PRs (`allowed_bots: claude[bot]`). + R5's second allowed edge, the fix-on-review comment, went with the deferred stage, + and the third edge that stage would also have needed is precisely why it was + deferred. Then: `timeout-minutes` and `--max-turns` on every job; PR-creation steps assert the PR exists and fail loudly; stage write-limits stated in the stage skills (mechanical enforcement arrives with Phase 3 hooks). **No stage pushes to a PR From 22f1d3307eccd39584e4757637bd9971c102d78a Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 16:05:18 -0400 Subject: [PATCH 12/13] spec: the operator may re-run a bot-originated review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit My own both-actors rule was written tightly enough to lock the operator out of re-running a review that failed transiently — the triggering actor on such a re-run is the operator, not the bot. The rule now names the two acceptable triggering actors explicitly: the operator, or the bot itself. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 6092f98..0c44ad9 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -64,10 +64,13 @@ depends on. R4 is deferred with the fix stage — see the amendment note above. person who pressed re-run appears only in `github.triggering_actor`. So any gate written on `github.actor` alone lets a write-capable collaborator re-run someone else's job — an operator's dispatch, or a bot-triggered review — and spend the - operator's subscription under a passing check. Every job in this lane therefore - requires the triggering actor to be the operator (or, on the bot edge, the bot - itself), alongside whatever `github.actor` or `allowed_bots` allows. The probe - workflow already carries both checks; this rule exists so no new job forgets it. + operator's subscription under a passing check. So every job in this lane checks + the triggering actor as well, and accepts exactly two: **the operator** — who may + re-run anything by hand, including a bot-originated review that failed + transiently — **or `claude[bot]` itself**, which is what the triggering actor is + on a run the lane started. Anyone else is refused, whatever `github.actor` or + `allowed_bots` says. The probe workflow already carries both checks; this rule + exists so no new job forgets them. Which gates: the two stage jobs and any dispatch are operator-gated; the review job opens exactly one bot edge, review-of-agent-PRs (`allowed_bots: claude[bot]`). From aa2e856edc61006b4fe30bf94bdd1c10e8508ca9 Mon Sep 17 00:00:00 2001 From: ci Date: Thu, 27 Aug 2026 16:09:38 -0400 Subject: [PATCH 13/13] spec: one OPEN PR per slug, so a rebuild can open a fresh one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex: closing a stale PR and re-dispatching leaves no legal move if the one-PR-per-slug rule counts closed PRs — reopening means pushing into an approval, and a new PR looks like a second one. The rule is about concurrency: one OPEN PR per slug. A closed PR is history. Co-Authored-By: Claude Fable 5 --- work/v2-phase-2/spec.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/work/v2-phase-2/spec.md b/work/v2-phase-2/spec.md index 0c44ad9..e18128a 100644 --- a/work/v2-phase-2/spec.md +++ b/work/v2-phase-2/spec.md @@ -93,7 +93,9 @@ depends on. R4 is deferred with the fix stage — see the amendment note above. by dispatch** — closing a PR or dismissing an approval fires no event, so the push-triggered stages would otherwise never wake and the slug would sit stranded. Both stage workflows therefore carry `workflow_dispatch` alongside their push - trigger, gated to the operator like every other dispatch here. **The runaway + trigger, gated to the operator like every other dispatch here. The one-PR-per-slug + rule means **one OPEN PR per slug**: a closed PR is history, so the rebuild opens + a fresh one rather than reopening the stale PR and pushing into an approval. **The runaway brake counts push-triggered runs only** (`--event push`): a dispatch by anyone who fails the actor gate still records a run whose job was skipped, and counting those would let a collaborator spam dispatches until the brake trips and starves