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 +16 -0
- package/README.md +16 -10
- package/extensions/phase-tracker.test.ts +78 -2
- package/extensions/phase-tracker.ts +11 -4
- package/extensions/telemetry-settlement-contract.test.ts +29 -1
- package/extensions/verify-before-ship.test.ts +47 -22
- package/extensions/verify-before-ship.ts +3 -3
- package/package.json +2 -2
- package/skills/brainstorming/reference/legs.md +57 -10
- package/skills/brainstorming/reference/spec-pr.md +41 -9
- package/skills/finishing-a-development-branch/SKILL.md +5 -5
- package/skills/finishing-a-development-branch/reference/legs.md +14 -4
- package/skills/gatekeep-pr/SKILL.md +39 -28
- package/skills/gatekeep-pr/reference/batch.md +10 -10
- package/skills/gatekeep-pr/reference/cleanup.md +5 -5
- package/skills/gatekeep-pr/reference/findings.md +28 -13
- package/skills/gatekeep-pr/reference/menus.md +103 -74
- package/skills/gatekeep-pr/reference/post-selection-loop.md +31 -17
- package/skills/gatekeep-pr/reference/report.md +33 -101
- package/skills/gatekeep-pr/reference/sync.md +1 -1
- package/skills/gatekeep-pr/verification-brief.md +6 -6
- package/skills/gauntlet-resume/reference/brief-contract.md +1 -1
- package/skills/using-git-worktrees/reference/pr-head.md +33 -7
- package/skills/writing-plans/SKILL.md +1 -1
- package/skills/writing-plans/reference/plan-contract.md +1 -1
- package/skills/gatekeep-pr/reference/decision-menu.md +0 -53
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.
|
|
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
|
|
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
|
|
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
|
|
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 [
|
|
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
|
|
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.
|
|
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 |
|
|
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
|
|
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 = "
|
|
2722
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
1031
|
-
const
|
|
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
|
|
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, /
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
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.
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|