pi-gauntlet 8.4.8 → 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,21 @@
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
+
9
+ ## v8.4.9 - 2026-11-11
10
+
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`.
12
+ - Gatekeep-pr preserves the freshness read for replies on approved, open PRs; a moved head skips the reply and retains its body. Set cleanup sharing checks unresolved `<issue>`, `<pr>` and `<merge-sha>` tokens, so an issue-only command remains PR-specific.
13
+ - Gatekeep-pr set mode retires each merged PR after one post-menu pick and collects its unrun steps in the parent's closing message. A shared cleanup item is recommended only on the last merged PR and runs at most once, even when selected earlier.
14
+ - Gatekeep-pr offers merger actions named in the PR or spec as post steps only when merge-base documentation supplies one command. Missing, ambiguous or incomplete commands stay open with their source sentences; PR command text is never executed as authority.
15
+ - Gatekeep-pr asks only once for an invalid post selection. A repeated invalid pick leaves the saved menu waiting without another question or any action.
16
+ - Gatekeep-pr collects terminal follow-ups in one post menu. The recommended combination names its member rows; replies, coverage, cleanup and individual overrides remain separately selectable, and the run ends after the picked steps finish.
17
+ - pi-cohort peer floor raised to 7.7.4.
18
+
3
19
  ## v8.4.8 - 2026-11-11
4
20
 
5
21
  - Brainstorming opens with Today, The change, and What I checked in everyday words, followed by one question. Sources stay in the draft; the opening span is capped at 220 words except corrected or unchecked claims and intact quoted ticket rows.
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,29 +75,35 @@ 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
 
88
88
  ## Architecture
89
89
 
90
- A `gatekeep-pr` run after your newest submitted, non-dismissed review uses that review's commit as its baseline when it is an ancestor of the assessed head. The reviewer checks your posted items and only the intervening code delta; the report shows `Prior review: <k> of <n> items addressed`. Contract checks still judge the whole head. See the [review task](skills/gatekeep-pr/reference/assessment.md#step-4-review-task).
90
+ A `gatekeep-pr` run after your newest submitted, non-dismissed review uses that review's commit as its baseline when it is an ancestor of the assessed head. The reviewer checks your posted items and only the intervening code delta; the report explains how many items were addressed in plain prose. Contract checks still judge the whole head. See the [review task](skills/gatekeep-pr/reference/assessment.md#step-4-review-task).
91
91
 
92
- `gatekeep-pr` selects one course per run: `assess` posts open blockers as one review (`request-changes` on someone else's PR, `comment` on your own), posts nothing with zero blockers, and performs no repair or sync; `deliver` repairs blockers, syncs, pushes, watches CI, and re-reviews until readiness, an obstacle, or a cap exit. `deliver with nits` stores `deliver-nits` and includes current and later nits. A ready first report skips the course pick and renders the final menu directly. Delivery rebases your own non-bot branch with `--force-with-lease` unless its history contains merge commits; someone else's, bot-authored, and merge-containing heads merge base instead. Final `merge` and `approve` remain separate picks; approval is offered only on someone else's non-draft PR with zero blockers. See the [course loop](skills/gatekeep-pr/reference/post-selection-loop.md) and [menu templates](skills/gatekeep-pr/reference/menus.md). In pi, `/skill:gatekeep-pr 12 15 18 [--rebase [base]]` gates a set of PR numbers or same-repository URLs, with one fresh `pr-gatekeeper` child and one worktree per PR. The parent shows a summary and unchanged per-PR menus in input order, queues every pick until the last menu, then executes the queued work in parallel. The set must fit pi-cohort's `parallel.maxTasks` (default `8`); it is never chunked. With `--rebase`, the parent asks once per set for eligible other-author or unreadable-author heads, excluding no-op heads and heads with merge commits. Multi-PR mode rejects positional issue references and is pi-only. An explicit no-argument invocation on either harness discovers your open PRs in origin, authored by you or directly awaiting your review (not team requests), excludes drafts and the `APPROVED` and `CHANGES_REQUESTED` states, and sorts by additions plus deletions, then PR number, ascending. One match gates directly on the single-PR path; multiple matches enter pi's batch path, while Claude Code stops with the sorted `discovered, smallest first:` list. Every report shows the refreshed viewer `Role:` under `Delivers`, not in the batch summary row. See the [parent-child contract](skills/gatekeep-pr/reference/batch.md).
92
+ `gatekeep-pr` selects one course per run: `assess` posts open blockers as one review (`request-changes` on someone else's PR, `comment` on your own), posts nothing with zero blockers, and performs no repair or sync; `deliver` repairs blockers, syncs, pushes, watches CI, and re-reviews until readiness, an obstacle, or a cap exit. `deliver with nits` stores `deliver-nits` and includes current and later nits. A ready first report skips the course pick and renders the final menu directly. Delivery rebases your own non-bot branch with `--force-with-lease` unless its history contains merge commits; someone else's, bot-authored, and merge-containing heads merge base instead. Final `merge` and `approve` remain separate picks; approval is offered only on someone else's non-draft PR with zero blockers. See the [course loop](skills/gatekeep-pr/reference/post-selection-loop.md) and [three menus](skills/gatekeep-pr/reference/menus.md). In pi, `/skill:gatekeep-pr 12 15 18 [--rebase [base]]` gates a set of PR numbers or same-repository URLs, with one fresh `pr-gatekeeper` child and one worktree per PR. The parent shows a summary and unchanged per-PR menus in input order and queues every pick until the last menu. It runs terminal consequences in roster order from the primary checkout and dispatches nonterminal child work in parallel. The set must fit pi-cohort's `parallel.maxTasks` (default `8`); it is never chunked. With `--rebase`, the parent asks once per set for eligible other-author or unreadable-author heads, excluding no-op heads and heads with merge commits. Multi-PR mode rejects positional issue references and is pi-only. An explicit no-argument invocation on either harness discovers your open PRs in origin, authored by you or directly awaiting your review (not team requests), excludes drafts and the `APPROVED` and `CHANGES_REQUESTED` states, and sorts by additions plus deletions, then PR number, ascending. One match gates directly on the single-PR path; multiple matches enter pi's batch path, while Claude Code stops with the sorted `discovered, smallest first:` list. Every report explains the refreshed viewer's role and the other requested reviewers in plain prose, not in the batch summary row. See the [parent-child contract](skills/gatekeep-pr/reference/batch.md).
93
+
94
+ Gatekeep uses three menus: action selects a course or continues after an obstacle, final offers merge, approval and remaining work, and post collects terminal follow-ups. Row 1 is the judged recommendation, not a stable action number. Contract proposals recommend preserve contract, and an unchanged obstacle no available action can change recommends stop. Otherwise, your own PR defaults to deliver; an author decision favors assess on someone else's PR, a ready repair or pending-only check favors deliver, and readiness favors merge regardless of author. An unchanged assess result recommends stop. The report has three unlabeled parts: a two-sentence change/scope opening, standing and problems with concrete examples, and a separate recommendation immediately above the choices. Detailed row mappings, counters, raw checks and IDs stay under `show evidence`.
95
+
96
+ After merge, one post menu offers drafted replies, ticket and PR coverage, guarded checkout cleanup, PR-named steps and individual cleanup overrides. Row 1 combines runnable steps except duplicate PR coverage when ticket coverage is included; each step also has its own row. A comma-separated reply selects a set, expands combinations and collapses duplicates. The gate runs the set in step order, keeps nested skills' gates intact and ends without another menu, listing every unpicked, skipped or failed step with its command or payload path. Approved and closed PRs use the same menu with eligible steps; an approved/open reply retains its pre-write freshness read. In a set, each PR retires after its pick and shared overrides run at most once.
93
97
 
94
98
  The course, target/scope baseline, repair and sync usage, and poll deadline live in one OS temporary continuation record, whose path appears in the tracker or checklist and `show evidence`. Gatekeep initializes independent repair-round and sync budgets to `2 x closureReview.maxFixRounds` (default `5`, no-tool fallback `2 x 5`). Only source-changing repair waves spend rounds; conflict-only syncs spend the separate sync budget, and description-only work, watches, and re-reviews spend neither. Obstacle `deliver` extends each cap by `2 x` the resolved value without resetting usage; a timeout continuation instead opens one more watch window without extending caps. Only the gate's own push automatically renews the CI window (`timeout minutes`, default `15`). The verify-phase runtime guard still uses `1 x` the resolved value and can block a gatekeep dispatch earlier.
95
99
 
100
+ PR-body or head-spec instructions assigning the merger an after-merge action become post steps. Their commands come only from the merge-base overrides file, `AGENTS.md`, or documents it links directly: exactly one distinct documented command makes the step runnable and part of row 1. PR-supplied command text never supplies execution authority. Missing or ambiguous commands and unresolved tokens leave the action open in the menu and closing message with its source sentence. See [PR-named steps](skills/gatekeep-pr/reference/findings.md#pr-named-steps).
101
+
96
102
  Non-final `stop` removes an eligible tracked-clean, non-primary worktree, created or reused, with no unpushed commits and a matching configured home; otherwise it keeps the tree and reports why. It never deletes the branch, pulls base, or runs cleanup overrides. See the [stop lifecycle](skills/gatekeep-pr/reference/cleanup.md#non-final-stop). Missing or invalid state stops course work without resetting budgets. Terminal exit deletes the record; OS cleanup can remove it earlier, and a new session never discovers or resumes authority automatically. This is a skill protocol, not hard runtime enforcement.
97
103
 
98
104
  pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
99
105
 
100
- - **21 skills** - the workflow logic. Sixteen activate automatically when pi sees the matching kind of task, and the workflow skills chain through their gates: `shape-ticket` (creates one tracker issue or repairs a comma/whitespace list of refs against a Context/Problem/Idea/Acceptance-Criteria template, with a grounded repo audit, an AC integrity check, a cheap council roast, and sequential per-ticket exact-diff write confirmations; model-invocable so other skills and commands can route "file a ticket" to it, the gate still needs your yes), `gatekeep-pr` (model-invocable on an explicit ask that names the PR and the verb; the gates still need your selection - is consent-gated pre-merge verification of a PR against the scope contract at its head - the approved spec's acceptance-criteria dispositions and Design when the PR carries one, else the PR's stated intent; the ticket is cross-checked, never judged here (`check-delivery` owns it) - read-only gathering, verification evidence resolved CI-first (green checks on the exact assessed head count as evidence; a pending commit status that GitHub does not report as blocking the gating actor (`mergeStateStatus` not `BLOCKED`) is not waited for - external-CI commit statuses included - while a running Actions check and an unknown merge state still withhold; a viewer GitHub reports as a ruleset bypass actor merges a `BLOCKED` PR on the ordinary `merge` row with CI evidence still binding and all other merge preconditions unchanged; false or unreadable bypass eligibility keeps `BLOCKED` withheld, and live non-admin bypass behavior remains unverified; the project's verification command runs only as fallback), a rubric-based review, a compact bottom-up report (what the PR delivers, including rows deferred or deviating per spec, PR comments, nits, blockers, then a `mergeable` / `fixable` verdict) and course and terminal menus whose rows derive from the actor's permissions on the PR; nothing mutates (repairs, pushes, reviews, merges, tracker comments) until you pick a course or terminal row, except the opt-in `--rebase [base]` sync that rebases, reviews, and lease-pushes the head before verification, or holds the rebased head locally when a conflict stop's scoped tests fail, so a selected delivery course resolves it before the lease push; after your own merge two opt-in rows post the `AC coverage` block to the ticket or the PR; own-merge, approved, and closed-PR menus offer a recommended `cleanup` row that tears down the gate's worktree and branch and runs the overrides file's `## cleanup` steps - run it with `/skill:gatekeep-pr [<pr>] [--rebase [base]]` (no argument: your open PRs)), `check-delivery` (model-invocable on an explicit ask that names the ticket and the verb; the gates still need your selection - is a post-merge detective control: proves an issue actually shipped (default-branch landing, delivery target, per-AC evidence) before its tracker status advances; it never writes a terminal status - run it with `/skill:check-delivery <ref>` or, in pi, `/skill:check-delivery #12 #15` for fresh parallel verification and one gate and write per ticket in turn), `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `loop-implementation` (the plan-free implement driver that iterates a fresh `loop-implementer` over the spec's acceptance-criteria ledger when approval picks the loop engine), `dispatching-parallel-agents`, `verification-before-completion`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `forge-skill`, `linear` (reads/searches/comments on/manages Linear tickets via the `linearis` CLI; owns all linearis mechanics and the `## Issue tracker` overrides schema; tracker-facing skills route to it). Five more require explicit entry (`disable-model-invocation: true`): `brainstorming` starts the gated workflow on explicit consent - use `/skill:brainstorming` for reliable discovery, or opt in through reachable explicit prose or a human-selected handoff; after spec approval, downstream planning and implementation still chain automatically. `chase-bug` is human-only bug triage: read-only root-cause discovery to an evidenced verdict menu (real bug -> ticket/brainstorm/hotfix/respond; five negative verdicts), then a gated response to the reporter for addressable origins (GitHub issue / tracker ticket) and a rendered verdict summary otherwise - it never fixes during triage; the hotfix row hands off to `skills/chase-bug/hotfix.md` after the menu - run it with `/skill:chase-bug`. `gauntlet-performance` reads the committed run telemetry (current repo by default, `--dir <path>` adds others) through the parse-only `gauntlet-performance` CLI and answers with one example-led recommendation, 3-5 cornerstone numbers, a council assessment when available, and a menu of at most three actions (render a report file, open the recommendation as a ticket via `shape-ticket`, drill into one run); it writes nothing unless you pick render - run it with `/skill:gauntlet-performance [--dir <path>]... [--since <version>]`. `gauntlet-handoff` ends a session whose flow a fresh session will continue: it invokes pi-cohort's `handoff` skill (`--out <path>` or `--key <name>`, default key = the run worktree's branch with `/` flattened to `-`, default file `<tmpdir>/pi-handoff/<key>.md`), then appends the gauntlet process-state section (phase/plan tracker status) per the shared contract `skills/gauntlet-resume/reference/brief-contract.md` - run it with `/skill:gauntlet-handoff [--out <path> | --key <name>]`. `gauntlet-resume` is the only way back into an interrupted flow from a fresh session: it takes a `gauntlet-handoff` brief (a file path, pasted text, or - with no arguments - a pick from the briefs under `<tmpdir>/pi-handoff/`), a bare worktree that already holds a spec, or a spec tracked on main under a configured spec dir (a spec seed: it creates or reuses the worktree through `using-git-worktrees` and pins the spec), restores phase/plan tracker state through the legal arming sequence (`start brainstorm`, `skip` with `resume:` reasons, `plan_check` before implement-or-later), and never infers approval from artifacts - run it with `/skill:gauntlet-resume [<brief-file> | <spec>.md | <worktree-name-or-path>]`.
106
+ - **21 skills** - the workflow logic. Sixteen activate automatically when pi sees the matching kind of task, and the workflow skills chain through their gates: `shape-ticket` (creates one tracker issue or repairs a comma/whitespace list of refs against a Context/Problem/Idea/Acceptance-Criteria template, with a grounded repo audit, an AC integrity check, a cheap council roast, and sequential per-ticket exact-diff write confirmations; model-invocable so other skills and commands can route "file a ticket" to it, the gate still needs your yes), `gatekeep-pr` (model-invocable on an explicit ask that names the PR and the verb; the gates still need your selection - is consent-gated pre-merge verification of a PR against the scope contract at its head - the approved spec's acceptance-criteria dispositions and Design when the PR carries one, else the PR's stated intent; the ticket is cross-checked, never judged here (`check-delivery` owns it) - read-only gathering, verification evidence resolved CI-first (green checks on the exact assessed head count as evidence; a pending commit status that GitHub does not report as blocking the gating actor (`mergeStateStatus` not `BLOCKED`) is not waited for - external-CI commit statuses included - while a running Actions check and an unknown merge state still withhold; a viewer GitHub reports as a ruleset bypass actor merges a `BLOCKED` PR on the ordinary `merge` row with CI evidence still binding and all other merge preconditions unchanged; false or unreadable bypass eligibility keeps `BLOCKED` withheld, and live non-admin bypass behavior remains unverified; the project's verification command runs only as fallback), a rubric-based review, a three-part plain-language report (change and scope, standing and problems, then recommendation) and action, final and post menus whose rows derive from the actor's permissions on the PR; nothing mutates (repairs, pushes, reviews, merges, tracker comments) until you pick a course or terminal row, except the opt-in `--rebase [base]` sync that rebases, reviews, and lease-pushes the head before verification, or holds the rebased head locally when a conflict stop's scoped tests fail, so a selected delivery course resolves it before the lease push; after your own merge one post menu offers drafted replies, ticket and PR copies of `AC coverage`, guarded checkout cleanup and separate `## cleanup` items; row 1 combines the runnable steps except duplicate PR coverage when ticket coverage is included, each step keeps its own row, and the run ends after the selected set finishes; approved and closed-PR states use the same post menu with their eligible steps - run it with `/skill:gatekeep-pr [<pr>] [--rebase [base]]` (no argument: your open PRs)), `check-delivery` (model-invocable on an explicit ask that names the ticket and the verb; the gates still need your selection - is a post-merge detective control: proves an issue actually shipped (default-branch landing, delivery target, per-AC evidence) before its tracker status advances; it never writes a terminal status - run it with `/skill:check-delivery <ref>` or, in pi, `/skill:check-delivery #12 #15` for fresh parallel verification and one gate and write per ticket in turn), `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `loop-implementation` (the plan-free implement driver that iterates a fresh `loop-implementer` over the spec's acceptance-criteria ledger when approval picks the loop engine), `dispatching-parallel-agents`, `verification-before-completion`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `forge-skill`, `linear` (reads/searches/comments on/manages Linear tickets via the `linearis` CLI; owns all linearis mechanics and the `## Issue tracker` overrides schema; tracker-facing skills route to it). Five more require explicit entry (`disable-model-invocation: true`): `brainstorming` starts the gated workflow on explicit consent - use `/skill:brainstorming` for reliable discovery, or opt in through reachable explicit prose or a human-selected handoff; after spec approval, downstream planning and implementation still chain automatically. `chase-bug` is human-only bug triage: read-only root-cause discovery to an evidenced verdict menu (real bug -> ticket/brainstorm/hotfix/respond; five negative verdicts), then a gated response to the reporter for addressable origins (GitHub issue / tracker ticket) and a rendered verdict summary otherwise - it never fixes during triage; the hotfix row hands off to `skills/chase-bug/hotfix.md` after the menu - run it with `/skill:chase-bug`. `gauntlet-performance` reads the committed run telemetry (current repo by default, `--dir <path>` adds others) through the parse-only `gauntlet-performance` CLI and answers with one example-led recommendation, 3-5 cornerstone numbers, a council assessment when available, and a menu of at most three actions (render a report file, open the recommendation as a ticket via `shape-ticket`, drill into one run); it writes nothing unless you pick render - run it with `/skill:gauntlet-performance [--dir <path>]... [--since <version>]`. `gauntlet-handoff` ends a session whose flow a fresh session will continue: it invokes pi-cohort's `handoff` skill (`--out <path>` or `--key <name>`, default key = the run worktree's branch with `/` flattened to `-`, default file `<tmpdir>/pi-handoff/<key>.md`), then appends the gauntlet process-state section (phase/plan tracker status) per the shared contract `skills/gauntlet-resume/reference/brief-contract.md` - run it with `/skill:gauntlet-handoff [--out <path> | --key <name>]`. `gauntlet-resume` is the only way back into an interrupted flow from a fresh session: it takes a `gauntlet-handoff` brief (a file path, pasted text, or - with no arguments - a pick from the briefs under `<tmpdir>/pi-handoff/`), a bare worktree that already holds a spec, or a spec tracked on main under a configured spec dir (a spec seed: it creates or reuses the worktree through `using-git-worktrees` and pins the spec), restores phase/plan tracker state through the legal arming sequence (`start brainstorm`, `skip` with `resume:` reasons, `plan_check` before implement-or-later), and never infers approval from artifacts - run it with `/skill:gauntlet-resume [<brief-file> | <spec>.md | <worktree-name-or-path>]`.
101
107
  - **11 subagent personas** - the specialized child agents the skills dispatch via pi-cohort: `implementer`, `loop-implementer`, `wave-reviewer`, `conformance-reviewer`, `spec-summarizer`, `plan-writer`, `spec-council-member`, `spec-council-synthesizer`, `spec-council-applier`, `pr-gatekeeper`, `delivery-checker`. See [doc/personas.md](./doc/personas.md) for what each one does and why its permissions are scoped the way they are.
102
108
  - **4 runtime extensions** - the enforcement layer. `plan-tracker` and `phase-tracker` are tools skills call to track progress (with a TUI widget); `verify-before-ship` is a hook that warns if you push or open a PR without a passing test run since your last edit (expected when a loop gate set does not match its patterns; the scoped `Verification:` line answers it, not a full local run); a phase-tracker flow guard reminds on implement-phase commits missing spec/code review. Both engines use the tracker: waves manages plan tasks, while loop calls `plan_tracker refresh` to project ledger rows and preserve conformance tasks. Loop implement completes when every row is closed, then verify step 1 runs the ledger's gate set in the owning checkouts. In a brainstorming-entered flow, phase-tracker rejects `implement` or `verify` completion while tracker tasks remain pending or in progress; see [its configuration reference](./doc/configuration.md#phase-tracker). phase-tracker also registers `plan_check`, which verifies a plan against its spec and against the grammar in [skills/writing-plans/reference/plan-contract.md](./skills/writing-plans/reference/plan-contract.md), including that each task's `Tests:` commands are selective and never the full suite, and that every test command carries a passing base-commit result; a pass stamps the plan and spec content hashes for implementation and returns a bounded task manifest; see the [manifest contract and size limit](./doc/configuration.md#phase-tracker). `telemetry` publishes YAML records on the `refs/telemetry/records` ref, outside the product diff: one sealed brainstorming run plus one session leg per settled `shape-ticket`, `gatekeep-pr` or `check-delivery` invocation. In-progress runs live in Git common-dir metadata; `gauntlet-telemetry-seal` publishes before landing without changing checkout HEAD or index. Publication failure warns without blocking ship; plain jj keeps local metadata and reports no Git remote. A fetched shipped record blocks a brainstorm `write` into that spec. See [the record layout, settings and migration guide](./doc/configuration.md#telemetry). See [doc/configuration.md](./doc/configuration.md) for the settings each one reads. `phase_tracker`, `plan_check`, and `plan_tracker` are model-only (not callable from codemode scripts) because their state is rebuilt from transcript tool results, which nested calls do not persist.
103
109
 
@@ -142,7 +148,7 @@ Run by a human against pi-cohort >= 7.1.0, before a pi-gauntlet release claims t
142
148
  ## Requirements
143
149
 
144
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.
145
- - [pi-cohort](https://github.com/jjuraszek/pi-cohort) >= 7.7.3 - required peer package, including binary-safe worktree patches; it also lets a persona with `subagent` in `tools:` start with inherited extensions ([pi-cohort #26](https://github.com/jjuraszek/pi-cohort/issues/26)), which multi-PR `gatekeep-pr` needs. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `forge-skill`, `shape-ticket`, `roasting-the-spec`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call. Gauntlet execution dispatches explicitly use `async: false`; do not enable pi-cohort's incompatible `forceTopLevelAsync` setting. See pi-cohort's [dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md) for that setting's owner and semantics. A wave of N >= 1 tasks dispatches its implementers and first spec reviews as one chain call. `subagent-driven-development` stops before dispatch on unsupported installs; there is no older-runtime fallback.
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.
146
152
 
147
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.
148
154
 
@@ -345,7 +351,7 @@ The gauntlet overrides file is the one place a repo tunes gauntlet's Markdown su
345
351
  | `## using-git-worktrees` `- home: <path pattern>` | using-git-worktrees, finishing-a-development-branch, gatekeep-pr | `<primary>/.worktrees/<name>` | overrides | where worktrees live; `<name>` is the branch (`/` -> `-`) or `pr-<N>`; the ownership test at cleanup |
346
352
  | `## using-git-worktrees` `- create: <command>` | using-git-worktrees, gatekeep-pr | `git worktree add` at `home:` | overrides | creation command run from the primary; `<name>`, `<branch>` substituted, other `<token>`s resolved from context or asked once |
347
353
  | `## using-git-worktrees` `- destroy: <command>` | finishing-a-development-branch, gatekeep-pr | `git worktree remove` + `prune` when the path matches `home:` | overrides | removal command run from the primary; a failure keeps the branch |
348
- | `## cleanup` items `- <label>: <command or /skill:...>` | gatekeep-pr | git teardown and base sync only | extends | ordered post-gate steps with `<issue>`, `<pr>`, `<merge-sha>`; inherits `## PR gate` `timeout minutes:`; missing placeholders or unlanded-change steps are skipped with a reason; `/skill:` items keep their own gates |
354
+ | `## cleanup` items `- <label>: <command or /skill:...>` | gatekeep-pr | git teardown and base sync only | extends | separate post-menu steps with `<issue>`, `<pr>`, `<merge-sha>` selected individually or in the recommended combination; inherits `## PR gate` `timeout minutes:`; missing placeholders or unlanded-change steps are skipped with a reason; `/skill:` items keep their own gates |
349
355
  | `## PR gate` keys (`verification command`, `timeout minutes`, `requires credentials`, `local verification`, `ci checks`, `issue fetch`, `merge policy`) | gatekeep-pr | per-key defaults in the block below | overrides | the gate's verification, evidence, and merge configuration |
350
356
  | `## comms style` | gatekeep-pr | shipped wording rules | extends | tone and format of reports, menus, and external payloads |
351
357
  | `## verification-before-completion` | verification-before-completion; gatekeep-pr (verification command only); chase-bug (hotfix verification) | repo test entrypoint discovery | overrides | the canonical verification target |
@@ -585,7 +591,7 @@ customization lives in three places, never in the wrapper itself:
585
591
  - merge policy: squash | merge-commit | rebase # optional; default squash; chooses gh pr merge flag for final merge only, never sync strategy or approval
586
592
  ```
587
593
 
588
- - **The gauntlet overrides file, `## cleanup` section** - the ordered post-gate steps the `cleanup` menu row runs (contract under Project-specific overrides above).
594
+ - **The gauntlet overrides file, `## cleanup` section** - the ordered follow-up steps selected through the post menu (contract under Project-specific overrides above).
589
595
 
590
596
  An existing `## verification-before-completion` overrides section is an accepted
591
597
  equivalent source for the verification command only; all other PR-gate keys
@@ -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
  }
@@ -1,6 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { readFileSync, readdirSync } from "node:fs";
3
3
  import test from "node:test";
4
+ import { fileURLToPath } from "node:url";
5
+ import { lintGateContract } from "../scripts/gate-contract-lint.mjs";
4
6
  import { settlementItems } from "./lib/telemetry-leg.ts";
5
7
  import { MODELS } from "../eval/lib/models.mjs";
6
8
 
@@ -41,7 +43,7 @@ test("skill owners keep terminal rows after writes and cleanup", () => {
41
43
  const gate = read("skills/gatekeep-pr/reference/post-selection-loop.md");
42
44
  assert.ok(gate.includes("#<N>@<head sha>: <outcome>"));
43
45
  assert.match(gate, /full assessed `headRefOid`/);
44
- assert.match(gate, /cleanup pick is answered/);
46
+ assert.match(gate, /Emit the terminal message only after the selected post set finishes or stop declines it; a report or menu is not settlement/);
45
47
  assert.match(read("skills/gatekeep-pr/reference/batch.md"), /gauntlet: retired #<N> - <reason>/);
46
48
  assert.match(read("skills/gatekeep-pr/reference/cleanup.md"), /### Settlement rows/);
47
49
  const ci = read("scripts/ci.mjs");
@@ -52,6 +54,32 @@ test("skill owners keep terminal rows after writes and cleanup", () => {
52
54
  }
53
55
  });
54
56
 
57
+ test("gatekeep published gate markers and terminal settlements stay compatible", () => {
58
+ const contract = read("doc/gate-contract.md");
59
+ for (const id of ["initial", "obstacle", "contract-exception", "final", "assess-terminal", "own-merge", "approved", "closed-pr", "merge-queued"]) {
60
+ assert.ok(contract.includes(`| gatekeep-pr/${id} | gate | skills/gatekeep-pr/reference/menus.md |`), id);
61
+ }
62
+ assert.deepEqual(lintGateContract(fileURLToPath(new URL("../", import.meta.url))), []);
63
+ const terminals = contract.split("\n")
64
+ .filter((line) => line.startsWith("| gatekeep-pr |"))
65
+ .map((line) => line.split("|")[4].trim());
66
+ assert.deepEqual([...new Set(terminals)].sort(), [
67
+ "gauntlet: done approved", "gauntlet: done merged", "gauntlet: done closed-pr",
68
+ "gauntlet: done final", "gauntlet: done merge-queued", "gauntlet: stuck",
69
+ "gauntlet: done retired", "gauntlet: stuck retired",
70
+ ].sort());
71
+ for (const outcome of ["approved", "merged", "closed-pr", "final", "merge-queued", "stuck - CI failed"]) {
72
+ const marker = outcome.startsWith("stuck -") ? "gauntlet: stuck CI failed" : `gauntlet: done ${outcome}`;
73
+ for (const ticket of [undefined, "#12"]) {
74
+ const row = `#31@${head}: ${outcome}${ticket ? ` (${ticket})` : ""}`;
75
+ assert.deepEqual(settlementItems("gatekeep-pr", `${marker}\n${row}\nCleanup was not picked.`), [
76
+ { kind: "pr", key: `#31@${head}`, outcome, ...(ticket ? { ticket } : {}) },
77
+ ]);
78
+ assert.equal(settlementItems("gatekeep-pr", `gauntlet: gate=gatekeep-pr/final recommended=1\n${row}`), undefined);
79
+ }
80
+ }
81
+ });
82
+
55
83
  test("configuration names the settlement grammar and per-skill tokens", () => {
56
84
  const docs = read("doc/configuration.md");
57
85
  assert.ok(docs.includes("<ref>: <outcome>[ (<ticket ref>)]"));
@@ -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.8",
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.3"
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