pi-gauntlet 7.40.1 → 7.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +16 -4
  3. package/agents/code-reviewer.md +2 -0
  4. package/agents/implementer.md +2 -0
  5. package/agents/loop-implementer.md +10 -0
  6. package/agents/plan-writer.md +1 -1
  7. package/agents/spec-reviewer.md +2 -0
  8. package/bin/gauntlet-telemetry-seal.mjs +18 -3
  9. package/bin/gauntlet-telemetry-seal.test.mjs +30 -0
  10. package/extensions/lib/plan-check.test.ts +116 -0
  11. package/extensions/lib/plan-check.ts +149 -20
  12. package/extensions/lib/telemetry-record.test.ts +11 -0
  13. package/extensions/lib/telemetry-record.ts +7 -0
  14. package/extensions/phase-tracker.test.ts +201 -3
  15. package/extensions/phase-tracker.ts +77 -9
  16. package/extensions/verify-before-ship.test.ts +240 -2
  17. package/extensions/verify-before-ship.ts +180 -25
  18. package/package.json +2 -2
  19. package/skills/brainstorming/SKILL.md +1 -1
  20. package/skills/brainstorming/gatherer.md +14 -1
  21. package/skills/brainstorming/reference/amendment-surface.md +13 -1
  22. package/skills/brainstorming/reference/legs.md +187 -0
  23. package/skills/brainstorming/reference/pr-subject.md +1 -1
  24. package/skills/brainstorming/reference/spec-finalization.md +14 -0
  25. package/skills/brainstorming/reference/spec-pr.md +42 -5
  26. package/skills/brainstorming/reference/ticket-acceptance.md +2 -2
  27. package/skills/finishing-a-development-branch/SKILL.md +2 -2
  28. package/skills/finishing-a-development-branch/reference/legs.md +76 -0
  29. package/skills/finishing-a-development-branch/reference/spec-pr.md +12 -4
  30. package/skills/gauntlet-handoff/SKILL.md +2 -2
  31. package/skills/gauntlet-resume/SKILL.md +3 -2
  32. package/skills/gauntlet-resume/reference/brief-contract.md +37 -3
  33. package/skills/gauntlet-resume/reference/reconstruction.md +4 -0
  34. package/skills/gauntlet-resume/reference/seed.md +2 -0
  35. package/skills/loop-implementation/SKILL.md +89 -2
  36. package/skills/subagent-driven-development/SKILL.md +10 -0
  37. package/skills/using-git-worktrees/SKILL.md +58 -21
  38. package/skills/using-git-worktrees/reference/pr-head.md +25 -1
  39. package/skills/writing-plans/SKILL.md +8 -0
  40. package/skills/writing-plans/reference/plan-contract.md +9 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## v7.41.1 - 2026-10-29
4
+
5
+ ### Changed
6
+
7
+ - Coordinated draft-PR handoff publishes companion branches before opening one home draft PR, with no brief or companion PRs. `brainstorming #N` reconstructs recorded companions from origin and re-gates. Header-linked finish rows 1/2 deliver companions in order before one home seal carrying every companion PR, then push home and apply readiness; companion URLs stay in the shipped summary, not the home PR body.
8
+ - Clarify the released interrupted-completion exclusion alongside pi-cohort's paused cleanup/recovery contract: closure-review and cadence credit require an exact-agent, zero-exit child with no explicit interruption, per child in live results and replay. Interrupted attempted work still spends fix rounds and grants and contributes to nudges and telemetry. Pi-cohort's cleanup warning is diagnostic, not completion credit; explicit resume starts a new child. Core actual-process tests with fake backends do not establish live cmux behavior or companion retention compliance: the live runtime and reporter's installed adapter version remain unavailable, so no live reproduction is claimed. (jjuraszek/pi-cohort#36)
9
+
10
+ ## v7.41.0 - 2026-10-29
11
+
12
+ ### Added
13
+
14
+ - Coordinated multi-repo runs: one brainstorm spans ordered repository legs under one shared branch, home spec, handoff brief, and ship choice. `- sibling:` clone URLs in the overrides file are the eligible companions; a run coordinates only the companions the ask or ticket selects, resolved beside the home primary by repo name. Each leg gets its own worktree, baseline, verification, review, and telemetry seal; `plan_check` admits leg-aware plans; both implement engines, resume, and finishing fan out per leg and ship through one choice with per-leg receipts for partial-ship retry. A single-member run finishes through its recorded `contract:` lines and carries a `Baseline <repo>:` plan header; legacy specs without `## Legs` read as home-only. (#99)
15
+
16
+ ### Changed
17
+
18
+ - The phase tracker no longer credits an interrupted conformance-reviewer or cadence helper as a completed step. (jjuraszek/pi-cohort#36)
19
+ - Eval reports may be per-sample partials of the samples a change's own hunks touch when a rebase onto the base branch re-stales samples the change never touched. (#99)
20
+
3
21
  ## v7.40.1 - 2026-10-29
4
22
 
5
23
  ### Changed
package/README.md CHANGED
@@ -36,13 +36,13 @@ pi-gauntlet's only hard dependency is pi-cohort - every gate that dispatches a r
36
36
  Concretely, one change through the gauntlet:
37
37
 
38
38
  0. *(Optional)* Before there's even a spec, `/skill:shape-ticket` can create one tracker issue or repair a comma/whitespace list of issue refs - shaping each into a Context/Problem/Idea/Acceptance Criteria ticket, grounded by a repository audit, an AC integrity check, and a cheap council roast. Repair finishes preparation for all tickets before presenting exact-diff confirmations sequentially in ref order; each approval writes that ticket before the next gate, and may include one optional gated comment for a Reporter note or overlap evidence. The audit checks factual claims, Idea fit, AC mechanisms against cited repo guidance, and already-stated observable legs in configured sibling checkouts. It reports in-motion work and attempts a best-effort scenario through authorized current-system reads, without promising reproduction or a fix; missing access or nothing to observe is reported, not a new approval gate. Implementer detail moves into the gated Reporter note; brainstorming reads that note with the ticket so the spec retains the detail. A failed roast is retried once, then surfaced inline at the gate if it fails again. It's a tool, not a phase: no worktree, no plan/phase tracker, runs from any repo state. The agent may start it on its own when the ask is "file a ticket" or another skill routes there; the write still waits for your yes on the presented diff. Similarly, `/skill:chase-bug` triages a raw bug report into an evidenced verdict - and can hand off to shape-ticket, brainstorming, or a bounded hotfix - before any spec exists.
39
- 1. Opt in with `/skill:brainstorming` and describe the change. The command is the reliable discovery-and-entry path; explicit prose requesting brainstorming/the gauntlet also supplies consent when the skill is already loaded or an available instruction or route identifies it. An ordinary implementation request or an agent recommendation is not consent. A leading run of GitHub references (`#N`, `owner/repo#N`, or a `/pull/N` or `/issues/N` URL), separated by whitespace or commas, resolves each reference as a PR first: related open, same-repository, non-fork PRs on one base form one rework subject, including drafts. Admission runs in listed order and relatedness is checked before provisioning; the first retained PR is home, whose existing branch rebases onto the fetched PR base by default, then member commits replay onto it in listed order. The first intent word `merge-forward` opts out and keeps the merge path. An unresolved conflict after its scoped helper aborts that step and continues to the spec with the repair as Task 1. A single non-PR reference follows the ticket path. The first non-reference token starts intent text; mid-sentence references and other ticket/free-text subjects keep their existing paths. The combined code, PR-carried specs, claims, and review threads are the brainstorm input, with each PR's provenance retained; only home's bound ticket supplies acceptance criteria, while member tickets supply context. A single PR retains the same rework path without member replays or a close offer. The combined state is judged against constraints from the fetched current base HEAD, not the fork point or the PRs' proposed policy edits; see the [PR-subject contract](skills/brainstorming/reference/pr-subject.md) and [PR-head worktree owner](skills/using-git-worktrees/reference/pr-head.md). **`brainstorming`** sets up an isolated worktree (on the existing branch for PR rework), explores the codebase, and turns the subject into a written spec - when its recon finds a cited case that the ask is the wrong change (misdiagnosed, wrong-sized, already solved, a band-aid, or cutting across a sound pattern - including a repo convention that is itself debt) and can name the alternative, question one is a framing question (`as framed`, `pivot: <alternative>`, or `smaller or none`); otherwise it opens with one sentence naming what it checked, and every approach it proposes ends with a `Pattern:` line naming what it follows or cuts across; when the run starts from a ticket, the spec carries the ticket's acceptance criteria verbatim, each with a disposition (`in-scope`, `deviates:`, `deferred:`, `venue:`, `elsewhere:` for a row another repo's part delivers), scope cuts come only from your answer to a cut question and are listed at the gate, the conformance gate checks the in-scope ones, and `/skill:check-delivery` verifies `venue:` rows before the prod deploy. A multi-model critique runs on it automatically. If the spec replaces a known prior spec, brainstorming marks the predecessor with a `> **Superseded by:**` banner under its title (default format, syntax overridable via `.pi/gauntlet-overrides.md`; candidates come from brainstorming's scout recon, never a mechanical sweep). **You read and approve the spec - human gate 1.** Options 1/2/4/5 approve and pick an engine; option 6 approves the content only: it writes `**Engine:** deferred` under `**Amend-grant:** none`, pushes the branch, opens a draft PR carrying just the spec, prints the URL, and ends the flow - no plan, no loop, no handoff brief. The branch stays; a gauntlet-owned worktree is removed, a host-owned checkout is reported and left. Option 6 is withheld on a PR subject (`Withheld: draft PR - #N already exists`) and when the branch's diff against the default branch is not spec-only. A later `brainstorming #N` on that PR classifies it from the diff alone: one added spec carrying the marker plus banner-only predecessor edits re-presents the gate with 1/2/4/5 (3 only when the spec commit carries a council audit) and skips gather, questionary, council, and scout; a spec-only diff without the marker prints `spec-only PR without the gate marker - running lint and council` and takes the normal brainstorm path with the carried spec as the draft; any other diff takes the rework path. Approval at that re-gate writes `**Engine:**`, `**Amend-grant:**`, and `**PR:** <owner/repo#N>` as a new commit on the PR branch and chains into planning or the loop as usual. Before approval, PR rework's sole write/run exception outside the spec directories permits only the base sync (rebase or merge), each member replay or merge, their conflict helpers' edits to the conflicted files, and their scoped conflict tests; other subjects have no implementation code yet.
39
+ 1. Opt in with `/skill:brainstorming` and describe the change. The command is the reliable discovery-and-entry path; explicit prose requesting brainstorming/the gauntlet also supplies consent when the skill is already loaded or an available instruction or route identifies it. An ordinary implementation request or an agent recommendation is not consent. A leading run of GitHub references (`#N`, `owner/repo#N`, or a `/pull/N` or `/issues/N` URL), separated by whitespace or commas, resolves each reference as a PR first: related open, same-repository, non-fork PRs on one base form one rework subject, including drafts. Admission runs in listed order and relatedness is checked before provisioning; the first retained PR is home, whose existing branch rebases onto the fetched PR base by default, then member commits replay onto it in listed order. The first intent word `merge-forward` opts out and keeps the merge path. An unresolved conflict after its scoped helper aborts that step and continues to the spec with the repair as Task 1. A single non-PR reference follows the ticket path. The first non-reference token starts intent text; mid-sentence references and other ticket/free-text subjects keep their existing paths. The combined code, PR-carried specs, claims, and review threads are the brainstorm input, with each PR's provenance retained; only home's bound ticket supplies acceptance criteria, while member tickets supply context. A single PR retains the same rework path without member replays or a close offer. The combined state is judged against constraints from the fetched current base HEAD, not the fork point or the PRs' proposed policy edits; see the [PR-subject contract](skills/brainstorming/reference/pr-subject.md) and [PR-head worktree owner](skills/using-git-worktrees/reference/pr-head.md). **`brainstorming`** sets up an isolated worktree (on the existing branch for PR rework), explores the codebase, and turns the subject into a written spec - when its recon finds a cited case that the ask is the wrong change (misdiagnosed, wrong-sized, already solved, a band-aid, or cutting across a sound pattern - including a repo convention that is itself debt) and can name the alternative, question one is a framing question (`as framed`, `pivot: <alternative>`, or `smaller or none`); otherwise it opens with one sentence naming what it checked, and every approach it proposes ends with a `Pattern:` line naming what it follows or cuts across; when the run starts from a ticket, the spec carries the ticket's acceptance criteria verbatim, each with a disposition (`in-scope`, `deviates:`, `deferred:`, `venue:`, `elsewhere:` for a row another repo's part delivers), scope cuts come only from your answer to a cut question and are listed at the gate, the conformance gate checks the in-scope ones, and `/skill:check-delivery` verifies `venue:` rows before the prod deploy. A multi-model critique runs on it automatically. If the spec replaces a known prior spec, brainstorming marks the predecessor with a `> **Superseded by:**` banner under its title (default format, syntax overridable via `.pi/gauntlet-overrides.md`; candidates come from brainstorming's scout recon, never a mechanical sweep). **You read and approve the spec - human gate 1.** Options 1/2/4/5 approve and pick an engine; option 6 approves the content only: it writes `**Engine:** deferred` under `**Amend-grant:** none`, pushes the branch, opens a draft PR carrying just the spec, prints the URL, and ends the flow - no plan, no loop, no handoff brief. For a coordinated spec, it first pushes every companion branch to origin, then opens one home draft PR; it creates no companion PRs. A later `brainstorming #N` reconstructs the recorded companions from their origin branches before re-presenting the implementation gate; the [publication and re-gate owner](skills/brainstorming/reference/spec-pr.md) defines all-member preflight, recovery, and cleanup. The branch stays; a gauntlet-owned worktree is removed, a host-owned checkout is reported and left. Option 6 is withheld on a PR subject (`Withheld: draft PR - #N already exists`) and when the branch's diff against the default branch is not spec-only. A later `brainstorming #N` on that PR classifies it from the diff alone: one added spec carrying the marker plus banner-only predecessor edits re-presents the gate with 1/2/4/5 (3 only when the spec commit carries a council audit) and skips gather, questionary, council, and scout; a spec-only diff without the marker prints `spec-only PR without the gate marker - running lint and council` and takes the normal brainstorm path with the carried spec as the draft; any other diff takes the rework path. Approval at that re-gate writes `**Engine:**`, `**Amend-grant:**`, and `**PR:** <owner/repo#N>` as a new commit on the PR branch and chains into planning or the loop as usual. Before approval, PR rework's sole write/run exception outside the spec directories permits only the base sync (rebase or merge), each member replay or merge, their conflict helpers' edits to the conflicted files, and their scoped conflict tests; other subjects have no implementation code yet.
40
40
  2. By default, **`writing-plans`** decomposes the approved spec into atomic, independently-verifiable tasks, grouped into parallel waves where they don't touch the same files. Experimental approval options 4/5 instead select **`loop-implementation`**: the plan phase is skipped and a stamped acceptance-criteria ledger replaces the task plan. Option 4 retains amendment review; option 5 grants auto-apply amends, with redraws still stopping. See [Loop engine operations](doc/configuration.md#loop-engine-operations).
41
41
  3. **`subagent-driven-development`** executes the plan one task at a time, each in a fresh subagent, behind spec-compliance review then code-quality review. TDD-locked: red, green, refactor. Independent implementation, review, and council batches remain parallel, but their dispatches are foreground: the orchestrator waits for terminal results before accepting work or advancing a phase.
42
42
  The loop alternative to step 3 runs one fresh `loop-implementer` per seed or row iteration, without implement-phase reviewers. It stops for a human decision on stagnation or a failed exit verification; passing full verification with every row closed enters the same verify gates.
43
43
 
44
44
  4. **verify**: the parent first runs the plan's full verification command set (the ledger's `verification:` command for loop); only a passing result permits whole-diff code review, then the **conformance gate**. A review fix invalidates that result, so the parent reruns full verification before the next review or conformance gate. When the overrides file declares a happy path for the touched paths (see [Project-specific overrides](#project-specific-overrides)), the parent runs it once here, between code review and the conformance gate, and the reviewer reads its transcript as runtime evidence - a hang or a failed request shows up in the audit with the transcript quoted - as a gap when an origin clause ties to it, otherwise as a `happy-path: failed - unattributable` line - never as a silent green. The conformance subagent reads the finished code and docs against your *original words* from step 1, not the plan, and reports per-requirement: delivered, partial, missing, drifted, or unauthorized. Unauthorized rows - shipped surface no human input asked for, whether it crept in or was laundered through the spec - follow their recommendation like every other row: a contained removal auto-runs, anything another requirement leans on is deferred with a plain-language "I'd cut it / I'd keep it" recommendation. Inside a brainstorming-entered flow this gate is machine-blocked from being skipped. Compatible executable recommendations auto-run through an isolated fix-and-re-audit loop with no prompt; anything still open surfaces as a numbered decision brief - one item per decision, the problem in plain words first, lettered options each with a one-line consequence, and `Recommended: <letter> - <reason>` or `No recommendation - pick one`. Reply with one line per item (`1. B`), or bare `1` to take every recommendation (no-recommendation items are re-asked by number); a current `CONFORMS` / no-concerns result goes straight to the branch options with no extra conformance sign-off.
45
- 5. **`finishing-a-development-branch`**: PR, draft PR, squash, keep, or discard; PR rework's row 1 offers a lease push of the rebased home branch to the existing PR (`Base:` records in `merge:` form push non-force). A spec whose header carries `**PR:**` (approved at a spec-PR re-gate) and no `## Rework record` replaces the two create rows with `Push to PR #N and mark it ready for review` and `Push to PR #N, keep it a draft` (`Push to PR #N (already ready for review)` when the PR is already ready): both push without force after strip-and-seal, the first then runs `gh pr ready`, neither rewrites the PR body, and both are withheld when origin's branch head is no longer an ancestor of local HEAD. A spec still carrying `**Engine:** deferred` stops finishing (and `gauntlet-resume`) with `spec approved for handoff only - run brainstorming #N` before any phase change, verification, or push. The PR-rework push appends a `## Rework` block naming absorbed PRs while retaining the original description bytes; the header-linked `**PR:**` push leaves the body untouched. Only after a successful rework push and home-description edit does one gated confirmation show the exact close comments and eligible member targets, linking home; declining or any other finish option leaves members open. Closing never deletes member branches, and finish posts no review-thread replies. Its `Before:` handle includes the full portable pre-rework SHA and the local `refs/gauntlet/pre-rework/pr-<N>` ref in the rework checkout. The [finishing owner](skills/finishing-a-development-branch/SKILL.md#pr-subject-row-1-push-to-the-pr-branch) defines the landing and body-update contract. Once a PR exists, run `/skill:gatekeep-pr <pr>` to verify it before merging - it judges the diff against the spec at the PR's head (the acceptance-criteria dispositions and Design you approved; the PR description when no spec applies) - rows the spec defers or deviates are settled, every other row needs its mechanism (code, tests, docs) - and against the rubric; whatever can be seen only after merge is left to step 6. **Human gate 2** - the only other decision you make.
45
+ 5. **`finishing-a-development-branch`**: PR, draft PR, squash, keep, or discard; PR rework's row 1 offers a lease push of the rebased home branch to the existing PR (`Base:` records in `merge:` form push non-force). A spec whose header carries `**PR:**` (approved at a spec-PR re-gate) and no `## Rework record` replaces the two create rows with `Push to PR #N and mark it ready for review` and `Push to PR #N, keep it a draft` (`Push to PR #N (already ready for review)` when the PR is already ready): both push without force after strip-and-seal, the first then runs `gh pr ready`, neither rewrites the PR body, and both are withheld when origin's branch head is no longer an ancestor of local HEAD. For coordinated specs, these [header-linked rows 1/2](skills/finishing-a-development-branch/reference/spec-pr.md#push-rows-12) deliver companions in recorded order before one home seal carrying every companion PR URL, then push home and apply its readiness choice. Row 1 makes companion drafts ready too; row 2 creates drafts but never converts an already-ready PR back to draft. Companion URLs appear in the shipped summary, companions first and home last, not in the unchanged home PR body; keep that summary for cross-repo review because successful home delivery removes receipts. A spec still carrying `**Engine:** deferred` stops finishing (and `gauntlet-resume`) with `spec approved for handoff only - run brainstorming #N` before any phase change, verification, or push. The PR-rework push appends a `## Rework` block naming absorbed PRs while retaining the original description bytes; the header-linked `**PR:**` push leaves the body untouched. Only after a successful rework push and home-description edit does one gated confirmation show the exact close comments and eligible member targets, linking home; declining or any other finish option leaves members open. Closing never deletes member branches, and finish posts no review-thread replies. Its `Before:` handle includes the full portable pre-rework SHA and the local `refs/gauntlet/pre-rework/pr-<N>` ref in the rework checkout. The [finishing owner](skills/finishing-a-development-branch/SKILL.md#pr-subject-row-1-push-to-the-pr-branch) defines the landing and body-update contract. Once a PR exists, run `/skill:gatekeep-pr <pr>` to verify it before merging - it judges the diff against the spec at the PR's head (the acceptance-criteria dispositions and Design you approved; the PR description when no spec applies) - rows the spec defers or deviates are settled, every other row needs its mechanism (code, tests, docs) - and against the rubric; whatever can be seen only after merge is left to step 6. **Human gate 2** - the only other decision you make.
46
46
  6. *(Optional)* Once the merge lands, `/skill:check-delivery <ref>` can prove delivery - default-branch landing, delivery target, per-AC evidence - before the tracker status advances. Explicit ask only, no auto-chain: deploys commonly lag merges by minutes to hours, so an auto-run would routinely check too early.
47
47
 
48
48
  Only the machine-owned `plan -> implement` and `verify -> ship` handoffs receive a branch-local one-shot nudge after an unexpected settled stop; it is fire-and-forget, does not bypass either human gate, and older Pi hosts without `agent_settled` retain existing behavior.
@@ -73,6 +73,18 @@ Use `/skill:shape-ticket --skip-overlap <ask>` to bypass discovery explicitly. R
73
73
 
74
74
  Everything between gate 1 and gate 2 - task breakdown, implementation, both review passes - runs without you in the loop, once gate 1 picks an engine; option 6 parks the approved spec in a draft PR and planning starts only when someone runs `brainstorming #N` on it and approves at the re-gate. Changing an approved spec later goes through brainstorming's `Amending an approved spec`: a fresh-context reviewer clears evidence-backed factual corrections on its own, escalations reach you as one readable batch, and only a redraw is a full stop - not a third numbered gate. That's the mechanism. What follows is the machinery behind it.
75
75
 
76
+ ### Multi-repo runs
77
+
78
+ A run has one home repository and one leg per selected repository. The home's `## shape-ticket` `- sibling:` declarations define eligible companions, not automatic membership: the ask must select a declared companion by repo identifier, the declared clone URL's `NAME`, or the URL itself, or the ticket must select it through a matching repo-group heading. Unselected siblings are not resolved. Members are ordered home first, then selected siblings in declaration order. Every member gets its own worktree with the same full branch and derived worktree name.
79
+
80
+ The approved home spec records `## Legs`, including each leg's scope, rules and resolved `contract:`. Each companion commits a leg file with the same filename in its own spec directory, carrying the home pointer, Problem, binding decisions and its own leg block. Plan and ledger stay home-owned; task `Leg:` selects the checkout for implementation and review. Committed artifacts use portable repo identifiers, not machine-local paths. See the [membership and artifact contract](skills/brainstorming/reference/legs.md).
81
+
82
+ Home overrides and settings govern flow policy. Each leg's own overrides and settings govern checkout operations, directories and verification; absent configuration uses defaults. Recorded `contract:` lines are authoritative for every spec with `## Legs`, including one-member runs: amend the home block and affected companion file before using a changed contract. Finish does not replace them by rereading overrides. Legacy specs without `## Legs` are home-only and retain the finish-time reread.
83
+
84
+ Option 6's draft-PR handle is one home draft PR plus companion branches on origin, with no brief, companion PRs or receipts. `brainstorming #N` preflights every recorded companion before provisioning them from origin and re-gating under the [publication and re-gate contract](skills/brainstorming/reference/spec-pr.md). This extends the [spec-only draft-PR design](doc/specs/2026-10-28-spec-only-draft-pr.md) without superseding it; PR lists still take the rework path. The separate `gauntlet-handoff` / `gauntlet-resume` route carries every member in the brief's `legs:` block. Resume starts only from the home primary, validates companion repository, branch and home pointer, and announces per-leg HEAD and dirty drift. On that route a missing companion worktree requires a live-validated completion receipt; draft-PR re-entry instead uses the published origin branch. The [brief contract](skills/gauntlet-resume/reference/brief-contract.md#coordinated-brief-grammar) owns the brief grammar and admission rules.
85
+
86
+ One finish choice spans all legs. PR, draft PR and squash deliver companions in declaration order, then seal and deliver home; keep preserves all legs, and discard lists all legs before one confirmation. Verification and conformance run per leg; home's passing tests do not credit companions. Delivery failure stops without rollback and reports shipped and unshipped legs. A retry from the brief validates home-owned receipts and skips only proven deliveries; stale PR heads are updated, and failed post-squash verification reruns without another squash. See the [delivery and recovery contract](skills/finishing-a-development-branch/reference/legs.md).
87
+
76
88
  ## Architecture
77
89
 
78
90
  `gatekeep-pr` selects one course per run: `assess` posts open blockers as one review (`request-changes` on someone else's PR, `comment` on your own), posts nothing with zero blockers, and performs no repair or sync; `deliver` repairs blockers, syncs, pushes, watches CI, and re-reviews until readiness, an obstacle, or a cap exit. `deliver with nits` stores `deliver-nits` and includes current and later nits. A ready first report skips the course pick and renders the final menu directly. Delivery rebases your own non-bot branch with `--force-with-lease` unless its history contains merge commits; someone else's, bot-authored, and merge-containing heads merge base instead. Final `merge` and `approve` remain separate picks; approval is offered only on someone else's non-draft PR with zero blockers. See the [course loop](skills/gatekeep-pr/reference/post-selection-loop.md) and [menu templates](skills/gatekeep-pr/reference/menus.md). In pi, `/skill:gatekeep-pr 12 15 18 [--rebase [base]]` gates a set of PR numbers or same-repository URLs, with one fresh `pr-gatekeeper` child and one worktree per PR. The parent shows a summary and unchanged per-PR menus in input order, queues every pick until the last menu, then executes the queued work in parallel. The set must fit pi-cohort's `parallel.maxTasks` (default `8`); it is never chunked. With `--rebase`, the parent asks once per set for eligible other-author or unreadable-author heads, excluding no-op heads and heads with merge commits. Multi-PR mode rejects positional issue references and is pi-only; single-PR behavior on Claude Code is unchanged. See the [parent-child contract](skills/gatekeep-pr/reference/batch.md).
@@ -128,7 +140,7 @@ Run by a human against pi-cohort >= 7.1.0, before a pi-gauntlet release claims t
128
140
  ## Requirements
129
141
 
130
142
  - [pi-coding-agent](https://github.com/earendil-works/pi) ≥ 0.85.1 - tested minimum for sequential `phase_tracker` tool execution, which persists a tracker update before a later tool call in the same model message.
131
- - [pi-cohort](https://github.com/jjuraszek/pi-cohort) >= 7.6.3 - required peer package, including binary-safe worktree patches; it also lets a persona with `subagent` in `tools:` start with inherited extensions ([pi-cohort #26](https://github.com/jjuraszek/pi-cohort/issues/26)), which multi-PR `gatekeep-pr` needs. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `forge-skill`, `shape-ticket`, `roasting-the-spec`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call. Gauntlet execution dispatches explicitly use `async: false`; do not enable pi-cohort's incompatible `forceTopLevelAsync` setting. See pi-cohort's [dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md) for that setting's owner and semantics. A wave of N >= 1 tasks dispatches its implementers and first spec reviews as one chain call. `subagent-driven-development` stops before dispatch on unsupported installs; there is no older-runtime fallback.
143
+ - [pi-cohort](https://github.com/jjuraszek/pi-cohort) >= 7.6.5 - required peer package, including binary-safe worktree patches; it also lets a persona with `subagent` in `tools:` start with inherited extensions ([pi-cohort #26](https://github.com/jjuraszek/pi-cohort/issues/26)), which multi-PR `gatekeep-pr` needs. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `forge-skill`, `shape-ticket`, `roasting-the-spec`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call. Gauntlet execution dispatches explicitly use `async: false`; do not enable pi-cohort's incompatible `forceTopLevelAsync` setting. See pi-cohort's [dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md) for that setting's owner and semantics. A wave of N >= 1 tasks dispatches its implementers and first spec reviews as one chain call. `subagent-driven-development` stops before dispatch on unsupported installs; there is no older-runtime fallback.
132
144
 
133
145
  Both packages must be listed in your `.pi/settings.json#packages` array (pi adds them automatically when you `pi install`). pi-gauntlet and pi-cohort are versioned independently but release together whenever dispatch semantics change - pin compatible versions of both. At each pi-gauntlet release, set the pi-cohort peer minimum and README floor together to the newest released pi-cohort version; `/skill:release` checks the registry before proceeding.
134
146
 
@@ -333,7 +345,7 @@ The gauntlet overrides file is the one place a repo tunes gauntlet's Markdown su
333
345
  | `## verification-before-completion` | verification-before-completion; gatekeep-pr (verification command only); chase-bug (hotfix verification) | repo test entrypoint discovery | overrides | the canonical verification target |
334
346
  | `## Spec index` `- docs: <glob>` | `gauntlet-spec-index` bin | `**/doc/**/*.md`, `**/docs/**/*.md`, `README.md`, `AGENTS.md` | overrides | the docs corpus include list; quotes/backticks stripped, fixed exclusions still apply, no other keys read |
335
347
  | `## Issue tracker` keys | shape-ticket, linear, chase-bug, check-delivery, brainstorming (`repo:` and tracker selection) | capability ladder / `git remote` | overrides | which tracker, repo, and commands ticket work uses |
336
- | `## shape-ticket` `- sibling: <clone url>` | shape-ticket | single-repo audit | extends | sibling checkouts for an already-stated second observable leg are expected beside `PRIMARY` (the primary checkout, not the current worktree) under the repository name; missing siblings report the path to clone to and never block, the committed URL names the sibling in report lines and supplies the clone hint while identity comparison ignores the clone scheme, and the flow never clones, fetches, or creates a sibling; paths are not accepted (see the [detailed grammar](skills/shape-ticket/reference/repo-audit.md#prepare-the-run)) |
348
+ | `## shape-ticket` `- sibling: <clone url>` | shape-ticket (audit), brainstorming (membership), gauntlet-resume, finishing-a-development-branch | single-repo audit and run | extends | sibling checkouts for an already-stated second observable leg are expected beside `PRIMARY` (the primary checkout, not the current worktree) under the repository name; missing siblings report the path to clone to and never block, the committed URL names the sibling in report lines and supplies the clone hint while identity comparison ignores the clone scheme, and the flow never clones, fetches, or creates a sibling; paths are not accepted (see the [detailed grammar](skills/shape-ticket/reference/repo-audit.md#prepare-the-run)); only companions explicitly selected by the ask or ticket join the run |
337
349
  | `## Capabilities` | shape-ticket | tool discovery | extends | tools, commands, env hosts available to ticket authoring |
338
350
  | `## Ticket wording` | shape-ticket | the shipped wording contract | overrides | ticket self-containment rules |
339
351
  | `## Response channels` | chase-bug | ask the user | overrides | where bug responses are posted |
@@ -11,6 +11,8 @@ completionGuard: false
11
11
 
12
12
  You are a code reviewer. You find issues before they ship. You **do not edit code**. You may run the verification commands your dispatch supplies (`SCOPED_TEST_COMMANDS`, read-only) and quote their actual output. Run ONLY those. Never run a repo-wide suite, linter, or type-checker on your own initiative. Dispatch carries no commands: say so in your report; run nothing.
13
13
 
14
+ A dispatch that carries `Rules:` lines or a `Leg:` value: Read the combined spec/task `Rules:` files before acting, resolve repo-relative rules in that leg's checkout, read the home spec at its supplied absolute path, review the caller's mapped original AC and leg, and keep git operations and supplied scoped commands in that checkout. Report a rules file you cannot read instead of inventing its contents. A dispatch without them: review as usual and write nothing about rules.
15
+
14
16
  ## Review priorities, in order
15
17
 
16
18
  1. **Correctness** — Does the change do what it claims? Are edge cases handled? Off-by-one, null/undefined, empty input, concurrency.
@@ -12,6 +12,8 @@ defaultProgress: true
12
12
 
13
13
  You are an implementation specialist. You execute an approved plan using strict test-driven development. You do not redesign or expand scope.
14
14
 
15
+ Read the combined spec/task `Rules:` files before acting. Resolve those repo-relative files in the caller's `Leg:` checkout; report missing rules instead of inventing contents. Use the caller's mapped original AC and leg, with the authoritative spec at its supplied absolute home path. Keep code, scoped commands and git operations in the supplied checkout.
16
+
15
17
  ## Workflow
16
18
 
17
19
  1. **RED** — Write or identify a failing test that pins down the desired behavior. Run it. Confirm it fails for the right reason (not a typo, not an import error).
@@ -23,6 +23,8 @@ Limit each sentence to 200 characters. Use the seed contract below for `target:
23
23
 
24
24
  Read the task fields: `iteration`, `spec` (absolute path), `ledger` (absolute path), `base` (SHA), `target` (row number or `seed`), `previous decision` (line or `none`), and `hint` (human retry hint or `none`).
25
25
 
26
+ Read additive inputs `Leg:`, `Rules:`, mapped original AC and, for companions, exact `pending:` tuple. Read the combined spec/task `Rules:` files before acting, resolving them in the supplied leg checkout. Consume the caller's mapping, not ownership inferred from spec prose. Keep git operations, code and scoped tests in the supplied checkout with its per-leg base; read spec and ledger at their absolute home paths.
27
+
26
28
  ## Procedure
27
29
 
28
30
  1. Read the given spec in full before reading the ledger or code. Read the given ledger next.
@@ -46,6 +48,14 @@ Append exactly one decision line: `row 0 done | seeded <k> rows, closed <m> on b
46
48
 
47
49
  Stage the seeded evidence changes and ledger, commit exactly once with subject `loop: seed evidence`, check for a clean tree, and reply with the appended seed decision only.
48
50
 
51
+ ## Companion result commit
52
+
53
+ For a companion `pending:` input, replace ledger mutation/staging and decision appending in the procedure and output contract with the commit-body path below; leave the home ledger untouched. Limit work and seed checks to mapped entries for this leg. Apply the same one-commit and bounded evidence contracts. Prepare the resulting four-column `row | status | attempts | evidence` target row in memory, not a sidecar. Serialize it in the companion commit body instead of editing the home ledger; for seed serialize all and only this leg's non-cut rows, preserving their attempts and proven baseline closures. For ordinary work serialize only its target, attempts +1 and status done/failed.
54
+
55
+ Use the existing subject `loop: row <n> <status>` or `loop: seed evidence`. Put the exact dispatched `pending:` tuple and consumed `iteration:` on separate body lines, then a blank line, then the resulting pipe-delimited table rows without a header, then a blank line and the existing bounded decision line. Include the check name plus at most three semicolon-separated result lines and escaped pipes in each evidence cell. Compute seed decision counts from this leg only. Stage code/tests explicitly and commit exactly once in the companion; check clean status. Reply with the existing decision line only. Let the parent validate the commit and write the home ledger.
56
+
57
+ For coordinated home work, edit only mapped home entries and its seeded state under the ordinary ledger contract; preserve foreign entries, mappings, bases and all other headers.
58
+
49
59
  ## Boundaries
50
60
 
51
61
  Edit code and tests only for the target row, or the open rows during seed. Outside `target: seed`, never edit a foreign ledger row. Preserve row identifiers, row text, ledger headers, and existing decisions. Never edit the spec or the plan directory outside the supplied ledger. Never touch `main`. Leave whole-project verification and reviews to the parent.
@@ -30,7 +30,7 @@ The task names five absolute paths: the worktree, the approved spec, the plan pa
30
30
 
31
31
  1. Read the repo's `AGENTS.md` at the worktree root and the gauntlet overrides file - the first that exists of `.pi/gauntlet-overrides.md`, `gauntlet-overrides.md`, `doc/gauntlet-overrides.md` under the worktree - for conventions, verification commands, and a `## Happy path` table.
32
32
  2. Read the plan skill file (`SKILL.md`) from its `## File Structure` heading through the end of its `## Self-Review` section, and the whole plan contract file (`plan-contract.md`). That text is the plan contract; follow it as written.
33
- 3. Read the spec in full. Recon the code the spec touches by reading it directly: exact paths to create, modify, or delete; call sites and tests with line ranges; the scoped command that runs one test file; scoped and repo-wide lint and format commands; the full-suite verification entrypoint and whether it bundles style checks. Dispatch nothing.
33
+ 3. Read the spec in full. Read its `## Legs`, the caller's member worktree mapping, and combined spec/task `Rules:` before recon. Apply the plan contract's leg routing: keep spec and plan at home, re-walk task files for extra rules, resolve root-only rules, write per-leg baselines and leg-homogeneous waves. Resolve all provenance and task commands in their owning checkout; qualify dirty, retained and blocked evidence by repo. Recon the code the spec touches by reading it directly: exact paths to create, modify, or delete; call sites and tests with line ranges; the scoped command that runs one test file; scoped and repo-wide lint and format commands; the full-suite verification entrypoint and whether it bundles style checks. Dispatch nothing.
34
34
  4. Write the plan at the plan path with `write` as a full replacement. The plan path is the only file you write; never create or edit any other file.
35
35
  5. Run Baseline from the skill's Self-Review before `plan_check({ planPath })`. On Findings/Amend re-dispatch, read provenance once for the current base; retain evidence for unchanged commands whose recorded SHA is BASE or proven tree-equivalent per the skill's Baseline step 2. Rerun missing, invalid, or changed-command evidence, every command on any proof failure, and every command for `Findings: rerun baseline`. Rewrite SHAs only after execution or a recorded tree-equivalence. Baseline owns and regenerates its rows; preserve unresolved contract rows.
36
36
  6. Run the deterministic checker and judgment checks in Self-Review, including Contract decisions. Repair `baseline-line` and substantive baseline `open-questions` findings only by Baseline execution, never by authoring evidence or deleting/rewording a row. Normalize malformed Open Questions into rows without changing substantive text. Fix every other finding; with substantive rows remaining, reply `OPEN QUESTIONS <abs plan path>` immediately, without a passing check or repair rounds for those rows. Any ordinary edit returns to step 5.
@@ -11,6 +11,8 @@ completionGuard: false
11
11
 
12
12
  You are a spec compliance reviewer. Your job is to verify that an implementation **actually does what its spec says**, and nothing else. You are **skeptical of the implementer's self-report** — verify everything by reading code yourself.
13
13
 
14
+ Read the combined spec/task `Rules:` files before acting. Resolve repo-relative rules in the caller's `Leg:` review checkout. Read the authoritative home spec at the supplied absolute path and review the caller's mapped original AC and leg; report missing rules or authority as blocking findings. Keep whole-file reads in the supplied review tree.
15
+
14
16
  ## Process
15
17
 
16
18
  <!-- clause decomposition / snippet non-authority / whole-file reads / task-contract supplement: keep in lockstep with skills/subagent-driven-development/spec-reviewer-prompt.md — change them together or not at all -->
@@ -317,6 +317,7 @@ function foldAccumulators(a, b) {
317
317
  out.events_dropped = a.events_dropped + b.events_dropped;
318
318
  return out;
319
319
  }
320
+ var validLegs = (legs) => Array.isArray(legs) && legs.every((leg) => leg && typeof leg.repo === "string" && leg.repo.trim().length > 0 && (typeof leg.sha === "string" && leg.sha.trim().length > 0 && leg.pr === void 0 || typeof leg.pr === "string" && /^https?:\/\/\S+$/.test(leg.pr) && leg.sha === void 0));
320
321
  function newRecord(o) {
321
322
  return { schema: 1, spec: o.spec, run_id: o.runId, branch: o.branch, status: "in_progress", created_at: o.now, sessions: [o.session], derived: { engine: "waves", duration_s: 0, phases: {}, personas: {}, conformance_loops: 0, gates: { spec_rounds: 0, plan_rounds: 0, fix_round_grants: 0, task_reopens: 0 }, amendments: 0, spec_edits_after_ship: 0, spec_writes: {}, events_dropped: 0 }, accumulators: {}, events: [] };
322
323
  }
@@ -403,6 +404,7 @@ function parseRecord(text) {
403
404
  if (!doc || typeof doc !== "object") return void 0;
404
405
  const d = doc;
405
406
  if (d.schema !== 1 || typeof d.spec !== "string" || typeof d.run_id !== "string") return void 0;
407
+ if (d.legs !== void 0 && !validLegs(d.legs)) return void 0;
406
408
  const accumulators = Object.fromEntries(Object.entries(d.accumulators ?? {}).flatMap(
407
409
  ([session, block]) => block && typeof block === "object" && !Array.isArray(block) ? [[session, { ...emptyAccumulators(), ...block }]] : []
408
410
  ));
@@ -417,7 +419,7 @@ var fail = (code, message) => {
417
419
  };
418
420
  var usage = () => fail(1, "usage: gauntlet-telemetry-seal --worktree <abs path> --option <pr|squash> --base <ref> [--spec <spec path inside the checkout>] [--dir <telemetry dir>]");
419
421
  function parseArgs(argv) {
420
- const opts = { worktree: void 0, option: void 0, base: void 0, spec: void 0, dir: void 0 };
422
+ const opts = { worktree: void 0, option: void 0, base: void 0, spec: void 0, dir: void 0, legs: [] };
421
423
  for (let i = 0; i < argv.length; i++) {
422
424
  const a = argv[i];
423
425
  const value = argv[i + 1];
@@ -429,7 +431,15 @@ function parseArgs(argv) {
429
431
  else if (a === "--option") takes("option");
430
432
  else if (a === "--base") takes("base");
431
433
  else if (a === "--spec") takes("spec");
432
- else if (a === "--dir") takes("dir");
434
+ else if (a === "--leg") {
435
+ if (!value || value.startsWith("--")) fail(1, "invalid --leg " + value);
436
+ i++;
437
+ const split = value.indexOf("=");
438
+ const repo = value.slice(0, split), result = value.slice(split + 1);
439
+ const leg = /^https?:\/\//.test(result) ? { repo, pr: result } : { repo, sha: result };
440
+ if (split < 1 || !validLegs([leg])) fail(1, "invalid --leg " + value);
441
+ opts.legs.push(leg);
442
+ } else if (a === "--dir") takes("dir");
433
443
  else usage();
434
444
  }
435
445
  if (!opts.worktree || !isAbsolute2(opts.worktree) || !opts.base || !OPTIONS.has(opts.option)) usage();
@@ -469,10 +479,15 @@ async function seal(vcs, rec, o) {
469
479
  const prior = readFileSync(abs);
470
480
  const parsed = parseRecord(prior.toString("utf8"));
471
481
  if (!parsed) fail(2, `unparseable record ${rec}: schema 1 with spec and run_id required`);
482
+ const normalized = (legs) => [...new Set(legs.map((leg) => JSON.stringify([leg.repo.toLowerCase(), leg.sha ?? leg.pr])))].sort();
483
+ if (parsed.status !== "in_progress" && JSON.stringify(normalized(parsed.legs ?? [])) !== JSON.stringify(normalized(o.legs))) {
484
+ fail(1, `seal legs differ: stored ${JSON.stringify(parsed.legs ?? [])}; supplied ${JSON.stringify(o.legs)}`);
485
+ }
472
486
  if (parsed.status !== "in_progress") {
473
487
  if (parsed.status !== "shipped" || recordSealed(vcs, rec)) return console.log(`already sealed ${rec}`);
474
488
  } else {
475
489
  const now = isoNow();
490
+ if (o.legs.length) parsed.legs = o.legs;
476
491
  parsed.status = "shipped";
477
492
  parsed.shipped_at = now;
478
493
  parsed.events.push({ ts: now, session: parsed.sessions.at(-1) ?? "seal", phase: "ship", kind: "ship", option: o.option });
@@ -509,7 +524,7 @@ async function main() {
509
524
  if (!telemetry.enabled) return console.log("telemetry disabled");
510
525
  const dir = override?.dir ?? telemetry.dir;
511
526
  if (!validateBase(vcs, opts.base)) fail(1, `no base ref ${opts.base}`);
512
- const o = { option: opts.option, base: opts.base, dir, buckets: telemetry.buckets, planDirs };
527
+ const o = { legs: opts.legs, option: opts.option, base: opts.base, dir, buckets: telemetry.buckets, planDirs };
513
528
  if (opts.spec) return seal(vcs, recordPathFor(dir, rel), o);
514
529
  const diff = changedNames(vcs, opts.base);
515
530
  if (!diff.ok) fail(1, `${vcs.kind} diff failed: ${diff.stderr}`);
@@ -115,6 +115,36 @@ test("seals an untracked in_progress record: stamp, ship event, diff, re-derived
115
115
  assert.equal(porcelain(root), "");
116
116
  });
117
117
 
118
+ test("shipped CLI records companion legs and validates complete retry sets", (t) => {
119
+ const root = repo(); cleanup(t, root);
120
+ const args = ["--spec", SPEC, "--leg", "repo-b=abc1234", "--leg", "repo-c=https://example.test/pull/2?q=yes"];
121
+ const result = run(root, args);
122
+ assert.equal(result.status, 0, result.stderr);
123
+ assert.deepEqual(record(root).legs, [
124
+ { repo: "repo-b", sha: "abc1234" },
125
+ { repo: "repo-c", pr: "https://example.test/pull/2?q=yes" },
126
+ ]);
127
+ const sealedHead = head(root);
128
+ assert.equal(run(root, ["--spec", SPEC, "--leg", args[5], "--leg", args[3]]).status, 0);
129
+ assert.equal(head(root), sealedHead);
130
+ for (const legs of [[], ["--leg", "repo-b=abc1234"], ["--leg", "repo-b=changed", "--leg", args[5]]]) {
131
+ const retry = run(root, ["--spec", SPEC, ...legs]);
132
+ assert.equal(retry.status, 1);
133
+ assert.match(retry.stderr, /legs differ/);
134
+ assert.equal(head(root), sealedHead);
135
+ }
136
+ assert.deepEqual(headFiles(root), [REC]);
137
+ });
138
+
139
+ test("shipped CLI rejects malformed leg values before sealing", () => {
140
+ for (const value of ["missing", "=abc", "repo=", "repo= "]) {
141
+ const result = invoke(["--leg", value]);
142
+ assert.equal(result.status, 1);
143
+ assert.match(result.stderr, /invalid --leg/);
144
+ assert.ok(result.stderr.includes(value.trim()));
145
+ }
146
+ });
147
+
118
148
  test("docs/specs record seals with --spec (no path classification)", (t) => {
119
149
  const root = docsRepo(); cleanup(t, root);
120
150
  const r = run(root, ["--spec", SPEC2]);
@@ -1218,3 +1218,119 @@ test("tests-block: fenced **Tests:** lines are ignored", () => {
1218
1218
  );
1219
1219
  assert.deepEqual(tb(mutated), []);
1220
1220
  });
1221
+
1222
+ const coordinatedSpec = ledgerSpec + `\n## Legs
1223
+ ### Leg: repo-a
1224
+ branch: feature/run
1225
+ scope: AC 1, AC 2
1226
+ ### Leg: repo-b
1227
+ branch: feature/run
1228
+ scope: AC 1
1229
+ `;
1230
+ const coordinatedLedger = () => ledger(["1 | open | 0 |", "2 | open | 0 |", "3 | cut | 0 | deferred: #70", "4 | open | 0 |"])
1231
+ .replace("base: abc", "base: abc1234\nbase repo-a: abc1234\nbase repo-b: def5678\nseeded repo-a: no\nseeded repo-b: no\ncap: 20")
1232
+ + "\n## Leg mapping\n| row | ac | leg |\n|---|---|---|\n| 1 | 1 | repo-a |\n| 2 | 1 | repo-b |\n| 3 | 2 | repo-a |\n| 4 | 3 | repo-a |\n";
1233
+ const emptyPort: FsPort = { exists: () => false, glob: () => [] };
1234
+ test("coordinated ledger expands shared ownership and defaults unlisted ACs home", () => {
1235
+ assert.deepEqual(checkPlan(coordinatedLedger(), coordinatedSpec, emptyPort), []);
1236
+ });
1237
+ for (const scope of ["", "scope: AC 1\nscope: AC 2", "scope: AC 1, AC 1", "scope: AC 0", "scope: AC -1", "scope: AC 1.5", "scope: AC 4", "scope: 1", "scope: AC 1-2", "scope: AC 1,", "scope: none, AC 1"]) {
1238
+ test(`coordinated scope rejects ${JSON.stringify(scope)}`, () => {
1239
+ assert.ok(checkPlan(coordinatedLedger(), coordinatedSpec.replace("scope: AC 1, AC 2", scope), emptyPort).some(f => f.check === "leg-scope"));
1240
+ });
1241
+ }
1242
+ for (const [name, change] of Object.entries({
1243
+ missingMapping: (s: string) => s.replace("| 2 | 1 | repo-b |\n", ""),
1244
+ duplicateMapping: (s: string) => s + "| 2 | 1 | repo-b |\n",
1245
+ wrongOrder: (s: string) => s.replace("| 1 | 1 | repo-a |", "| 1 | 1 | repo-b |"),
1246
+ foreignBase: (s: string) => s + "base stranger: abc1234\n",
1247
+ missingSeed: (s: string) => s.replace("seeded repo-b: no\n", ""),
1248
+ invalidCap: (s: string) => s.replace("cap: 20", "cap: 0"),
1249
+ homePending: (s: string) => s + "pending: 1 | repo-a | abc1234\n",
1250
+ foreignTarget: (s: string) => s + "pending: 3 | repo-b | abc1234\n",
1251
+ duplicatePending: (s: string) => s + "pending: seed | repo-b | abc1234\npending: seed | repo-b | abc1234\n",
1252
+ })) test(`coordinated ledger rejects ${name}`, () => {
1253
+ assert.ok(checkPlan(change(coordinatedLedger()), coordinatedSpec, emptyPort).length);
1254
+ });
1255
+ test("coordinated ledger admits partial seeds, recovery tuples and later cap grants", () => {
1256
+ for (const target of ["2", "seed"]) assert.deepEqual(checkPlan(coordinatedLedger().replace("seeded repo-a: no", "seeded repo-a: yes").replace("cap: 20", "cap: 99") + `pending: ${target} | REPO-B | abc1234\n`, coordinatedSpec, emptyPort), []);
1257
+ });
1258
+
1259
+ const legPlanSpec = SPEC_TEXT + "\n## Legs\n### Leg: repo-a\nscope: none\n### Leg: repo-b\nscope: none\n";
1260
+ const legPlan = () => VALID_PLAN.replace("---", "Baseline repo-a: abc1234\nBaseline repo-b: def5678\n---")
1261
+ .replace("### Task 2: Implement naming", "## Wave 3 - Companion\n\n### Task 2: Implement naming\nLeg: REPO-B")
1262
+ .replace("- baseline: abc1234 pass\n\nThis task handles", "- baseline: def5678 pass\n\nThis task handles");
1263
+ test("plan accepts distinct leg baselines, case-insensitive membership and absent home Leg", () => {
1264
+ const probes: (string | undefined)[] = [];
1265
+ assert.deepEqual(checkPlan(legPlan(), legPlanSpec, { exists: (_p, leg) => { probes.push(leg); return true; }, glob: () => [] }), []);
1266
+ assert.ok(probes.includes("repo-b"));
1267
+ assert.ok(probes.includes("repo-a"));
1268
+ });
1269
+ test("wrong-leg baseline and mixed-leg waves fail", () => {
1270
+ assert.ok(checkPlan(legPlan().replace("def5678 pass", "abc1234 pass"), legPlanSpec, alwaysTruePort()).some(f => f.check === "baseline-line"));
1271
+ assert.ok(checkPlan(legPlan().replace("## Wave 3 - Companion\n\n", ""), legPlanSpec, alwaysTruePort()).some(f => f.check === "wave-leg"));
1272
+ });
1273
+ test("unknown legs fail before filesystem probing", () => {
1274
+ const probes: string[] = [];
1275
+ assert.ok(checkPlan(legPlan().replace("Leg: REPO-B", "Leg: stranger"), legPlanSpec, { exists: (p) => { probes.push(p); return true; }, glob: () => [] }).some(f => f.check === "leg-unknown"));
1276
+ assert.deepEqual(probes, []);
1277
+ });
1278
+ test("cross-leg creates do not supply Test paths or skipped baseline evidence", () => {
1279
+ const plan = legPlan().replaceAll("extensions/lib/fixture-task2.test.ts", "extensions/lib/fixture-task1.ts").replace("def5678 pass", "def5678 skipped - extensions/lib/fixture-task1.ts created by this task");
1280
+ const findings = checkPlan(plan, legPlanSpec, { exists: p => p !== "extensions/lib/fixture-task1.ts", glob: () => [] });
1281
+ assert.ok(findings.some(f => f.check === "tests-block" && /unknown/.test(f.reason)));
1282
+ assert.ok(findings.some(f => f.check === "baseline-line" && /not a Create/.test(f.reason)));
1283
+ });
1284
+ test("identical paths across legs do not overlap even in rejected mixed waves", () => {
1285
+ const plan = legPlan().replace("## Wave 3 - Companion\n\n", "").replaceAll("fixture-other.ts", "fixture-shared.ts");
1286
+ assert.deepEqual(checkPlan(plan, legPlanSpec, alwaysTruePort()).filter(f => f.check === "wave-file-disjointness"), []);
1287
+ });
1288
+
1289
+ for (const [name, change] of Object.entries({
1290
+ duplicateBase: (s: string) => s + "base repo-b: def5678\n",
1291
+ malformedSha: (s: string) => s.replace("base repo-b: def5678", "base repo-b: not-a-sha"),
1292
+ baseMismatch: (s: string) => s.replace("base: abc1234", "base: bbb1234"),
1293
+ invalidSeed: (s: string) => s.replace("seeded repo-a: no", "seeded repo-a: maybe"),
1294
+ duplicateSeed: (s: string) => s + "seeded repo-b: yes\n",
1295
+ negativeIterations: (s: string) => s.replace("iterations: 0", "iterations: -1"),
1296
+ unsafeIterations: (s: string) => s.replace("iterations: 0", "iterations: 9007199254740992"),
1297
+ fractionalCap: (s: string) => s.replace("cap: 20", "cap: 1.5"),
1298
+ unsafeCap: (s: string) => s.replace("cap: 20", "cap: 9007199254740992"),
1299
+ malformedPending: (s: string) => s + "pending: 2 | repo-b | not-a-sha\n",
1300
+ unknownPending: (s: string) => s + "pending: seed | stranger | abc1234\n",
1301
+ nonexistentPending: (s: string) => s + "pending: 99 | repo-b | abc1234\n",
1302
+ nonexistentAC: (s: string) => s.replace("| 4 | 3 | repo-a |", "| 4 | 4 | repo-a |"),
1303
+ duplicatePair: (s: string) => s.replace("| 2 | 1 | repo-b |", "| 2 | 1 | repo-a |"),
1304
+ unknownMapping: (s: string) => s.replace("| 2 | 1 | repo-b |", "| 2 | 1 | stranger |"),
1305
+ disposition: (s: string) => s.replace("| 3 | cut | 0 | deferred: #70 |", "| 3 | open | 0 | |"),
1306
+ })) test(`coordinated metadata rejects ${name}`, () => {
1307
+ assert.ok(checkPlan(change(coordinatedLedger()), coordinatedSpec, emptyPort).length);
1308
+ });
1309
+ test("scope textual order does not change original AC or member order, and design claims no ownership", () => {
1310
+ const spec = coordinatedSpec.replace("scope: AC 1, AC 2", "scope: AC 2, AC 1\ndesign: AC 3 touches lib/other.ts");
1311
+ assert.deepEqual(checkPlan(coordinatedLedger().replace("| 4 | open | 0 | |", "| 4 | done | 1 | checked a\\|b |"), spec, emptyPort), []);
1312
+ });
1313
+ test("scope none defaults all original ACs home, preserving cut numbering and ignoring nested checks", () => {
1314
+ const spec = coordinatedSpec.replace("- [ ] First", "- [ ] First\n - [ ] Nested").replace("scope: AC 1, AC 2", "scope: none").replace("scope: AC 1\n", "scope: none\n");
1315
+ const input = coordinatedLedger().replace("| 2 | open | 0 | |\n", "").replace("| 3 | cut", "| 2 | cut").replace("| 4 | open", "| 3 | open")
1316
+ .replace("| 2 | 1 | repo-b |\n", "").replace("| 3 | 2 | repo-a |", "| 2 | 2 | repo-a |").replace("| 4 | 3 | repo-a |", "| 3 | 3 | repo-a |");
1317
+ assert.deepEqual(checkPlan(input, spec, emptyPort), []);
1318
+ });
1319
+ test("single-member ledger preserves legacy headers and four-column bytes", () => {
1320
+ assert.deepEqual(checkPlan(ledger(), ledgerSpec + "\n## Legs\n### Leg: repo-a\n", emptyPort), []);
1321
+ });
1322
+ test("leg-local globs and plain Files: resolve at each normalized leg", () => {
1323
+ const calls: string[] = [];
1324
+ const plan = legPlan().replaceAll("**Files:**", "Files:").replace("Modify: extensions/lib/fixture-other.ts", "Modify: extensions/lib/companion-*.ts");
1325
+ assert.deepEqual(checkPlan(plan, legPlanSpec, { exists: () => true, glob: (pattern, leg) => { calls.push(`${leg}:${pattern}`); return ["extensions/lib/companion-one.ts"]; } }), []);
1326
+ assert.deepEqual(calls, ["repo-b:extensions/lib/companion-*.ts"]);
1327
+ });
1328
+ test("coordinated scope counts only original unfenced top-level AC rows", () => {
1329
+ const spec = coordinatedSpec.replace("- [ ] First", "```text\n- [ ] Example, not an AC\n```\n- [ ] First");
1330
+ assert.deepEqual(checkPlan(coordinatedLedger(), spec, emptyPort), []);
1331
+ assert.ok(checkPlan(coordinatedLedger(), spec.replace("scope: AC 1\n", "scope: AC 4\n"), emptyPort).some(f => f.check === "leg-scope"));
1332
+ });
1333
+ test("coordinated ledger rejects duplicate mapping sections", () => {
1334
+ const input = coordinatedLedger();
1335
+ assert.ok(checkPlan(input + input.slice(input.indexOf("## Leg mapping")), coordinatedSpec, emptyPort).length);
1336
+ });