pi-gauntlet 8.4.9 → 8.4.10

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v8.4.10 - 2026-11-12
4
+
5
+ - Draft-PR handoff cleanup checks live remote equality before removing owned trees and conditionally deleting local branches at the pinned commit. Guarded wrappers can delete branches; host-owned, mismatched and failed cleanups report the state read back.
6
+ - Coordinated brainstorms record portable script/native creation and destruction commands without inventing a `home:` path. Keyed and built-in creators retain `home:`, pre-existing host-owned trees retain ownership, and companion lookup uses the recorded branch with checkout identity validation.
7
+ - pi-cohort peer floor raised to 7.7.5.
8
+
3
9
  ## v8.4.9 - 2026-11-11
4
10
 
5
11
  - Gatekeep-pr uses action, final and post menus with the judged recommendation on row 1. Its plain-language report describes the change and scope, explains standing and problems, then recommends a choice; detailed evidence stays behind `show evidence`.
package/README.md CHANGED
@@ -36,7 +36,7 @@ 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. 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.
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. After confirming the live remote and local branch match the worktree commit, cleanup removes an owned tree and conditionally deletes the local branch at that pinned commit. A host-owned checkout or failed remote check keeps tree and branch, with a per-repo reason. 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 auto-applies amends, including unreachable-row cuts, and funds conformance rounds up to the allowance when the cap blocks. Redraws, exhausted bounds and unproven recovery still stop. See [Loop engine operations](doc/configuration.md#loop-engine-operations).
41
41
  3. **`subagent-driven-development`** executes the plan in waves, with task implementers followed by one integrated `wave-reviewer` after the scoped test gate passes. The reviewer checks anchored spec clauses and code quality together; repairs stay on the current tree and pass the same gate before re-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. Each row starts with the shared allowance (default 4); approval replies such as `4; allowance 6` override it for one run. Under either loop approval option, attempt `escalateAfter + 1` (default 4), clamped to the last allowed attempt, escalates once per row on `escalationLoop.implModel` (the main-loop model when unset); later attempts use the ordinary worker model. The allowance also bounds seed and exit-verification repair counts; verification repairs use the same escalation model and count home repair commits plus an unmatched `pending: verify` reservation. Stagnation supplies a row-local hint, not an early stop. Interrupted returns spend attempts after ownership-fenced recovery: the driver confirms owned jobs exited before resetting the checkout; unknown ownership or failed cleanup requires a human decision. An admitted `unreachable:` target failure becomes an audited scope cut under amend permission, never a passing row. At a row stop, `retry <n> | <hint>` adds one attempt to that row, while `rows +N` raises every row's allowance. Attempts never reset and there is no total iteration cap. Ledger refresh completes implement when every row is closed; verify step 1 runs the gate set: deduplicated (owning checkout, command) pairs from done rows' recorded checks, with ownership from the ledger. Gate output goes to logs; the driver keeps exit codes and paths, not test output. CI on push owns the full run.
@@ -75,13 +75,13 @@ Between gate 1 and gate 2, both engines take named authorized recommendations un
75
75
 
76
76
  ### Multi-repo runs
77
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.
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 shared creation NAME (branch with `/` replaced by `-`); a script or native tool can choose a different folder name.
79
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).
80
+ The approved home spec records `## Legs`, including each leg's scope, rules and resolved `contract:`. Keyed and built-in creators retain `home:`; flow-created script/native trees record portable `create:` and `destroy:` without `home:`, and a pre-existing host-owned tree without a keyed contract records neither `home:` nor `destroy:`. Script creation passes the shared NAME and explicit branch, while destruction uses the located folder's basename. Companion lookup selects the unique linked tree on the recorded branch and validates its checkout identity, rather than computing its location from `home:`. 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
81
 
82
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
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.
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). Cleanup runs companions first, then home, removing owned trees and deleting local branches only after a live remote equality check and confirmed removal; each leg reports its read-back state even when another cleanup fails. This supersedes the kept-local-branch cleanup in the [spec-only draft-PR design](doc/specs/2026-10-28-spec-only-draft-pr.md); a remote branch deleted after handoff now needs restoration from a clone that still has it, not the cleaned-up machine. 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
85
 
86
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
87
 
@@ -148,7 +148,7 @@ Run by a human against pi-cohort >= 7.1.0, before a pi-gauntlet release claims t
148
148
  ## Requirements
149
149
 
150
150
  - [pi-coding-agent](https://github.com/earendil-works/pi) >= 1.5.0 - supports model-only tracker exposure and sequential tracker execution, so updates persist as transcript tool results rather than nested calls.
151
- - [pi-cohort](https://github.com/jjuraszek/pi-cohort) >= 7.7.4 - 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.
151
+ - [pi-cohort](https://github.com/jjuraszek/pi-cohort) >= 7.7.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.
152
152
 
153
153
  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.
154
154
 
@@ -2718,8 +2718,8 @@ for (const pattern of [".worktrees/<name>", "../companion-<name>"]) test(`plan_c
2718
2718
  const repo = `team/${name}`;
2719
2719
  const cloneUrl = `git@example.test:${repo}.git`;
2720
2720
  execFileSync("git", ["config", "remote.origin.url", `https://example.test/${repo}.git`], { cwd: companion });
2721
- const branch = "feature/legs";
2722
- const companionRoot = join(companion, pattern.replace("<name>", "feature-legs"));
2721
+ const branch = "user/slug";
2722
+ let companionRoot = join(companion, pattern.replace("<name>", "user-slug"));
2723
2723
  execFileSync("git", ["worktree", "add", "-b", branch, companionRoot], { cwd: companion, stdio: "pipe" });
2724
2724
  if (pattern.startsWith("..")) tempDirs.push(companionRoot);
2725
2725
  mkdirSync(join(primary, ".pi"), { recursive: true });
@@ -2801,6 +2801,22 @@ for (const pattern of [".worktrees/<name>", "../companion-<name>"]) test(`plan_c
2801
2801
  for (const spy of spies) spy.mock.restore();
2802
2802
  syncBuiltinESMExports();
2803
2803
  }
2804
+ const specPath = join(primary, "doc/specs/spec.md");
2805
+ const originalSpec = readFileSync(specPath, "utf8");
2806
+ const scriptHome = join(companion, "..", `${name}-worktrees`);
2807
+ tempDirs.push(scriptHome);
2808
+ mkdirSync(scriptHome);
2809
+ const scriptRoot = join(scriptHome, "slug");
2810
+ execFileSync("git", ["worktree", "move", companionRoot, scriptRoot], { cwd: companion, stdio: "pipe" });
2811
+ companionRoot = scriptRoot;
2812
+ for (const home of [undefined, `../${name}-worktrees/slug`, ".worktrees/<name>"]) {
2813
+ await t.test(`script-placed companion with home ${home ?? "absent"}`, async () => {
2814
+ writeFileSync(specPath, originalSpec.replace(`contract: home: ${pattern}`, home ? `contract:\n- home: ${home}` : "contract:"));
2815
+ const result = await tool.execute("script-placed", { planPath }, undefined, undefined, h.ctx) as { details: { status: string; findings?: unknown } };
2816
+ assert.equal(result.details.status, "pass", JSON.stringify(result.details.findings));
2817
+ });
2818
+ }
2819
+ writeFileSync(specPath, originalSpec);
2804
2820
  const originalPlan = readFileSync(planPath, "utf8");
2805
2821
  const foreignSpec = join(companionRoot, "foreign-spec.md");
2806
2822
  writeFileSync(foreignSpec, readFileSync(join(primary, "doc/specs/spec.md")));
@@ -2817,6 +2833,66 @@ for (const pattern of [".worktrees/<name>", "../companion-<name>"]) test(`plan_c
2817
2833
  assert.ok(missing.details.findings.some(f => f.check === "input"));
2818
2834
  });
2819
2835
 
2836
+ for (const state of ["unlinked", "primary", "detached", "not a checkout", "foreign linked checkout", "duplicate", "missing branch", "lookup failed"] as const) {
2837
+ test(`plan_check rejects companion branch lookup: ${state}`, async () => {
2838
+ const primary = gitPrimary().primary;
2839
+ const companion = gitPrimary().primary;
2840
+ const name = companion.split("/").at(-1)!;
2841
+ const repo = `team/${name}`;
2842
+ const branch = "user/slug";
2843
+ const companionRoot = join(companion, ".worktrees", "slug");
2844
+ const git = (...args: string[]) => execFileSync("git", ["-C", companion, ...args], { stdio: "pipe" });
2845
+ git("config", "remote.origin.url", `https://example.test/${repo}.git`);
2846
+ git("branch", branch);
2847
+ if (state === "primary") git("checkout", branch);
2848
+ if (state === "detached") git("worktree", "add", "--detach", companionRoot, branch);
2849
+ if (state === "not a checkout" || state === "foreign linked checkout" || state === "duplicate") {
2850
+ git("worktree", "add", companionRoot, branch);
2851
+ if (state === "duplicate") git("worktree", "add", "--force", `${companionRoot}-other`, branch);
2852
+ else {
2853
+ rmSync(companionRoot, { recursive: true });
2854
+ if (state === "foreign linked checkout") {
2855
+ execFileSync("git", ["-C", primary, "worktree", "add", "--detach", companionRoot], { stdio: "pipe" });
2856
+ } else mkdirSync(companionRoot);
2857
+ }
2858
+ }
2859
+ mkdirSync(join(primary, ".pi"), { recursive: true });
2860
+ writeFileSync(join(primary, ".pi/gauntlet-overrides.md"), `## shape-ticket\n- sibling: git@example.test:${repo}.git\n`);
2861
+ const { planPath } = writePlanFixture(primary, {
2862
+ mutateSpec: s => s + `\n## Legs\n### Leg: team/repo-a\nscope: none\nbranch: ${branch}\n### Leg: ${repo}\nscope: none\n${state === "missing branch" ? "" : `branch: ${branch}\n`}contract:\n`,
2863
+ mutatePlan: s => s.replace("---", `Baseline team/repo-a: abc1234\nBaseline ${repo}: def5678\n---`).replace("### Task 2: Implement naming", `## Wave 2 - Companion\n\n### Task 2: Implement naming\nLeg: ${repo}`).replace("abc1234 skipped - lib/task2", "def5678 skipped - lib/task2"),
2864
+ });
2865
+ const h = harness({ cwd: primary });
2866
+ await h.emit("session_start");
2867
+ const tool = h.tools.find(t => t.name === "plan_check")!;
2868
+ const original = childProcess.execFileSync;
2869
+ const spy = state === "lookup failed" ? mock.method(childProcess, "execFileSync", (...args: any[]) => {
2870
+ if (args[0] === "git" && args[2]?.cwd === companion && JSON.stringify(args[1]) === JSON.stringify(["worktree", "list", "--porcelain", "-z"])) {
2871
+ throw Object.assign(new Error("worktree listing failed"), { status: 128 });
2872
+ }
2873
+ return (original as Function)(...args);
2874
+ }) : undefined;
2875
+ if (spy) syncBuiltinESMExports();
2876
+ let result: { details: { status: string; findings: { check: string; reason: string }[] } };
2877
+ try {
2878
+ result = await tool.execute(state, { planPath }, undefined, undefined, h.ctx) as typeof result;
2879
+ } finally {
2880
+ if (spy) {
2881
+ spy.mock.restore();
2882
+ syncBuiltinESMExports();
2883
+ }
2884
+ }
2885
+ const reason = state === "duplicate" ? `2 worktrees hold ${branch}`
2886
+ : state === "not a checkout" || state === "foreign linked checkout" ? `${companionRoot} is not a checkout`
2887
+ : state === "lookup failed" ? `worktree lookup failed for ${branch}`
2888
+ : `no worktree holds ${branch}`;
2889
+ assert.equal(result.details.status, "fail");
2890
+ assert.deepEqual(result.details.findings.map(({ check, reason }) => ({ check, reason })), [
2891
+ { check: "input", reason: state === "missing branch" ? `invalid recorded branch for ${repo.toLowerCase()}` : `cannot resolve worktree for ${repo.toLowerCase()}: ${reason}` },
2892
+ ]);
2893
+ });
2894
+ }
2895
+
2820
2896
  for (const length of [32767, 32768, 32769]) {
2821
2897
  test(`plan_check complete manifest JS length ${length} bounds stamping`, async () => {
2822
2898
  const dir = gitPrimary().primary;
@@ -1026,13 +1026,20 @@ export default function (pi: ExtensionAPI) {
1026
1026
  }
1027
1027
  if (candidates.length !== 1) return inputFailure(`cannot resolve selected sibling ${leg.repo}`);
1028
1028
  const branch = /^branch:\s*(\S+)\s*$/m.exec(leg.body)?.[1];
1029
- const home = /^(?:contract:\s*)?(?:-\s*)?home:\s*(\S+)\s*$/m.exec(leg.body)?.[1];
1030
- if (!branch || !home || home.split("<name>").length !== 2 || /<[^>]+>/.test(home.replace("<name>", ""))) return inputFailure(`invalid recorded branch or home pattern for ${leg.repo}`);
1031
- const legRoot = resolve(candidates[0], home.replace("<name>", branch.replaceAll("/", "-")));
1029
+ if (!branch) return inputFailure(`invalid recorded branch for ${leg.repo}`);
1030
+ const unresolved = (reason: string) => inputFailure(`cannot resolve worktree for ${leg.repo}: ${reason}`);
1031
+ const listing = gitSync(["worktree", "list", "--porcelain", "-z"], candidates[0]);
1032
+ if (listing.code !== 0) return unresolved(`worktree lookup failed for ${branch}`);
1033
+ const matches = listing.stdout.split("\0\0").slice(1)
1034
+ .map(block => block.split("\0"))
1035
+ .filter(fields => fields.includes(`branch refs/heads/${branch}`));
1036
+ if (!matches.length) return unresolved(`no worktree holds ${branch}`);
1037
+ if (matches.length !== 1) return unresolved(`${matches.length} worktrees hold ${branch}`);
1038
+ const legRoot = matches[0].find(field => field.startsWith("worktree "))!.slice("worktree ".length);
1032
1039
  const checkout = existsSync(legRoot) ? await checkoutOf(legRoot, gitSync) : undefined;
1033
1040
  const expected = gitSync(["rev-parse", "--path-format=absolute", "--git-common-dir"], candidates[0]);
1034
1041
  const actual = checkout ? gitSync(["rev-parse", "--path-format=absolute", "--git-common-dir"], legRoot) : undefined;
1035
- if (!checkout || checkout.toplevel !== legRoot || expected.code !== 0 || actual?.code !== 0 || actual.stdout.trim() !== expected.stdout.trim()) return inputFailure(`cannot resolve worktree for ${leg.repo}`);
1042
+ if (!checkout || checkout.isPrimary || checkout.toplevel !== realpathSync(legRoot) || expected.code !== 0 || actual?.code !== 0 || realpathSync(actual.stdout.trim()) !== realpathSync(expected.stdout.trim())) return unresolved(`${legRoot} is not a checkout`);
1036
1043
  roots.set(leg.repo, legRoot);
1037
1044
  }
1038
1045
  }
@@ -313,28 +313,53 @@ test("settings errors appear once per chained call", async () => {
313
313
  assert.equal(warning.split("gauntlet settings load error").length - 1, 1, warning);
314
314
  });
315
315
 
316
- test("coordinated linked worktree verification uses its primary's custom policy", async () => {
317
- const h = harness();
318
- const git = (...args: string[]) => execFileSync("git", ["-C", h.dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
319
- git("init", "-b", "main");
320
- git("-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "--allow-empty", "-m", "base");
321
- const worktree = join(h.dir, "linked-feature");
322
- git("worktree", "add", "-b", "feature", worktree);
323
- mkdirSync(join(h.dir, ".pi"));
324
- writeFileSync(join(h.dir, ".pi/settings.json"), JSON.stringify({ piGauntlet: { verifyBeforeShip: { testCommands: ["custom-check"] } } }));
325
- git("remote", "add", "origin", "https://example.invalid/team/companion.git");
326
- const spec = join(h.dir, "spec.md");
327
- writeFileSync(spec, "## Legs\n### Leg: team/home\n### Leg: team/companion\nbranch: feature\nhome: linked-<name>\n");
328
- await h.emitEvent("tool_result", { toolName: "plan_check", isError: false, details: { status: "pass", specPath: spec } });
329
- for (const [id, command, warns] of [["default", "npm test", true], ["custom", "custom-check", false]] as const) {
330
- await h.emitEvent("tool_call", bashCall(id, `(cd ${worktree} && ${command})`));
331
- await h.emitEvent("tool_result", bashResult(id));
332
- await h.emitEvent("tool_call", bashCall("ship", `git -C ${worktree} push`));
333
- const warning = warningOf(await h.emitEvent("tool_result", bashResult("ship")));
334
- if (warns) assert.match(warning ?? "", /custom-check/);
335
- else assert.equal(warning, undefined);
336
- }
337
- });
316
+ for (const { name, home, state } of [
317
+ { name: "no home", home: "", state: "member" },
318
+ { name: "literal home", home: "home: ../sib-worktrees/slug\n", state: "member" },
319
+ { name: "mismatching home pattern", home: "home: linked-<name>\n", state: "member" },
320
+ { name: "another branch at the recorded home", home: "home: ../sib-worktrees/<name>\n", state: "other" },
321
+ { name: "detached HEAD at the recorded home", home: "home: ../sib-worktrees/<name>\n", state: "detached" },
322
+ { name: "duplicate branch checkouts", home: "", state: "duplicate" },
323
+ ]) {
324
+ test(`coordinated linked worktree policy follows branch membership: ${name}`, async () => {
325
+ const h = harness();
326
+ const primary = join(h.dir, "sib");
327
+ mkdirSync(primary);
328
+ const git = (...args: string[]) => execFileSync("git", ["-C", primary, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
329
+ git("init", "-b", "main");
330
+ git("-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "--allow-empty", "-m", "base");
331
+ const branch = "user/slug";
332
+ const worktree = join(h.dir, "sib-worktrees", state === "other" || state === "detached" ? "user-slug" : "slug");
333
+ git("worktree", "add", "-b", state === "other" ? "other" : branch, worktree);
334
+ if (state === "detached") git("-C", worktree, "checkout", "--detach");
335
+ const targets = [worktree];
336
+ if (state === "duplicate") {
337
+ targets.push(join(h.dir, "sib-worktrees", "duplicate"));
338
+ git("worktree", "add", "--force", targets[1], branch);
339
+ }
340
+ for (const [root, command] of [[primary, "primary-check"], ...targets.map(target => [target, "local-check"])]) {
341
+ mkdirSync(join(root, ".pi"));
342
+ writeFileSync(join(root, ".pi/settings.json"), JSON.stringify({ piGauntlet: { verifyBeforeShip: { testCommands: [command] } } }));
343
+ }
344
+ git("remote", "add", "origin", "https://example.invalid/team/sib.git");
345
+ const spec = join(h.dir, "spec.md");
346
+ writeFileSync(spec, `## Legs\n### Leg: team/home\n### Leg: team/sib\nbranch: ${branch}\n${home}`);
347
+ await h.emitEvent("tool_result", { toolName: "plan_check", isError: false, details: { status: "pass", specPath: spec } });
348
+ const member = state === "member" || state === "duplicate";
349
+ const accepted = member ? "primary-check" : "local-check";
350
+ const rejected = member ? "local-check" : "primary-check";
351
+ for (const target of targets) {
352
+ for (const [command, warns] of [[rejected, true], [accepted, false]] as const) {
353
+ await h.emitEvent("tool_call", bashCall("test", `(cd ${target} && ${command})`));
354
+ await h.emitEvent("tool_result", bashResult("test"));
355
+ await h.emitEvent("tool_call", bashCall("ship", `git -C ${target} push`));
356
+ const warning = warningOf(await h.emitEvent("tool_result", bashResult("ship")));
357
+ if (warns) assert.ok(warning?.includes(accepted), warning);
358
+ else assert.equal(warning, undefined);
359
+ }
360
+ }
361
+ });
362
+ }
338
363
 
339
364
  test("real linked worktree evidence stays separate from primary post-squash verification", async () => {
340
365
  const h = harness();
@@ -209,9 +209,9 @@ export default function (pi: ExtensionAPI) {
209
209
  }
210
210
  return legs.slice(1).some(leg => {
211
211
  const branch = /^branch:\s*(\S+)\s*$/m.exec(leg.body)?.[1];
212
- const home = /^(?:contract:\s*)?(?:-\s*)?home:\s*(\S+)\s*$/m.exec(leg.body)?.[1];
213
- return leg.repo === repo && !!branch && !!home && home.split("<name>").length === 2 &&
214
- target === checkout(resolve(primary, home.replace("<name>", branch.replaceAll("/", "-"))));
212
+ if (leg.repo !== repo || !branch) return false;
213
+ const current = gitSync(["branch", "--show-current"], target);
214
+ return current.code === 0 && current.stdout.trim() === branch;
215
215
  });
216
216
  };
217
217
  const pendingTests = new Map<string, Set<string>>();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "8.4.9",
3
+ "version": "8.4.10",
4
4
  "description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
5
5
  "author": "Jacek Juraszek",
6
6
  "type": "module",
@@ -62,7 +62,7 @@
62
62
  "test": "node scripts/ci.mjs"
63
63
  },
64
64
  "peerDependencies": {
65
- "pi-cohort": ">=7.7.4"
65
+ "pi-cohort": ">=7.7.5"
66
66
  },
67
67
  "dependencies": {
68
68
  "yaml": "^2.9.0"
@@ -51,16 +51,32 @@ Keep flow policy (council, grants, tracker, response channels, finish choice and
51
51
  home flow guards) under home `.pi/settings.json#piGauntlet` and home overrides.
52
52
  Never use `gauntlet_setting` for companions or add a `piGauntlet.*` key.
53
53
 
54
- Record portable templates, not expanded absolute paths:
55
-
56
- - Create: retain the leg's `## using-git-worktrees` keyed `home:`/`create:` contract,
57
- native tool, discovered wrapper, or owned built-in. Resolve other wrapper tokens
58
- from that leg's touched top-level directory; zero or several candidates ask once
59
- for that leg. Retain the resolved portable value, not a machine path.
60
- - Destroy: prefer the `## cleanup` worktree line, then `destroy:`, then owned
61
- built-in removal matching `home:`. Include configured
62
- `subagents.worktreeTeardownHook` with pi-cohort's existing semantics. Never
63
- bypass a wrapper with `git worktree remove`; unmatched homes remain host-owned.
54
+ ### Record portable templates
55
+
56
+ Record only the contract fields authorized by the creator; never record a produced path:
57
+
58
+ | Creator | Recorded fields |
59
+ |---|---|
60
+ | Keyed `## using-git-worktrees` contract | Retain `home:`, `create:`, `destroy:`, including the keyed `.worktrees/<name>` default for omitted `home:`. |
61
+ | Owned built-in | Retain `.worktrees/<name>` as `home:`, with built-in `create:` and `destroy:`. |
62
+ | Native tool or discovered script, tree created by this flow | Record `create:` and `destroy:`; omit `home:`. |
63
+ | Tree the session was already inside, no keyed contract | Omit `home:` and `destroy:`; retain host ownership. |
64
+
65
+ - Create: retain the keyed contract, native tool, discovered wrapper, or owned
66
+ built-in command. Render discovered-script commands from the primary with the
67
+ script's repo-relative path, `<name>` for shared NAME (branch with `/` replaced
68
+ by `-`), and an explicit `<branch>` argument. Use that same NAME at continuation.
69
+ Resolve other wrapper tokens from that leg's touched top-level directory; zero
70
+ or several candidates ask once for that leg. Keep portable values, not machine
71
+ paths, user names, or expanded folders. Require Step 4's branch read-back before
72
+ recording a block; a script unable to take the branch gets no block on mismatch.
73
+ - Destroy: for a flow-created or keyed tree prefer the `## cleanup` worktree line,
74
+ then `destroy:`, then owned built-in removal matching `home:`. Render native and
75
+ script commands from the primary with portable tokens and the script's
76
+ repo-relative path; set `<name>` at execution to the located tree's folder
77
+ basename, not shared NAME. Include configured `subagents.worktreeTeardownHook`
78
+ with pi-cohort's existing semantics. Never bypass a wrapper with `git worktree
79
+ remove`; keep unmatched homes host-owned.
64
80
  - Directories: resolve `flowGuards.specDirs` with the existing default
65
81
  `doc/specs`, `docs/specs`; derive plan directories with `planDirsFor`.
66
82
  Never read `planDirs` as settings input. Choose the leg's spec directory by the
@@ -76,6 +92,34 @@ Carry every `Worktree ready at <path>` result, home
76
92
  first. On any failure stop naming repo and branch and list already-created legs;
77
93
  leave them preserved, never auto-remove them.
78
94
 
95
+ ## Locate a leg tree
96
+
97
+ Keep the home leg at the session's home worktree. For a companion, read
98
+ `git -C <leg primary> worktree list --porcelain` and select the linked block
99
+ carrying `branch refs/heads/<recorded branch>`; exclude the first (primary) block.
100
+ Locate by branch regardless of a recorded `home:`; never expand `home:` or shared
101
+ NAME into a lookup path or record the returned path in the spec.
102
+
103
+ Validate the selected path: require it to exist, its checkout toplevel to equal
104
+ its realpath, the checkout to differ from the primary, and its canonical
105
+ `--git-common-dir` to equal the primary's. Treat a listed block as retained
106
+ administration, not proof of a live checkout.
107
+
108
+ | Result | Action |
109
+ |---|---|
110
+ | Exactly one matching block, identity passes | Use its `worktree` path for all later admission, plan paths, and provisioning reuse. |
111
+ | No matching block | Stop: `cannot resolve worktree for <repo>: no worktree holds <branch>`. Only continuation provisioning may proceed by creating a tree. |
112
+ | Several matching blocks | Stop: `cannot resolve worktree for <repo>: <n> worktrees hold <branch>`. |
113
+ | Selected path fails identity | Stop: `cannot resolve worktree for <repo>: <path> is not a checkout`. |
114
+
115
+ For cleanup lookup failures, run no destroy command and delete no branch for
116
+ that leg; report the repo, branch and reason without an invented path, then
117
+ continue with remaining legs. Apply this rule to plan writing, finishing,
118
+ draft-PR cleanup and continuation, and resume's absent companion locations.
119
+ Use it also when loop-implementation seed or authorized restore and bare-worktree
120
+ gauntlet-resume capture companion paths without a brief `legs:` location.
121
+ Keep receipt admission of missing delivered trees unchanged.
122
+
79
123
  ## Gather rules
80
124
 
81
125
  Dispatch one scout per leg with its worktree as `cwd`. Collect root `AGENTS.md`
@@ -117,6 +161,9 @@ scope: AC 1, AC 3
117
161
  design: <owned paths, observable outcomes, and clauses binding this leg>
118
162
  ```
119
163
 
164
+ For a no-`home:` variant, apply the creator-specific omissions in
165
+ [Record portable templates](#record-portable-templates) to this template.
166
+
120
167
  ### Scope grammar and validation
121
168
 
122
169
  Write exactly one `scope:` line in each block: `scope: none` or a comma-separated
@@ -24,7 +24,7 @@ git -C <worktree> diff --name-status origin/$DEFAULT...HEAD
24
24
 
25
25
  Prove the shared predicate against `origin/$DEFAULT` before pushing. Stop on command errors. On invalid shape print the offending name-status lines and `origin/$DEFAULT`, retain the amended spec and checkout, and redraw with `Withheld: draft PR - diff against origin/$DEFAULT is not spec-only (<paths>)` replacing 6. Keep implementation approvals available.
26
26
 
27
- Push, then create with explicit head/base and the spec H1 as title. Run gh against the worktree repository:
27
+ Push, then create with explicit head/base and the spec H1 as title. Pass the summary verbatim through the quoted heredoc shown below, including its literal delimiter in narrated commands. Run gh against the worktree repository:
28
28
 
29
29
  ```bash
30
30
  git -C <worktree> push -u origin <branch>
@@ -40,7 +40,28 @@ EOF
40
40
  | Retry 6 after published handoff | Refresh origin's branch head; require `origin/<branch>` == HEAD. On mismatch print both full SHAs and stop. On equality rerun create only, no push, no amend. |
41
41
  | Create succeeds | Print URL before completing brainstorm; then perform ownership-based cleanup below. |
42
42
 
43
- After URL output call `phase_tracker({ action: "complete", phase: "brainstorm" })`. Read finishing's [Step 6](../../finishing-a-development-branch/SKILL.md#step-6-cleanup-workspace), using only its ownership test and wrapper selection: resolve home/destroy, legacy native script, or native remove/prune. Disable every branch-deletion step, including within a destroy wrapper: verify the wrapper preserves the branch before running it; stop cleanup and report when it cannot. Never delete the local branch. Report host-owned or failed removal with checkout path and command output; explain that later `brainstorming #N` hits pr-head's checked-out-at-another-path stop until that checkout is gone. Start no plan or loop, produce no handoff brief, and leave telemetry unchanged.
43
+ After URL output call `phase_tracker({ action: "complete", phase: "brainstorm" })`, then run [Draft-PR cleanup](#draft-pr-cleanup) once for home. Start no plan or loop, produce no handoff brief, and leave telemetry unchanged.
44
+
45
+ ### Draft-PR cleanup
46
+
47
+ Bind `<repo>` and `<branch>` from the recorded block, `<primary>` to that repo's primary, and `<tree>` to the session's home worktree. For a legacy spec without `## Legs`, use the repo and branch just pushed and the recipe's worktree. For companions, locate `<tree>` by [Locate a leg tree](legs.md#locate-a-leg-tree), not the carried Worktree First path; on lookup failure run no destroy or branch deletion, report `<repo>: kept <branch> - <lookup reason>`, and continue.
48
+
49
+ 1. Require `git -C <tree> branch --show-current` to print `<branch>`. Read `git -C <tree> ls-remote --exit-code --heads origin refs/heads/<branch>`, `git -C <tree> rev-parse HEAD`, and `git -C <primary> rev-parse refs/heads/<branch>`. Pin the SHA only on exit 0 with all three SHAs equal. Otherwise keep tree and branch and report the reason below.
50
+ 2. Select ownership and destruction through finishing's [Step 6](../../finishing-a-development-branch/SKILL.md#step-6-cleanup-workspace), using the recorded contract for a spec with Legs and the overrides reread only for a legacy spec. A recorded block with `home:` keeps its pattern ownership test. Without `home:`, run its recorded `destroy:` from the primary on the located tree, with `<name>` set to the folder basename and `<branch>` to the recorded branch; resolve other tokens before execution. Neither `home:` nor `destroy:` means host-owned: keep both. Never fall back to native removal after a refused or failed wrapper. Before running a branch-deleting wrapper, require its source to delete only after the tree is gone and only at the pinned commit or behind a check refusing a tip the remote does not hold; refuse unguarded deletion.
51
+ 3. After destruction, whatever its exit, read `git -C <primary> worktree list --porcelain`, the folder's on-disk presence, and `git -C <primary> rev-parse -q --verify refs/heads/<branch>`. Count the tree removed only when neither listed nor on disk; otherwise keep any remaining branch. Decide from read-back, never the destroy exit alone.
52
+ 4. With the tree removed and the ref still present, compare its tip to the pinned SHA. Keep a moved ref; otherwise run `git -C <primary> update-ref -d refs/heads/<branch> <pinned SHA>`, never `git branch -D`. Read back the ref after deletion to report its actual state. A guarded wrapper that already deleted it needs no second deletion.
53
+ 5. Report from the observed tree and branch state:
54
+
55
+ | State | Report |
56
+ |---|---|
57
+ | Tree removed, branch deleted | `<repo>: removed <path>, deleted <branch>` |
58
+ | Both remain | `<repo>: kept <path> and <branch> - <reason>` |
59
+ | Tree removed, branch remains | `<repo>: removed <path>, branch <branch> kept - <reason>` |
60
+ | Tree remains, branch gone | `<repo>: kept <path>, branch <branch> deleted - <output>` |
61
+
62
+ Use the observed reason: `tree is on <current branch or detached>, not <branch>`, `origin/<branch> is at <sha>, tree is at <sha>`, `<branch> is at <sha>, tree is at <sha>`, `origin/<branch> is missing` (ls-remote exit 2), `remote lookup failed: <output>` (other nonzero exit), `host-owned`, `destroy deletes <branch> unguarded`, or `removal failed: <output>`. For a removed tree with a retained branch, report `<branch> moved to <sha>` or the update-ref output. A nonzero destroy exit with the tree removed still proceeds to step 4; append ` - destroy exited <code>: <output>` to the report.
63
+
64
+ Attempt every leg regardless of another cleanup outcome, companions in declared order then home. For a retained tree outside a recorded `home:`'s resolved path, explain that later `brainstorming #N` stops on a branch checked out at another path; a no-`home:` tree is reused by continuation. Run no cleanup when PR creation fails.
44
65
 
45
66
  ### Coordinated publication passes
46
67
 
@@ -61,23 +82,34 @@ Publish companions in declared order from the preflight observations:
61
82
 
62
83
  Then run home's existing push/create recipe. Home push/create failures keep its failure rows and also list published companions; never roll back. After home create failure freeze the published handoff commit: the create-only retry above does not amend or push any member or rerun publication passes. On success print `pushed: <repo> origin/<branch>` for each companion before the single home URL, then complete brainstorm.
63
84
 
64
- Clean companions in declared order, then home. Apply finishing Step 6's ownership test and wrapper selection to each recorded contract (legacy without Legs keeps its reread). Verify every wrapper preserves its branch before execution; refuse unsafe wrappers without native fallback. Report each host-owned/refused/failed removal with repo, path and command output, leave its branch intact, and attempt all remaining cleanups even after refusal. Keep branches everywhere, including after owned built-in remove/prune. Write no receipt, companion PR, telemetry or brief.
85
+ Run [Draft-PR cleanup](#draft-pr-cleanup) for companions in declared order, then home, retaining each leg's report even after another leg's refusal. Write no receipt, companion PR, telemetry or brief.
65
86
 
66
87
  ### Coordinated marked settlement
67
88
 
68
89
  Use only the N>1 context observed by pr-subject's marked classifier at the original PR head. Preserve home's provisioned tree on every stop. Read the committed Legs in order; reject missing repo/branch/contract, mismatched shared branches or invalid blocks without falling into ordinary sibling discovery.
69
90
 
70
- 1. Settle only recorded companions with [legs.md](legs.md)'s first-found home overrides at `BASE_SHA` (`git show <BASE_SHA>:<overrides path>`), sibling parser and `<home PRIMARY parent>/<NAME>` paths. Match repo identifiers before probing paths; validate declaration, checkout, stored origin identity and nonhome canonical path. Never activate unrecorded siblings or infer membership from existing PRs. On no match stop: `leg <repo> matches no sibling declaration in <overrides path>`.
71
- 2. Preflight every companion before creating or syncing any companion: run `git -C <PATH> fetch origin` in its primary, then `git -C <PATH> ls-remote --exit-code --heads origin refs/heads/<branch>`. Exit 2 means missing: take the missing-remote stop below even when a stale `origin/<branch>` still resolves. Exit 0 proves live existence; require the recorded shared `origin/<branch>`. On any fetch failure or other lookup exit stop naming member and command output, never diagnose absence. Resolve its own settings/overrides home/create with legs.md's preset/default rules. Compare both portable values with the recorded contract. Stop on drift: `contract drift for <repo>: recorded <key>: <recorded value>, resolved <current value>; amend the recorded contract or restore the repo convention, then rerun brainstorming #N`. Never silently choose between them. Resolve a user amendment through the existing home-plus-companion reconciliation path.
72
- 3. Permit fetch/tracking-ref updates during preflight, but no worktree add, local branch/ref update or merge. A missing last declaration/remote prevents provisioning even the first companion. On missing remote say `origin/<branch> is missing in <repo>; on the machine that ran option 6, push <branch> from that companion's checkout, then rerun brainstorming #N`. For recovery, first verify the leg file's commit with `git log -1 --format=%H refs/heads/<branch> -- <spec-dir>/<home spec filename>` in the retained tree or primary without checking out or modifying the primary, then run `git push -u origin <branch>` from that existing branch and rerun. No commit anywhere means an incomplete handle: stop; regenerate nothing. An unrelated branch-tip commit does not prove the file exists.
73
- 4. After every companion passes, invoke using-git-worktrees once each with explicit `ROOT` = primary, shared `BRANCH`, `REMOTE_BRANCH=existing`. Use pr-head's companion mode and race backstop; carry each Step 4 report, including `Base sync recovered: <sha7>` and its proof. Never construct companion PR fields, pre-rework refs, thread queries or `MEMBER_HEADS`. Do not bypass wrapper safety (`-b`/`-B` falls back to native existing-branch creation) or invent a second provisioning path.
91
+ 1. Settle only recorded companions with [legs.md](legs.md)'s first-found home overrides at `BASE_SHA` (`git show <BASE_SHA>:<overrides path>`), sibling parser and `<home PRIMARY parent>/<NAME>` paths. Match repo identifiers before probing paths; validate declaration, checkout and stored origin identity. Canonicalize both PATH and home PRIMARY, compare them explicitly, and stop on equality before any companion provisioning. Never activate unrecorded siblings or infer membership from existing PRs. On no match stop: `leg <repo> matches no sibling declaration in <overrides path>`.
92
+ 2. Preflight every companion before creating or syncing any companion: run `git -C <PATH> fetch origin` in its primary, then `git -C <PATH> ls-remote --exit-code --heads origin refs/heads/<branch>`. Exit 2 means missing: take the missing-remote stop below even when a stale `origin/<branch>` still resolves. Exit 0 proves live existence; require the recorded shared `origin/<branch>`. On any fetch failure or other lookup exit stop naming member and command output, never diagnose absence. Resolve its own settings/overrides with legs.md's preset/default rules. Compare portable values by the table below. Stop on drift: `contract drift for <repo>: recorded <key>: <recorded value>, resolved <current value>; amend the recorded contract or restore the repo convention, then rerun brainstorming #N`. Never silently choose between them. Resolve a user amendment through the existing home-plus-companion reconciliation path.
93
+ 3. Permit fetch/tracking-ref updates during preflight, but no worktree add, local branch/ref update or merge. A missing last declaration/remote prevents provisioning even the first companion. On missing remote use pr-head's [Missing remote recovery](../../using-git-worktrees/reference/pr-head.md#missing-remote-recovery): check the local branch's file-scoped log in the primary before choosing the push or restore ending.
94
+ 4. After every companion passes, invoke using-git-worktrees once each with explicit `ROOT` = primary, shared `BRANCH`, `REMOTE_BRANCH=existing`. Use pr-head's companion mode and race backstop; carry each Step 4 report, including `Base sync recovered: <sha7>` and its proof. Never construct companion PR fields, pre-rework refs, thread queries or `MEMBER_HEADS`. Apply pr-head's [Recorded no-home provisioning](../../using-git-worktrees/reference/pr-head.md#recorded-no-home-provisioning) for a block without `home:`; keep the `-b`/`-B` native fallback only for a block with `home:`. Invent no second provisioning path.
74
95
  5. On unresolved companion base merge stop: `base merge unresolved for <repo> (<files>) - merge origin/<base> by hand in <worktree> and rerun brainstorming #N`. Keep all trees and refs; on every stop print `Preserved created legs:` listing home and every already-provisioned companion, with repo and path. Never remove any on retries. Preserve home's recovered base-sync proof even if a later companion stops; rerun using the same home recovery path.
75
96
 
76
- At gate readiness list every Worktree First path home first, then companions in recorded order. Offer the ordinary AC-dependent 1/2/4/5 choices and optional pinned audit 3 below, and `Withheld: draft PR - #N already exists`. Keep ordinary pinned-audit and approval persistence below; no companion approval record or second council.
97
+ Compare the recorded contract before provisioning:
98
+
99
+ | Recorded field | Current value |
100
+ |---|---|
101
+ | Keyed or built-in `create:` | Step 1a's resolved portable `create:`. |
102
+ | Discovered-script `create:` | Step 1b's native-tool/script discovery in that primary, rendered through [Record portable templates](legs.md#record-portable-templates), not Step 1a's built-in default. |
103
+ | Recorded `home:` | The current creator's `home:`; when none, print the drift stop with `resolved absent`. |
104
+ | No `home:` recorded | Skip home comparison. |
105
+
106
+ For a script-placed leg with an unwanted recorded `home:`, require a user-approved amendment dropping that line from both the home block and companion file through their existing reconciliation path. Rewrite nothing automatically.
107
+
108
+ At gate readiness render each observed Step 4 report in full, home first then companions in recorded order: `Worktree ready at <path>`, `Branch: <branch>`, Base and Base SHA, and the observed `merge:` outcome. Include each returned `create: override skipped (contains -b)` and `Base sync recovered:` line; preserve home's PR and Pre-rework head lines only on home. Inside the rendered gate block, explicitly print `Worktrees: <home repo> <home path>, <companion repo> <companion path>, ...` naming every Worktree First path, home first then each companion in recorded order, never home alone. Render these values, not a promise to include them. Offer the ordinary AC-dependent 1/2/4/5 choices and optional pinned audit 3 below, and `Withheld: draft PR - #N already exists`. Keep ordinary pinned-audit and approval persistence below; no companion approval record or second council.
77
109
 
78
110
  ## Re-gate a marked spec PR
79
111
 
80
- Reuse [pr-head](../../using-git-worktrees/reference/pr-head.md)'s checkout, base merge, and Step 4 report, not its before-gather thread fetch. Preserve its checked-out-at-another-path stop. On observed `merge: unresolved - <files>`, print `base merge unresolved for PR #N (<files>) - merge origin/<base> by hand in <worktree> and rerun brainstorming #N`; preserve checkout, render no gate, write no approval or repair task.
112
+ Reuse [pr-head](../../using-git-worktrees/reference/pr-head.md)'s checkout, base merge, and Step 4 report, not its before-gather thread fetch. Preserve its checked-out-at-another-path stop for recorded `home:` blocks and unmarked subjects; use its no-home provisioning rows for a marked home without `home:`. On observed `merge: unresolved - <files>`, print `base merge unresolved for PR #N (<files>) - merge origin/<base> by hand in <worktree> and rerun brainstorming #N`; preserve checkout, render no gate, write no approval or repair task.
81
113
 
82
114
  For observed N>1 carried Legs, finish [coordinated marked settlement](#coordinated-marked-settlement) after home's Step 4 report and before rendering the gate. For zero/one block keep the home-only route unchanged.
83
115
 
@@ -380,11 +380,11 @@ If confirmed: Cleanup worktree (Step 6), then, if Step 6 removed the worktree, `
380
380
 
381
381
  **Only runs for the Squash-merge and Discard options (3 and 5).** The PR, draft PR, and Keep options always preserve the worktree.
382
382
 
383
- Use recorded `contract:` lines when the spec carries `## Legs`, including one member; apply the recorded cleanup authority from the Step 1 reference. For a legacy spec without `## Legs`, read the gauntlet overrides file now (ladder in Project overrides) and its `## using-git-worktrees` section as the cleanup contract (keys written like `## cleanup` items: `using-git-worktrees/SKILL.md` Step 1a).
383
+ Use recorded `contract:` lines when the spec carries `## Legs`, including one member; apply [recorded cleanup ownership](reference/legs.md#recorded-cleanup-ownership) before the keyed path below. For a legacy spec without `## Legs`, read the gauntlet overrides file now (ladder in Project overrides) and its `## using-git-worktrees` section as the cleanup contract (keys written like `## cleanup` items: `using-git-worktrees/SKILL.md` Step 1a).
384
384
 
385
385
  **Keyed contract** - the section has at least one of `- home:`, `- create:`, `- destroy:`:
386
386
 
387
- 1. Resolve `home:` against `$PRIMARY` (absent -> `$PRIMARY/.worktrees/<name>`; an absolute value is used as-is). `<name>` must appear exactly once, inside one path segment that may carry a literal prefix or suffix; otherwise stop and name the line.
387
+ 1. Resolve `home:` against `$PRIMARY` (absent in legacy overrides only -> `$PRIMARY/.worktrees/<name>`; an absolute value is used as-is). `<name>` must appear exactly once, inside one path segment that may carry a literal prefix or suffix; otherwise stop and name the line.
388
388
  2. Ownership match: canonicalize both sides (`realpath`) and compare `$WORKTREE` to the resolved pattern, treating the `<name>` segment as `<prefix>*<suffix>` where `*` matches the remainder of that one segment. The text `*` matched is `NAME` - the creation identifier (`jacek/fix-x` was created as `jacek-fix-x`; `home: ../acme-mono-<name>` recovers `fix-x` from `acme-mono-fix-x`). A pattern that cannot isolate one segment -> stop and report.
389
389
  3. Match and `destroy:` set -> run it from `$PRIMARY` with `<name>` -> `'NAME'` and `<branch>` -> `'$FEATURE'`, each value one single-quoted shell word (`'...'`, an embedded `'` escaped), any other `<token>` resolved from context or asked once, never executed raw. Non-zero exit -> print its output, keep the branch, end this step; never fall back to `git worktree remove --force`. On success, run no `git worktree remove` or `prune`; continue to `git -C "$PRIMARY" branch -D "$FEATURE"`.
390
390
  4. Match and no `destroy:` -> `git -C "$PRIMARY" worktree remove "$WORKTREE"` then `git -C "$PRIMARY" worktree prune`; after success, continue to `git -C "$PRIMARY" branch -D "$FEATURE"`.
@@ -442,7 +442,7 @@ A host-owned worktree (Step 6: no `home:` match, or the legacy "Otherwise") keep
442
442
 
443
443
  **Cleaning up harness-owned worktrees**
444
444
  - **Problem:** Removing a worktree the harness created causes phantom state
445
- - **Fix:** Only clean up worktrees that match the `home:` pattern of `## using-git-worktrees`, or - on the legacy path - under `.worktrees/`, `~/.worktrees/<project>/`, or paths produced by a project-native worktree script
445
+ - **Fix:** Apply [recorded cleanup ownership](reference/legs.md#recorded-cleanup-ownership) for specs with `## Legs`; otherwise use Step 6's overrides or legacy ownership proof
446
446
 
447
447
  **No confirmation for discard**
448
448
  - **Problem:** Accidentally delete work
@@ -488,8 +488,8 @@ Once the merge (and any deploy) has landed, `/skill:check-delivery <ticket-ref>`
488
488
  - Get typed confirmation for Option 5 (Discard)
489
489
  - Clean up worktree for Options 3 & 5 (Squash-merge, Discard) only
490
490
  - Target `$PRIMARY` with `git -C` for merge, worktree removal, and branch deletion
491
- - Run `git -C "$PRIMARY" worktree prune` after removal
492
- - Match `$WORKTREE` against the `home:` pattern of `## using-git-worktrees` before removing anything when the keyed contract is active
491
+ - Run `git -C "$PRIMARY" worktree prune` only after owned built-in removal
492
+ - Match `$WORKTREE` against `home:` for recorded blocks that have it; apply Step 6's default only to legacy overrides
493
493
  - Surface the closure / conformance verdict as its own section before the options menu
494
494
 
495
495
  ## Project overrides
@@ -2,12 +2,22 @@
2
2
 
3
3
  ## Activation and authority
4
4
 
5
- Read the home spec's `## Legs` once at Step 1. Use the membership, portable identifiers, scope validation and path resolution in [the leg grammar](../../brainstorming/reference/legs.md). Resolve selected primaries from home's declared siblings and worktrees from the brief or recorded `home:` pattern. Validate branch, repository identity and each companion's full `**Home:**` tuple before acting. Admit a missing delivered worktree only through a validated receipt below.
5
+ Read the home spec's `## Legs` once at Step 1. Use the membership, portable identifiers, scope validation and [Locate a leg tree](../../brainstorming/reference/legs.md#locate-a-leg-tree) rules in the leg grammar. Resolve selected primaries from home's declared siblings; keep the session's home worktree and locate companions by recorded branch, regardless of a brief path or `home:`. Validate branch, repository identity and each companion's full `**Home:**` tuple before acting. Admit a missing delivered worktree only through a validated receipt below.
6
6
 
7
7
  Use recorded `contract:` lines for every spec with `## Legs`, including one member. Amend the home block and affected companion file before using a changed contract; do not reread overrides to replace it. For one member, return to the ordinary finish steps using that block's contract: keep exactly one `### Leg:`, no companion artifact, `legs:` brief block, receipt, `--leg` argument or companion loop. For no `## Legs`, use the ordinary home-only flow and Step 6's overrides reread.
8
8
 
9
9
  For more than one member, apply the following steps instead of the home-only verification, delivery and cleanup mechanics. Keep Step 3.5's disposition protocol, the home's review policy, Step 4 menu and confirmations. Keep machine-local paths out of committed bodies and messages.
10
10
 
11
+ ## Recorded cleanup ownership
12
+
13
+ Apply this table to every recorded block, including a one-member spec, before Step 6's keyed path. Keep legacy specs without `## Legs` on the finish-time overrides reread. Use the recorded cleanup worktree command in preference to `destroy:` and retain the configured teardown hook's existing semantics; never replace recorded authority from current overrides.
14
+
15
+ | Block | Cleanup |
16
+ |---|---|
17
+ | Has `home:` | Apply Step 6's existing ownership pattern match and recovered `<name>`, then `destroy:` or owned built-in removal; preserve unmatched host-owned trees and branches. |
18
+ | No `home:`, has `destroy:` | Run the recorded command from the leg's primary against the located tree: set `<name>` to its folder basename and `<branch>` to the recorded branch, each as one single-quoted shell word using Step 6's escaping; resolve other tokens from context or ask once. After the command, whatever its exit, read `git -C <primary> worktree list --porcelain`, test the folder's existence, and read `git -C <primary> rev-parse -q --verify refs/heads/<branch>`. Treat the tree as removed only when neither listed nor on disk. After confirmed removal, delete a remaining branch through finish's existing `git -C <primary> branch -D <branch>` step; otherwise keep it. Report the observed tree/ref state and any command failure, not success from exit alone. Run no built-in remove or prune fallback on any outcome. Draft-PR cleanup uses its own gated branch deletion instead. |
19
+ | Neither | Keep the host-owned tree and branch; run no cleanup command and report ownership. |
20
+
11
21
  ## Durable receipts
12
22
 
13
23
  Keep one home-owned file per delivered leg at `<home git common dir>/gauntlet/receipts/<spec filename>/<repo>`, where `<home git common dir>` is `git -C <home worktree> rev-parse --path-format=absolute --git-common-dir` (the home primary's `.git`, e.g. `/work/repo-a/.git/gauntlet/receipts/shared.md/repo-b`) - never inside any worktree, so a removed home tree never takes a receipt with it. Include the full spec filename, including `.md`, and preserve repo path segments; create directories before writing. Git metadata is never tracked, so no exclude entry is needed. Treat them as flow restoration artifacts, not telemetry, approval evidence or proof without validation.
@@ -67,10 +77,10 @@ A squash with failed post-verification belongs under Not shipped with its create
67
77
 
68
78
  ## Discard and recorded cleanup - Steps 5 and 6
69
79
 
70
- List every member's branch, commit count, and worktree path as a resolved absolute path (the brief's or the recorded `home:` pattern expanded for this branch, e.g. `/work/repo-b/.worktrees/feature-shared-change`; never a pattern or placeholder) before one typed `discard` confirmation. Obtain exact confirmation once for the run, then clean every member, home included, through its recorded contract regardless of other members' refusals. Do not strip or seal for discard.
80
+ List every member's branch, commit count, and located worktree path as a resolved absolute path (e.g. `/work/repo-b/trees/fix-x`; never a pattern or placeholder), or the lookup failure reason in place of a path, before one typed `discard` confirmation. Obtain exact confirmation once for the run, then clean every member, home included, through its recorded contract regardless of other members' refusals. Do not strip or seal for discard.
71
81
 
72
- For cleanup, use the recorded `## cleanup` worktree command, else `destroy:`, with the configured teardown hook using pi-cohort's existing semantics, else the owned built-in matched against that leg's recorded `home:`. Execute from that leg's primary. Expand recorded `<name>` and `<branch>` as single shell words using Step 6's escaping and ownership proof; resolve other tokens before execution. Never use `git worktree remove` past a wrapper or hook failure. Leave unmatched host-owned trees and their branches preserved.
82
+ For cleanup, apply [Recorded cleanup ownership](#recorded-cleanup-ownership) from each leg's primary. Retain its read-back rule even after a wrapper failure; a hook refusal leaves the wrapper unrun. Never bypass a wrapper or hook with `git worktree remove`.
73
83
 
74
- Clean companions in declared order, then home. Report a refusal as that leg's `failed` line, leave the tree intact, and continue the remaining cleanup attempts and the remaining report lines. After squash, stop only the refused companion's cleanup and attempt every remaining companion's recorded contract from its own primary; on any companion cleanup failure, skip home's cleanup and report `<home repo>: preserved - companion cleanup failed` to retain the home checkout for a fresh-session retry via /skill:gauntlet-resume. Delete a branch only after successful removal of its worktree. Run prune only for successful owned built-in removal, not after a wrapper. Report each repo's actual removed, preserved or failed state.
84
+ Clean companions in declared order, then home. Report a refusal as that leg's `failed` line with its observed tree/ref state, preserve anything remaining, and continue the remaining cleanup attempts and report lines. After squash, stop only the refused companion's cleanup and attempt every remaining companion's recorded contract from its own primary; on any companion cleanup failure, skip home's cleanup and report `<home repo>: preserved - companion cleanup failed` to retain the home checkout for a fresh-session retry via /skill:gauntlet-resume. Delete a branch only after successful removal of its worktree. Run prune only for successful owned built-in removal, not after a wrapper. Report each repo's actual removed, preserved or failed state.
75
85
 
76
86
  Remove receipts only after home delivery and all required cleanup succeed, and report that removal in one explicit line (`Home delivery and required cleanup succeeded; receipts removed`). Receipts live in the home primary's git dir, so worktree removal never touches them; when any required cleanup failed, report `receipts retained - required cleanup failed` and leave every receipt in place for the retry. PR/draft success has no cleanup requirement and preserves every worktree, so its receipts are removed right after the home PR lands. Keep retains receipts for later validation. Mark ship complete only after the chosen action succeeds for every member; never mark discard as shipped.
@@ -106,7 +106,7 @@ Keep fields in this order. Use the portable repo identifier from `../../brainsto
106
106
 
107
107
  Run resume entry check 3 against the run home unchanged. Resolve the home spec through Candidates, retaining human approval/confirmation requirements; artifact presence supplies no approval. Before any tracker write, compare ordered brief membership with the home spec's `### Leg:` blocks exactly; name the missing, extra or reordered leg and both lists on mismatch. Use `../../brainstorming/reference/legs.md` for membership, companion-file and scope grammar; keep no duplicate grammar here.
108
108
 
109
- For each companion, parse the home overrides' selected sibling clone URL with shape-ticket's shared parser and use `<home PRIMARY parent>/<NAME>` as its declared primary. Canonicalize that primary's git common directory. Resolve an absent location from the leg's recorded `home:` contract and shared NAME (shared branch with `/` replaced by `-`), not a newly inferred wrapper. Render that derivation in the stop: name the recorded `home:` pattern and the shared NAME it was resolved from, and state that the pattern locates the tree without authorizing its recreation, a synthesized receipt or inferred approval. Require the companion worktree's canonical git common directory to equal the declared primary's, its live branch to equal the shared branch, and its leg file's full `**Home:**` tuple to equal the run home's repo, shared branch and repo-relative spec path. Stop naming the offending leg and expected/observed path, branch or full tuple. A live tree's branch or tuple mismatch is not informational drift.
109
+ For each companion, parse the home overrides' selected sibling clone URL with shape-ticket's shared parser and use `<home PRIMARY parent>/<NAME>` as its declared primary. Canonicalize that primary's git common directory. For a brief's `worktree: missing`, apply [Locate a leg tree](../../brainstorming/reference/legs.md#locate-a-leg-tree) using the declared primary and recorded branch. Use a found tree as the companion worktree for the remaining admission checks. On lookup failure, report that rule's repo, branch and reason; state that an empty lookup authorizes no recreation, receipt or approval, retaining live receipt admission below for a missing delivered tree. Keep an explicit brief `worktree:` path as recorded and validate it without replacing it from the lookup. Require the companion worktree's canonical git common directory to equal the declared primary's, its live branch to equal the shared branch, and its leg file's full `**Home:**` tuple to equal the run home's repo, shared branch and repo-relative spec path. Stop naming the offending leg and expected/observed path, branch or full tuple. A live tree's branch or tuple mismatch is not informational drift.
110
110
 
111
111
  Admit a missing companion tree only after live receipt validation under `../../finishing-a-development-branch/reference/legs.md`. A copied receipt alone proves nothing. Announce per-leg HEAD and dirty drift against live trees without stopping; identify each leg. Keep approval, standing-grant validation, arming order and task snapshots unchanged. Dispatch task `Leg:` work in that leg's checkout, not the home by default.
112
112
 
@@ -6,18 +6,44 @@ For `$ROOT`, `$BRANCH`, `REMOTE_BRANCH=existing`, follow companion mode below be
6
6
 
7
7
  ### Companion identity and provisioning
8
8
 
9
- Use caller `$ROOT` unchanged, not the session's git common directory; apply Step 0's submodule guard against that root. Use `$BRANCH` as the branch and the companion's repo identity in every stop, never a PR number. Run `git -C "$ROOT" fetch origin`, then repeat the live check `git -C "$ROOT" ls-remote --exit-code --heads origin refs/heads/<branch>` before any companion writes. On any fetch failure or lookup exit other than 0 or 2 stop naming repo and command output, never diagnose absence. On exit 0 require `origin/<branch>`. On exit 2 stop even when a stale tracking ref still resolves: `origin/<branch> is missing in <repo>; on the machine that ran option 6, push <branch> from that companion's checkout, then rerun brainstorming #N`. On that machine, verify `git log -1 --format=%H refs/heads/<branch> -- <spec-dir>/<home spec filename>` shows the leg-file commit without checking out or modifying the primary, then run `git push -u origin <branch>`. Stop continuation when that commit exists on no machine; do not regenerate it.
9
+ Use caller `$ROOT` unchanged, not the session's git common directory; apply Step 0's submodule guard against that root. Use `$BRANCH` as the branch and the companion's repo identity in every stop, never a PR number. Run `git -C "$ROOT" fetch origin`, then repeat the live check `git -C "$ROOT" ls-remote --exit-code --heads origin refs/heads/<branch>` before any companion writes. On any fetch failure or lookup exit other than 0 or 2 stop naming repo and command output, never diagnose absence. On exit 0 require `origin/<branch>`. On exit 2 stop even when a stale tracking ref still resolves; use [Missing remote recovery](#missing-remote-recovery) before selecting the stop's recovery ending.
10
10
 
11
- Set `ORIGINAL_HEAD=$(git -C "$ROOT" rev-parse origin/<branch>)`. Obtain nonempty `baseRefName` with `gh repo view <owner/name> --json defaultBranchRef -q .defaultBranchRef.name` (positional repo, never `-R`), then `BASE_SHA=$(git -C "$ROOT" rev-parse origin/<baseRefName>)`; retain full SHAs. Resolve home and create through Step 1a's rules from this repo's own overrides and `.pi/settings.json` read by file with existing preset rules, already compared with recorded contract in caller preflight. Derive NAME from BRANCH with `/` replaced by `-`.
11
+ Set `ORIGINAL_HEAD=$(git -C "$ROOT" rev-parse origin/<branch>)`. Obtain nonempty `baseRefName` with `gh repo view <owner/name> --json defaultBranchRef -q .defaultBranchRef.name` (positional repo, never `-R`), then `BASE_SHA=$(git -C "$ROOT" rev-parse origin/<baseRefName>)`; retain full SHAs. Use this repo's own overrides and `.pi/settings.json` read by file with existing preset rules, already compared with the recorded contract in caller preflight. For a recorded `home:`, resolve home/create through Step 1a; without `home:`, select and provision through [Recorded no-home provisioning](#recorded-no-home-provisioning). Derive NAME from BRANCH with `/` replaced by `-`.
12
12
 
13
- Read no `headRefOid`, `MEMBER_HEADS` or `PRE_REWORK_HEAD` on this path. Skip cross-repository/PR-head-unreachable checks, PR reports, pre-rework refs and thread fetching. Keep the checked-out-elsewhere stop from the home table, substituting BRANCH and naming repo. Before the local-branch ancestry stop, apply coordinated base-sync recovery below. Without recovery require `git -C "$ROOT" merge-base --is-ancestor refs/heads/<branch> origin/<branch>` for an existing branch; on exit 1 use the local-branch ancestry stop naming repo and replacing "rework needs the PR head" with "companion needs the published branch head". Surface command errors, not negative proofs.
13
+ Read no `headRefOid`, `MEMBER_HEADS` or `PRE_REWORK_HEAD` on this path. Skip cross-repository/PR-head-unreachable checks, PR reports, pre-rework refs and thread fetching. Keep the checked-out-elsewhere stop from the home table only for a recorded `home:`, substituting BRANCH and naming repo. Before the local-branch ancestry stop, apply coordinated base-sync recovery below. Without recovery require `git -C "$ROOT" merge-base --is-ancestor refs/heads/<branch> origin/<branch>` for an existing branch; on exit 1 use the local-branch ancestry stop naming repo and replacing "rework needs the PR head" with "companion needs the published branch head". Surface command errors, not negative proofs.
14
14
 
15
- Apply the "Provision the exact head" rows and wrapper/token/home safety rules with BRANCH for headRefName and ORIGINAL_HEAD for headRefOid. Keep their order: validated recovery, selected-home dirty-tree ask then ff-only and exact head, absent tree/local ancestor proof then update-ref and add, absent tree/absent branch existing-branch DWIM add. Never supply `-b`/`-B`. Run a compatible existing-branch wrapper only; retain `create: override skipped (contains -b)` otherwise. After creation require HEAD equal ORIGINAL_HEAD or validated recovery tip; on mismatch preserve the tree and print its path and both full SHAs.
15
+ Apply the "Provision the exact head" rows and wrapper/token/home safety rules with BRANCH for headRefName and ORIGINAL_HEAD for headRefOid; for a block without `home:`, replace their tree selection and add commands through the no-home table below, including on validated recovery. Keep their order: validated recovery, selected-home dirty-tree ask then ff-only and exact head, absent tree/local ancestor proof then update-ref and add, absent tree/absent branch existing-branch DWIM add. Never supply `-b`/`-B`. Run a compatible existing-branch wrapper only; retain `create: override skipped (contains -b)` otherwise. After creation require HEAD equal ORIGINAL_HEAD or validated recovery tip; on mismatch preserve the tree and print its path and both literal full SHAs (define any report aliases with all 40 characters).
16
16
 
17
- Run only "Merge the fetched base", with companion WORKTREE and its own BASE_SHA overrides for SCOPED_TEST_COMMANDS. Keep helper scope, staging, contained-delta and orchestrator completion checks. On failure abort and record `merge: unresolved - <files>`; return the unresolved result to the coordinated caller for its by-hand stop, not the home Task 1 continuation. Require `git -C "$WORKTREE" merge-base --is-ancestor <ORIGINAL_HEAD> HEAD` after merge. Report Base SHA and merge outcome, not `PR:` or `Pre-rework head:`. Require Step 4's coordinated read-back (`worktree list --porcelain` path and `branch --show-current` exactly BRANCH) before `Worktree ready at <path>`.
17
+ Run only "Merge the fetched base", with companion WORKTREE and its own BASE_SHA overrides for SCOPED_TEST_COMMANDS. Keep helper scope, staging, contained-delta and orchestrator completion checks. On failure abort and record `merge: unresolved - <files>`; end with the preserved-leg inventory and the caller's by-hand stop, even after a clean abort. Emit no `Worktree ready` or home Task 1 continuation on this path. Require `git -C "$WORKTREE" merge-base --is-ancestor <ORIGINAL_HEAD> HEAD` after merge. Report Base SHA and merge outcome, not `PR:` or `Pre-rework head:`. Require Step 4's coordinated read-back (`worktree list --porcelain` path and `branch --show-current` exactly BRANCH) before `Worktree ready at <path>`.
18
18
 
19
19
  On every companion provisioning failure name repo and branch and print `Preserved created legs:` using Step 4's inventory, including the stopped member's tree after a successful creator. Remove nothing.
20
20
 
21
+ ### Missing remote recovery
22
+
23
+ Print `origin/<branch> is missing in <repo>`. Run `git -C <primary> log -1 --format=%H refs/heads/<branch> -- <spec-dir>/<home spec filename>` on this machine without checking out or modifying the primary. Select the recovery ending from that file-scoped result, not the branch tip or a stale tracking ref:
24
+
25
+ | Observation | Recovery ending |
26
+ |---|---|
27
+ | The file-scoped log prints a commit | Offer `git push -u origin <branch>` from that primary, then rerun `brainstorming #N`. |
28
+ | No local branch, or a local branch without the leg-file commit; the log prints nothing | State `no local branch on this machine holds the leg file; restore origin/<branch> from a clone that still has it and rerun brainstorming #N; nothing is regenerated`. Offer no push from this machine. |
29
+
30
+ Preserve all provisioned trees. Run no creation, local ref update or merge on this stop.
31
+
32
+ ### Recorded no-home provisioning
33
+
34
+ Apply to a companion block without `home:` and to a marked home's block without `home:`. Keep blocks with `home:` and unmarked PR subjects on their existing rows, including the another-path stop and the `create: override skipped (contains -b)` native fallback.
35
+
36
+ Locate by [Locate a leg tree](../../brainstorming/reference/legs.md#locate-a-leg-tree) in ROOT using the recorded branch, including for marked home selection before its worktree exists. Use its linked-block and checkout identity checks; never derive a folder from NAME or default a missing recorded `home:`. Keep NAME as the branch with `/` replaced by `-` for creation tokens, not the located folder basename.
37
+
38
+ | Observed state | Action |
39
+ |---|---|
40
+ | One valid linked tree on the branch | Set WORKTREE to that path; bypass the another-path stop. Run the existing dirty-tree ask, ff-only and exact published-head checks there, or reuse the validated recovery tip unchanged. |
41
+ | Lookup fails other than `no worktree holds <branch>` | Stop naming repo and branch with the lookup reason; create nothing. |
42
+ | No tree, recorded script supports an existing-branch form | Read its source before any write. After the existing source-clean ask and local-branch ancestry proof, update-ref only for an unrecovered local ancestor; preserve a recovered tip. Run the script's existing-branch form from ROOT with shared NAME for `<name>` and the recorded branch for `<branch>`, quoted as single shell words. Never pass `-b`/`-B`; surface nonzero output without a built-in fallback. Locate the created tree by branch and require HEAD equal to the published head or validated recovery tip, then complete normal base sync and Step 4 read-back. |
43
+ | No tree, recorded script cannot check out an existing branch | Stop before any write, including update-ref: `cannot provision <repo>: <script> cannot check out existing branch <branch>`. Create no built-in tree. |
44
+
45
+ Resolve other tokens through the existing wrapper-token rule before execution. Keep built-in existing-branch creation only for a recorded built-in creator, never as a fallback for a script-owned tree.
46
+
21
47
  ### Recover coordinated base sync
22
48
 
23
49
  Apply only for a companion or a caller-observed marked home with N>1 Legs, before ancestry and exact-head stops. Use ORIGINAL_HEAD for companions and home headRefOid for marked home as the published head. Inspect the actual `refs/heads/<branch>` tip, not session HEAD. Keep the active-rebase guard below. For a differing local tip require published-head ancestry, then walk `git -C "$ROOT" log --first-parent --format='%H %P' origin/<branch>..refs/heads/<branch>`. Require a nonempty walk, exactly two parents on every entry, and `git -C "$ROOT" merge-base --is-ancestor <second parent> origin/<baseRefName>` exit 0 for each. Require a clean existing selected worktree. A failed proof takes the existing ancestry/carries-commits-beyond stop naming repo; preserve local commits and all created legs.
@@ -26,14 +52,14 @@ Reuse the validated tip without reset, update-ref or `-b`: keep its selected tre
26
52
 
27
53
  ## Select home and stop before writes
28
54
 
29
- Consume SYNC beside the ordered array of admitted PR values, home first; use home's `headRefName` for checkout and `PRE_REWORK_HEAD=<home headRefOid>`. Carry the remaining entries as MEMBER_HEADS `{ number, url, title, headRefName, headRefOid }`. For a caller passing PR values, follow this reference instead of `using-git-worktrees/SKILL.md` Steps 0 and 1. Derive ROOT from the absolute git common directory as in Step 0, applying its submodule guard. Resolve the worktree home through the existing overrides/native-tool/wrapper contract; default to `$ROOT/.worktrees/<name>`. Set `<name>` to `headRefName` with every `/` replaced by `-`. Never adopt the session's linked tree unless it is the selected home on `headRefName`.
55
+ Consume SYNC beside the ordered array of admitted PR values, home first; use home's `headRefName` for checkout and `PRE_REWORK_HEAD=<home headRefOid>`. Carry the remaining entries as MEMBER_HEADS `{ number, url, title, headRefName, headRefOid }`. For a caller passing PR values, follow this reference instead of `using-git-worktrees/SKILL.md` Steps 0 and 1. Derive ROOT from the absolute git common directory as in Step 0, applying its submodule guard. For a marked home with a recorded block without `home:`, use [Recorded no-home provisioning](#recorded-no-home-provisioning) for selection and every add below. Otherwise resolve the worktree home through the existing overrides/native-tool/wrapper contract; default to `$ROOT/.worktrees/<name>`. Set `<name>` to `headRefName` with every `/` replaced by `-`. Never adopt the session's linked tree unless it is the selected home on `headRefName`.
30
56
 
31
57
  Run `git -C "$ROOT" fetch origin`, then check reachability and remote equality for every admitted PR (home and members) before any worktree write: require each original `headRefOid` object and `origin/<headRefName>` equal to it; use the branch-moved stop below naming that PR. Resolve `BASE_SHA=$(git -C "$ROOT" rev-parse origin/<baseRefName>)` as a full SHA before any conflict helper needs fetched-base overrides. Then check these stops in order before creating or modifying any file in the primary or a worktree, or running `git worktree add`.
32
58
 
33
59
  | Observable case | Exact stop |
34
60
  |---|---|
35
61
  | `isCrossRepository` true | `PR #N's head lives on fork <headRepository.nameWithOwner>; rework needs an in-repo branch.` |
36
- | `headRefName` listed by `git -C "$ROOT" worktree list --porcelain` at another path, including `.worktrees/pr-<N>` | `branch <headRefName> is checked out at <path>; finish or remove that worktree first.` |
62
+ | Recorded `home:` or unmarked subject, and `headRefName` listed by `git -C "$ROOT" worktree list --porcelain` at another path, including `.worktrees/pr-<N>` | `branch <headRefName> is checked out at <path>; finish or remove that worktree first.` |
37
63
  | `git -C "$ROOT" rev-parse --verify -q <headRefOid>^{commit}` fails after fetch, or `origin/<headRefName>` differs from `headRefOid` | `PR #N head <headRefOid7> is not reachable (origin/<headRefName> is at <sha7>); the branch moved.` |
38
64
  | Local branch exists without validated recovery (after recovery below; N=1 without a rework-base ref, with or without a worktree, always takes this check) and `git -C "$ROOT" merge-base --is-ancestor <headRefName> origin/<headRefName>` exits 1 | `local branch <headRefName> is at <sha7>, not an ancestor of origin/<headRefName> (<sha7>); rework needs the PR head.` |
39
65
 
@@ -114,7 +114,7 @@ If you can't list the files, the spec isn't ready: amend or redraw per brainstor
114
114
 
115
115
  ## Leg routing
116
116
 
117
- Read the home spec's `## Legs` and [the leg contract](../brainstorming/reference/legs.md) before recon. Keep the plan and `**Spec:**` header home-owned. Resolve each member's checkout from its declared primary and recorded `home:` with shared `NAME`; admit task ownership through `plan_check`, never from `design:` prose. Follow [plan-contract.md](reference/plan-contract.md#legs-leg-unknown) for `Leg:`, per-leg baselines and path keys. Repair unknown task legs in the plan, not spec membership.
117
+ Read the home spec's `## Legs` and [the leg contract](../brainstorming/reference/legs.md) before recon. Keep the plan and `**Spec:**` header home-owned. Keep home's checkout at the session's home worktree and resolve companions through [Locate a leg tree](../brainstorming/reference/legs.md#locate-a-leg-tree); admit task ownership through `plan_check`, never from `design:` prose. Stop plan writing on a failed lookup and report its reason; create no plan against a guessed checkout. Follow [plan-contract.md](reference/plan-contract.md#legs-leg-unknown) for `Leg:`, per-leg baselines and path keys. Repair unknown task legs in the plan, not spec membership.
118
118
 
119
119
  Re-walk every task's `Files:` using Gather rules in the leg contract. Append extra task `Rules:` and resolve `Rules: root only - paths unresolved`; retain missing-link evidence. Split mixed waves into consecutive single-leg waves without violating dependencies.
120
120
 
@@ -13,7 +13,7 @@ A wave is a maximal set of tasks that have no ordering dependency on each other,
13
13
 
14
14
  Read the home spec's `## Legs` using [the leg contract](../../brainstorming/reference/legs.md). Write `Leg: <repo>` immediately after each task heading, before `**Files:**`; absence means home. Keep the plan and its `**Spec:**` header at home, citing the home spec. Repair a `leg-unknown` task in the plan, never by adding spec membership.
15
15
 
16
- Write one `Baseline <repo>: <sha>` header per member. Match each task's baseline bullets to its own leg's SHA; require equality within a leg, not across legs. Resolve `Modify:`/`Create:`/`Test:`/`Delete:` paths and globs against the admitted leg checkout, using its recorded `home:` with shared `NAME` against its declared primary. Key file overlap and cross-task created anchors on `(leg, path)`.
16
+ Write one `Baseline <repo>: <sha>` header per member. Match each task's baseline bullets to its own leg's SHA; require equality within a leg, not across legs. Resolve `Modify:`/`Create:`/`Test:`/`Delete:` paths and globs against the admitted leg checkout from [Locate a leg tree](../../brainstorming/reference/legs.md#locate-a-leg-tree), keeping home at the session's home worktree. Stop plan writing on a failed lookup and report its reason; create no plan against a guessed checkout. Key file overlap and cross-task created anchors on `(leg, path)`.
17
17
 
18
18
  Split mixed waves into consecutive leg-homogeneous waves, preserving dependencies. Re-walk each task's `Files:` under its leg using the leg contract's Gather rules procedure. Append task `Rules:` for files missing from the spec list; resolve `Rules: root only - paths unresolved`. Read the combined lists before execution and review.
19
19