pi-gauntlet 4.3.0 → 4.4.0

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,69 @@
1
1
  # Changelog
2
2
 
3
+ ## v4.4.0 - 2026-07-12
4
+
5
+ Add an unconditional parallel context-gather step to `/skill:brainstorming`. Step 3's
6
+ inline exploration (raw whole-file reads polluting the main session, pruned by
7
+ pi-condense right before spec-writing, serialized for no reason) is replaced by a
8
+ recon fan-out whose output persists on disk as a context draft at the spec path,
9
+ fully overwritten by the real spec at spec-writing.
10
+
11
+ - **`brainstorming/gatherer.md` (new):** the gather procedure - one parallel-tasks
12
+ `subagent` call: `scout` always, `context-builder` only when the request carries
13
+ external refs (URL, tracker ID with a fetch path, GitHub issue ref). Outputs land
14
+ in a temp dir; the main loop assembles them into a draft headed by a line-1
15
+ marker (`# CONTEXT DRAFT - NOT A SPEC - fully replaced at spec-writing`) with
16
+ `## Codebase recon` / `## External context` / `## Appended during questionary`
17
+ sections. Builder failure degrades to a noted thin draft, never blocks, never
18
+ surfaces at gather time. Foreground, no user interaction - the first thing the
19
+ operator sees after gather is questionary question one.
20
+ - **`brainstorming/SKILL.md`:** checklist gains the gather step; step 3 rewritten
21
+ around the draft (unconditional `Read` before question one; helper-not-fence -
22
+ load-bearing claims still verified against real code; append bar for citable
23
+ findings); spec-writing gains a 3-step overwrite discipline (re-read draft
24
+ immediately before, `write`-tool full replacement, line-1 marker-gone check
25
+ before any downstream dispatch); slug minted once at gather, rename-with-delete
26
+ at spec-writing; 4 new Red Flags.
27
+ - **`phase-tracker.ts`:** new `substep` action - `phase_tracker({ action: "substep",
28
+ phase, substep })` labels an `in_progress` phase in the widget
29
+ (`brainstorm(gather)`), `null`/omitted clears, any transition drops it. New
30
+ marker commit guard between the branch-op block and the mutation warning: during
31
+ brainstorm, `git commit` (incl. `git -C` / `cd X &&` forms) is blocked while any
32
+ file under `flowGuards.specDirs` still begins with the marker line; skipped
33
+ entirely when `piGauntlet.flowGuards.enforce` is `false`.
34
+ - **`extensions/lib/phase-tracker-helpers.ts` (new):** pure, injected-fs logic for
35
+ the above (commit-form parsing, repo-dir resolution, line-1 marker scan, substep
36
+ transition contract) with 17 `node --test` cases registered in `scripts/ci.mjs`.
37
+ - **`doc/configuration.md`:** documents the `substep` action and the third flow
38
+ guard.
39
+
40
+ ## v4.3.1 - 2026-07-06
41
+
42
+ Fix spec-summary pruning at the brainstorming user-review gate. The gate's
43
+ `spec-summarizer` dispatch now writes to a temp file via `outputMode: "file-only"`
44
+ and the main loop reads it back verbatim, so the ~9KB summary survives pi-condense
45
+ instead of being pruned to a paraphrase before render.
46
+
47
+ - **`brainstorming` gate transport:** dispatch `spec-summarizer` with an absolute
48
+ temp-dir `output:` path + `outputMode: "file-only"`, `Read` it back as the last
49
+ content-producing tool call before the gate, and degrade to a one-line note on a
50
+ stub / truncated / missing file. The temp path is outside the worktree, so it is
51
+ never committed.
52
+ - **`spec-summarizer` persona:** stays `tools: read`; a directive tells it not to
53
+ attempt the injected "write your findings" instruction (the harness persists its
54
+ final text for it), dodging the documented stub/stall failure on weaker models.
55
+ Adds a length-proportionality directive so summaries scale with spec size.
56
+ - **`AGENTS.md`:** `outputMode` added to the call-site-overridable knobs list.
57
+
58
+ Restore the per-task spec gate in `subagent-driven-development` Parallel-Wave mode.
59
+ Wave-mode step 3 reviewed spec compliance inline instead of dispatching the
60
+ `spec-reviewer` agent (sequential mode already dispatched it), silently collapsing
61
+ the gate.
62
+
63
+ - **`subagent-driven-development` wave step 3:** fan out one `spec-reviewer` per
64
+ accepted patch (fresh context, `cwd` worktree, diff-based); pin the two-stage
65
+ line to "dispatched spec-reviewer - not inline"; add a Red Flag.
66
+
3
67
  ## v4.3.0 - 2026-07-06
4
68
 
5
69
  DRY gauntlet settings resolution. All `piGauntlet.*` settings reads now route
package/README.md CHANGED
@@ -6,77 +6,94 @@
6
6
 
7
7
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-donate-yellow?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/jjurasszek)
8
8
 
9
- A workflow library for the [pi coding agent](https://github.com/earendil-works/pi): opinionated skills, ready-to-use subagent personas, and three runtime extensions that turn "run the agent in a loop" into a gated pipeline from idea to merge.
9
+ The gated workflow for the [pi coding agent](https://github.com/earendil-works/pi): brainstorm, plan, implement, verify, ship - each stage a gate the next can't open until it closes.
10
10
 
11
- ## Why
11
+ ## The problem
12
12
 
13
- The agentic loop everyone's excited about - point an agent at a problem, let it iterate until done - is the easy part. A bare loop has nothing to aim at, nothing to stop it shipping the wrong thing, and no check that the output matches the ask. It holds up on narrow tasks and falls apart on everything open-ended.
13
+ Point an agent at a task and let it loop until done - that's the easy 5%. A bare loop has nothing to aim at, nothing to stop it shipping the wrong thing, and no check that the final output matches what you actually asked for. It holds up on a narrow, well-specified task and drifts on anything open-ended: the agent reinterprets the ask as it goes, nobody catches it until review, and by then the diff is large enough that review is theater too.
14
14
 
15
- pi-gauntlet is the scaffolding that makes the loop hold: **brainstorm plan implement verify ship**, each stage a gate the next one can't open until it closes.
15
+ That's not a model problem. Cursor, Claude Code, Codex, Devin all run some version of the same loop, and all of them drift the same way on long tasks - because nothing in the loop confronts the output against the *original* intent.
16
16
 
17
- **A gate here is an automated check, not a signature to collect.** Most are machine-enforced - a multi-model spec critique, an adversarial code review, a conformance check that confronts the finished diff against your original ask, and an extension that blocks a commit or push until verification has actually passed. The agent can't wave itself through, and you're not rubber-stamping each step. Human judgment is spent on the two decisions that need it - *what to build* up front and *how to land it* at the end - while the middle (plan → execution, task → task) runs without pausing for you.
17
+ ## Why pi-gauntlet exists
18
18
 
19
- The spec and docs it produces get committed to the repo, so the next change starts from ground truth, not a blank slate. The loop is the easy 5%; the gated system around it is the whole job - that's what this is.
19
+ pi-gauntlet is the scaffolding that makes the loop hold: **brainstorm plan implement verify ship**. Gates between phases are automated checks, not signatures to collect - a multi-model spec critique, an adversarial code review, and a closing conformance check that confronts the finished diff **and docs** against your **original verbatim prompt**, not the plan that got derived from it. The agent can't wave itself through a gate, and you're not rubber-stamping each step by hand.
20
20
 
21
- Rebuilt for pi with enforced gates, a spec council, conformance review, and parallel execution waves. Inspired by [obra/superpowers](https://github.com/obra/superpowers) (Claude Code), by way of [coctostan/pi-superpowers-plus](https://github.com/coctostan/pi-superpowers-plus); see [Lineage](#lineage).
21
+ Human judgment is spent on the two decisions that need it - *what to build*, up front, and *how to land it*, at the end. The middle runs without pausing for you. The spec and docs get committed to the repo, so the next change starts from ground truth, not a blank slate.
22
22
 
23
- ## The workflow
23
+ ## Part of the pi agent toolkit
24
24
 
25
- pi-gauntlet is **opinionated**: every non-trivial change rides one pipeline, idea to merge. There is no separate "just edit a file and commit" path — the skills gate each other, so the next phase can't open until the current one closes.
25
+ Four independent extensions for the [pi coding agent](https://github.com/earendil-works/pi), each owning one concern of running agents seriously:
26
26
 
27
- ```
28
- brainstorm plan implement verify ship
27
+ - [pi-quiver](https://github.com/jjuraszek/pi-quiver) - capabilities (ground-truth ingestion: fetch, doc conversion, session tools)
28
+ - [pi-cohort](https://github.com/jjuraszek/pi-cohort) - coordination (delegate to focused child agents)
29
+ - [pi-condense](https://github.com/jjuraszek/pi-condense) - context economy (prune context, keep it recoverable)
30
+ - **pi-gauntlet - process (this repo: the gated brainstorm→ship workflow)**
31
+
32
+ pi-gauntlet's only hard dependency is pi-cohort - every gate that dispatches a reviewer or an implementer does it through pi-cohort's `subagent()`. pi-condense is not required, but a long gated run generates a lot of tool output; pruning it as you go is what keeps that run affordable.
33
+
34
+ ## What a run looks like
35
+
36
+ Concretely, one change through the gauntlet:
37
+
38
+ 1. You describe the change. **`brainstorming`** sets up an isolated worktree, explores the codebase, and turns your description into a written spec. A multi-model critique runs on it automatically. **You read and approve the spec - human gate 1.** No implementation code exists yet.
39
+ 2. **`writing-plans`** decomposes the approved spec into atomic, independently-verifiable tasks, grouped into parallel waves where they don't touch the same files.
40
+ 3. **`subagent-driven-development`** executes the plan one task at a time, each in a fresh subagent, behind spec-compliance review then code-quality review. TDD-locked: red, green, refactor.
41
+ 4. **verify**: a whole-diff code review, then the **conformance gate** - a subagent reads the finished code and docs against your *original words* from step 1, not the plan, and reports per-requirement: delivered, partial, missing, drifted, or unauthorized. This gate is machine-blocked from being skipped.
42
+ 5. **`finishing-a-development-branch`**: squash, PR, keep, or discard. **Human gate 2** - the only other decision you make.
43
+
44
+ ```mermaid
45
+ flowchart LR
46
+ R([request]) --> B[brainstorm<br/>+ spec]
47
+ B --> G1{{human gate 1:<br/>approve spec}}
48
+ G1 --> P[plan]
49
+ P --> I[implement<br/>waves + reviews]
50
+ I --> V[verify]
51
+ V --> M{{machine gate:<br/>conformance vs<br/>original words}}
52
+ M --> S[ship]
53
+ S --> G2{{human gate 2:<br/>merge / PR / discard}}
54
+ G2 --> D([done])
29
55
  ```
30
56
 
31
- 1. **`brainstorming`** every change starts here. Sets up an isolated worktree, explores the codebase, and turns the idea into a written spec under `doc/specs/`. A multi-model critique runs automatically before you read it (`roasting-the-spec` when a council is configured, else one fresh `worker`). **Hard gate:** no implementation code is written until you approve the spec.
32
- 2. **`writing-plans`** — derives an implementation plan from the approved spec, decomposed into atomic, independently-verifiable tasks (grouped into parallel waves when they're file- and resource-disjoint). Auto-chains into execution.
33
- 3. **`subagent-driven-development`** — executes the plan one atomic task at a time, each in a **fresh subagent**, behind a **two-stage review**: spec compliance first (`spec-reviewer`), then code quality (`code-reviewer`). The `implementer` persona is TDD-locked (RED→GREEN→REFACTOR). You orchestrate; you never hand-write the code.
34
- 4. **verify** — after the last task: a whole-diff review (`requesting-code-review`), then the `conformance-reviewer` closing-loop gate that confronts the delivered code **and** docs against the *origin* (spec + your verbatim original prompt), not the plan. The phase-tracker **blocks `complete verify`** until a conformance dispatch has run, and on a successful `complete verify` (ship still pending) injects an advisory to invoke `finishing-a-development-branch` immediately without a redundant "ready to finish?" prompt - or to reopen verify if a requirement decision is still open.
35
- 5. **`finishing-a-development-branch`** — squash / PR / keep / discard. This menu is the single human decision gate at the end, mirroring spec approval at the start.
57
+ <!-- TODO GIF: a real gauntlet run end to end -->
36
58
 
37
- Spec, plan, and implementation are all developed in the **same worktree**. The squash commit ships spec and implementation; the plan is ephemeral and is stripped before any landing path.
59
+ Everything between gate 1 and gate 2 - task breakdown, implementation, both review passes - runs without you in the loop. That's the mechanism. What follows is the machinery behind it.
38
60
 
39
- **Supporting skills** slot in as the pipeline needs them: `using-git-worktrees` (isolation, before the spec), `test-driven-development` (inside every implementer), `dispatching-parallel-agents` (wave fan-out), `systematic-debugging` (when something breaks), `receiving-code-review` (when you get feedback), `writing-skills` (authoring more of these).
61
+ ## Architecture
40
62
 
41
- **The gates are enforced, not suggested.** Brainstorming refuses to write code before spec approval; the phase-tracker refuses to close verify before the conformance gate runs; `verify-before-ship` warns on any commit/push/PR without a passing test run since your last edit. Reach for a shortcut and a gate stops you — that is the design, not a side effect.
63
+ pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
42
64
 
43
- ## What you get
65
+ - **13 skills** - the workflow logic. They activate automatically when pi sees the matching kind of task, and each one gates the next: `brainstorming`, `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `dispatching-parallel-agents`, `verification-before-completion`, `systematic-debugging`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `writing-skills`.
66
+ - **7 subagent personas** - the specialized child agents the skills dispatch via pi-cohort: `implementer`, `code-reviewer`, `spec-reviewer`, `conformance-reviewer`, `spec-summarizer`, `spec-council-member`, `spec-council-synthesizer`. See [doc/personas.md](./doc/personas.md) for what each one does and why its permissions are scoped the way they are.
67
+ - **3 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 commit/push without a passing test run since your last edit. See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
44
68
 
45
- **13 skills** that activate automatically when pi sees the right kind of task:
69
+ pi-gauntlet is **opinionated**: every non-trivial change rides this one pipeline. There's no separate "just edit a file and commit" path - the skills gate each other, so the phase-tracker extension mechanically blocks a phase from closing before its gate runs. Reach for a shortcut and a gate stops you; that's the design, not friction.
46
70
 
47
- - **Design & planning** — `brainstorming`, `writing-plans`, `roasting-the-spec`
48
- - **Implementation** — `test-driven-development`, `subagent-driven-development`, `dispatching-parallel-agents`
49
- - **Verification** — `verification-before-completion`, `systematic-debugging`
50
- - **Review** — `requesting-code-review`, `receiving-code-review`
51
- - **Worktree lifecycle** — `using-git-worktrees`, `finishing-a-development-branch`
52
- - **Meta** — `writing-skills`
71
+ ## Key concepts
53
72
 
54
- **7 subagent personas** dispatchable via [pi-cohort](https://github.com/jjuraszek/pi-cohort):
73
+ | Term | Meaning |
74
+ | --- | --- |
75
+ | Gate | A machine-enforced checkpoint between phases (e.g. `complete verify` is blocked until conformance review has run). Not a suggestion. |
76
+ | Spec council | Multi-model critique of the spec before you see it (`roasting-the-spec`); falls back to a single-model critique if no council is configured. |
77
+ | Conformance gate | The closing check: does the delivered code + docs match your *original prompt*, not the derived plan? Per-requirement verdict, no auto-fix. |
78
+ | Wave | A batch of plan tasks that don't touch the same files, dispatched to implementers in parallel. |
79
+ | Overrides file | `.pi/gauntlet-overrides.md` - where you put project-specific detail the generic skills don't know (CI command, worktree wrapper, routing rules). |
55
80
 
56
- - `implementer` strict RED→GREEN→REFACTOR TDD, completion-guarded.
57
- - `code-reviewer` — read-only review, Critical/Moderate/Minor severity.
58
- - `spec-reviewer` — verifies an implementation against its plan/spec, per-requirement table.
59
- - `conformance-reviewer` — closing-loop intent gate; confronts the delivered code+docs against the *origin* (spec + verbatim prompt), skipping the plan, and emits a per-requirement coverage verdict. Read-only; proposes remediation, never fixes or decides. Ships model-free — pin its model per preset (see [Conformance gate](#conformance-gate-model)).
60
- - `spec-summarizer` - produces a tight, spec-only human summary for the brainstorming user review gate. Fresh context, read-only (`tools: read`), reads only the spec it is given; output is ephemeral (rendered at the gate, never committed). Dispatched only by `brainstorming`; not for direct dispatch. Ships model-free - set `subagents.agentOverrides.spec-summarizer.model` per preset to override (unset -> inherits the main loop).
61
- - `spec-council-member` — adversarial single-model spec critic; one per configured council model. Dispatched only by `roasting-the-spec`.
62
- - `spec-council-synthesizer` — neutral chair that consolidates and adjudicates member critiques. Dispatched only by `roasting-the-spec`.
81
+ ## When to use / when NOT to use
63
82
 
64
- **3 runtime extensions**:
83
+ **Use it** for any change with more than one moving part: a feature, a refactor across files, anything where "what did we actually agree to build" matters by the time it's done.
65
84
 
66
- - `plan-tracker` persistent task list with a TUI widget. Use the `plan_tracker` tool from skills.
67
- - `phase-tracker` — tracks workflow phase (brainstorm → plan → implement → verify → ship) with a TUI widget. Use the `phase_tracker` tool from skills. Distinct from `plan-tracker` which tracks per-task progress within the implement phase.
68
- - `verify-before-ship` — advisory warning if you run `git commit` / `git push` / `gh pr create` without passing tests since your last source edit.
85
+ **Don't use it** for a one-line fix, a typo, or a throwaway spike you're going to discard. The gates have real overhead - a spec, a plan, a conformance check - and that overhead isn't worth paying for a change trivial enough to just make.
69
86
 
70
87
  ## Requirements
71
88
 
72
89
  - [pi-coding-agent](https://github.com/earendil-works/pi) ≥ 0.1.0
73
- - [pi-cohort](https://github.com/jjuraszek/pi-cohort) ≥ 1.4.5 required peer package. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `writing-skills`) call `subagent({})`, which is provided by pi-cohort. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call.
90
+ - [pi-cohort](https://github.com/jjuraszek/pi-cohort) ≥ 1.4.5 - required peer package. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `writing-skills`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call.
74
91
 
75
- 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.
92
+ 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.
76
93
 
77
94
  ## Install
78
95
 
79
- **Project scope** (recommended committable via the repo's `.pi/settings.json`; `-l` writes to project settings):
96
+ **Project scope** (recommended - committable via the repo's `.pi/settings.json`; `-l` writes to project settings):
80
97
 
81
98
  ```bash
82
99
  pi install -l npm:pi-cohort
@@ -90,45 +107,19 @@ pi install npm:pi-cohort
90
107
  pi install npm:pi-gauntlet
91
108
  ```
92
109
 
93
- Pin an exact release with `npm:pi-gauntlet@X.Y.Z`. Pi clones the package, runs `npm install --omit=dev`, which triggers the `postinstall` script. Where personas land depends on the install location:
94
-
95
- - **User install** (package under `<home>/.pi/<profile>/...`): symlinks the seven agent files into `getAgentDir()/agents` — i.e. `$PI_CODING_AGENT_DIR/agents`, defaulting to `~/.pi/agent/agents`. This is pi-cohort's profile-scoped user dir, so each pi profile (`agent`, `agent.anthropic`, …) gets its own personas instead of sharing the machine-global `~/.agents/`. Earlier releases installed into the machine-global `~/.agents/`; on upgrade the postinstall removes stale `~/.agents/<name>.md` symlinks that point into a pi-gauntlet package (which would otherwise shadow the profile-scoped copy) and leaves your own files there alone.
96
- - **Project install** (package under `<repo>/.pi/...`): copies the seven agent files into `<repo>/.pi/agents/` (the project-scope discovery path). Copy, not symlink, so the files stay valid if you commit them; gitignore `.pi/agents/` if you'd rather keep them install-managed. Project scope wins over user scope on name collisions, so each repo's personas are independent of the user dir and of other repos.
97
-
98
- ## Upgrading from v3.x
110
+ Pin an exact release with `npm:pi-gauntlet@X.Y.Z`. See [doc/install-internals.md](./doc/install-internals.md) for what the postinstall step actually does (symlink vs copy, `PI_GAUNTLET_AGENT_DIR`, upgrading from the pre-rename package).
99
111
 
100
- v4.0.0 is the first public release and is a **breaking rename** (no behavior change). If you ran the package under its old identity:
101
-
102
- - Reinstall under the new name: `pi install -l npm:pi-gauntlet` (was `@jjuraszek/pi-superpowers`).
103
- - Rename settings namespace `piSuperpowers.*` -> `piGauntlet.*` in every preset's `settings.json` (a preset still on the old key silently gets defaults).
104
- - Rename your override file `.pi/superpowers-overrides.md` -> `.pi/gauntlet-overrides.md`.
105
- - Rename the env override `PI_SUPERPOWERS_AGENT_DIR` -> `PI_GAUNTLET_AGENT_DIR` if you set it.
106
-
107
- See [CHANGELOG.md](CHANGELOG.md) for the full v4.0.0 entry.
108
-
109
- ## Install (local development)
112
+ For local development against a checkout instead of npm:
110
113
 
111
114
  ```bash
112
115
  git clone git@github.com:jjuraszek/pi-gauntlet.git ~/repos/pi-gauntlet
113
- cd ~/path/to/your/repo
114
- pi install -l ~/repos/pi-gauntlet
115
- # Local-path installs skip `npm install`; run the symlink step manually:
116
- cd ~/repos/pi-gauntlet && npm run link-agents
116
+ cd ~/path/to/your/repo && pi install -l ~/repos/pi-gauntlet
117
+ cd ~/repos/pi-gauntlet && npm run link-agents # local-path installs skip npm install; run this once
117
118
  ```
118
119
 
119
- After that, edits in `~/repos/pi-gauntlet/` are picked up on next pi launch.
120
-
121
120
  ## Project-specific overrides
122
121
 
123
- The skills shipped here are generic on purpose they describe *how* to TDD, brainstorm, debug, request review, etc., without naming your services, your CI command, or your worktree wrapper. When you need that level of detail, drop a file at:
124
-
125
- ```
126
- .pi/gauntlet-overrides.md
127
- ```
128
-
129
- …in your repo. The skills read it at runtime and merge sections that match the skill's name or topic.
130
-
131
- ### Example `.pi/gauntlet-overrides.md`
122
+ The skills shipped here are generic on purpose - they describe *how* to TDD, brainstorm, debug, request review, etc., without naming your services, your CI command, or your worktree wrapper. When you need that level of detail, drop a file at `.pi/gauntlet-overrides.md` in your repo. The skills read it at runtime and merge sections that match the skill's name or topic:
132
123
 
133
124
  ```markdown
134
125
  ## verification-before-completion
@@ -140,152 +131,21 @@ the gate — it skips integration tests.
140
131
 
141
132
  Use the project's wrapper: `script/worktree create <name>`. It provisions an isolated
142
133
  database and copies `.env.local`. Never call `git worktree add` directly.
143
-
144
- ## brainstorming
145
-
146
- Project routing: dashboard work → `dashboard/AGENTS.md`. Compliance work → `compliance/AGENTS.md`.
147
- Spec docs land in `doc/specs/`, plans in `doc/plans/`, both sibling to each other.
148
- ```
149
-
150
- Two notes:
151
-
152
- - The override file is read by the **skill instructions** at runtime — not by the pi runtime itself. So adding a section here doesn't load anything; the skill that's currently active reads the file and pulls in the matching section.
153
- - Section headers should match skill names (`## verification-before-completion`) or skill topics (`## worktrees`, `## routing`). Skills look for both.
154
-
155
- ## Subagent personas
156
-
157
- On a user install the seven personas in `agents/` are symlinked into `getAgentDir()/agents` (profile-scoped user dir — `$PI_CODING_AGENT_DIR/agents`, default `~/.pi/agent/agents`). On a project install they are copied into `<repo>/.pi/agents/` (project scope, isolated per repo). Override precedence is `project > user > builtin`, so a project install always shadows the user personas for that repo, and you can hand-edit or drop your own `.pi/agents/<name>.md` to shadow them further.
158
-
159
- Target dir override: set `PI_GAUNTLET_AGENT_DIR` to force symlinking into a specific dir (leading `~` expanded; always symlink mode).
160
-
161
- ### Thinking budgets
162
-
163
- `implementer`, `code-reviewer`, and `spec-reviewer` ship without `thinking:` in their frontmatter — pi-cohort `agentOverrides` only fill frontmatter-unset fields, so leaving it unset makes the budget a per-preset config knob. Set it in each preset's `settings.json` (use `false` on non-thinking models → provider default):
164
-
165
- ```json
166
- {
167
- "subagents": {
168
- "agentOverrides": {
169
- "implementer": { "thinking": "medium" },
170
- "code-reviewer": { "thinking": "high" },
171
- "spec-reviewer": { "thinking": "medium" }
172
- }
173
- }
174
- }
175
- ```
176
-
177
- Unset → provider default thinking for that model. `conformance-reviewer` and the two `spec-council-*` personas stay frontmatter-pinned at `xhigh` and are not configurable — the gate and the council must run at max budget even when they inherit the session's model.
178
-
179
- ### Conformance gate model
180
-
181
- `conformance-reviewer` ships without a `model:` in its frontmatter — like the spec-council personas, its model is supplied per preset so each profile points the last correctness gate at the strongest reasoning model its providers can reach. The verify-step skills resolve `piGauntlet.closureReview.model` **repo-local first** (a repo's `.pi/settings.json` overrides the preset whole-object - defining `closureReview` there replaces the preset's entire block, so set every leaf you need together) and inject it **call-site** on the conformance dispatch (the same mechanism the spec-council chair uses). Add it to each preset's `settings.json` (or a repo's `.pi/settings.json` to override per repo):
182
-
183
- ```json
184
- {
185
- "piGauntlet": {
186
- "closureReview": { "model": "<provider/model>", "enforce": true, "maxFixRounds": 2 }
187
- }
188
- }
189
134
  ```
190
135
 
191
- Frontmatter pins `thinking: xhigh` and `defaultContext: fresh` (the gate always runs cold, with max reasoning) and `thinking` is not call-site overridable, so the config supplies only `model`. If `closureReview.model` is unset the dispatch omits `model:` and the gate inherits the parent's model; if the configured model is unreachable it retries once inherited.
192
-
193
- When `closureReview.model` **is** set, the phase-tracker match-checks call-site injection: a `subagent` dispatch of `conformance-reviewer` that omits `model:` is **blocked at tool-call time** (before it runs) so the gate can never silently degrade to the parent's builder model, and a dispatch whose `model:` **differs** from the configured value gets a non-blocking **warning** appended to the result (drift is surfaced, not blocked). The documented one-retry fallback still works - pass an explicit model and it runs (with a warning if it differs). Disabling `closureReview.enforce` disables this guard too.
194
-
195
- `closureReview.enforce` (default `true`) controls the phase-tracker gate that
196
- blocks `complete verify` until the conformance-reviewer has run; set `false` to
197
- disable enforcement for a preset.
198
-
199
- `closureReview.maxFixRounds` (default `2`) caps the conformance **remediation loop**: when a `GAPS` verdict's gaps are dispositioned `fix`, the orchestrator dispatches isolated fix waves and re-audits the delta, up to this many rounds before escalating to the user with the per-gap history. Missing or non-integer -> `2`; `< 0` clamps to `0`; `0` disables fix dispatch (the gap menu offers accept / rescope only). Enforced by the protocol prose in `verification-before-completion/reference/conformance-check.md`, not by the phase-tracker extension.
200
-
201
- If you want to know what's in each persona before using it, see [`agents/`](./agents/). The frontmatter (tools, thinking level, context mode) is documented in [`AGENTS.md`](./AGENTS.md#agents).
202
-
203
- ## Spec council
204
-
205
- `/skill:roasting-the-spec` runs a multi-model critique of a spec before the brainstorming user-review gate. It is the **critique half** of brainstorming's self-review: when a council is configured in the active preset's `settings.json`, brainstorming **auto-dispatches** it (no prompt); when none is configured, brainstorming runs a single fresh-`worker` critique instead. Each member runs on a different model (divergent critiques), a neutral chair consolidates and adjudicates, and you approve what gets applied.
206
-
207
- ```json
208
- {
209
- "piGauntlet": {
210
- "specCouncil": {
211
- "members": ["<provider/model>", "<provider/model>", "<provider/model>"],
212
- "chair": "<provider/model>"
213
- }
214
- }
215
- }
216
- ```
217
-
218
- - `members` (required) — roster of `provider/model` strings; council size = array length, one critique per model. Empty or absent → the council never runs; brainstorming falls back to a single fresh-`worker` critique (scope + ambiguity, auto-applied).
219
- - `chair` (optional) — model for the consolidating synthesizer; defaults to the inherited model when omitted.
220
-
221
- Rosters resolve **repo-local first**: a repo's `.pi/settings.json` overrides the preset (whole-object — the first file that defines `specCouncil` wins), otherwise each pi profile (`agent`, `agent.anthropic`, `agent.bedrock`, …) reads its own `settings.json`. List only models the resolving config's providers can reach. The two personas it dispatches — `spec-council-member` and `spec-council-synthesizer` — are model-free; their model is injected per task from this config.
222
-
223
- ## Extensions
224
-
225
- ### `plan-tracker`
136
+ Section headers should match skill names (`## verification-before-completion`) or skill topics (`## worktrees`, `## routing`). The override file is read by the skill instructions at runtime, not by the pi runtime itself, so adding a section only matters once the matching skill is active.
226
137
 
227
- A tool, not a hook. Skills call `plan_tracker({ action: "init" | "update" | "status" | "clear", ... })` to manage a task list; a TUI widget above the editor shows progress (✓/→/○). State branches with the session, no config needed.
138
+ ## Configuring the gates
228
139
 
229
- ### `phase-tracker`
140
+ The conformance gate's model, the spec council's roster, and the phase-tracker's flow guards are all configured per pi preset (or per repo, via `.pi/settings.json`). See [doc/configuration.md](./doc/configuration.md) for every setting, its default, and how repo-local config overrides a preset.
230
141
 
231
- A tool, not a hook. Skills call `phase_tracker({ action: "start" | "complete" | "skip" | "status" | "reset", phase?, reason? })` to track workflow phase progress. A TUI widget shows the five-phase pipeline: `○ brainstorm → ○ plan → ○ implement → ○ verify → ○ ship`. State branches with the session, no config needed. Phases are entered **explicitly** by the phase-owning skills, so outside a gauntlet flow the widget stays dormant. The `brainstorming` skill resets both trackers on entry (new flow, clean slate); `implement` auto-completes from `plan-tracker` once a skill has started it.
142
+ ## Relationship to the other repos
232
143
 
233
- Distinct from `plan-tracker`: `phase-tracker` answers "what stage of the workflow am I in?"; `plan-tracker` answers "which task within the current stage am I on?"
144
+ pi-gauntlet is the process layer: it enforces the workflow, but every reviewer and implementer it dispatches runs through [pi-cohort](https://github.com/jjuraszek/pi-cohort)'s `subagent()` - that's a hard dependency, not an integration you can skip. [pi-condense](https://github.com/jjuraszek/pi-condense) is optional but keeps a long gated run's context (and cost) from growing unbounded across all those dispatches. [pi-quiver](https://github.com/jjuraszek/pi-quiver) is complementary - if a brainstorm or implementation step needs to pull in a real doc or web page, that's what ingests it safely.
234
145
 
235
- **`gauntlet_setting` tool.** `phase-tracker` also registers `gauntlet_setting({ key: "specCouncil" | "closureReview" })`, a gauntlet-internal tool through which skills resolve merged `piGauntlet.*` settings (repo `.pi/settings.json` over the agent preset, via pi's own `SettingsManager`). It returns the resolved value as a JSON block in the tool result — `specCouncil` yields the council-vs-worker verdict, `closureReview` yields the conformance-gate `model`/`enforce`/`maxFixRounds`. It introduces no new settings key. Every `piGauntlet.*` read — the skills via this tool, both extensions directly — routes through one shared helper (`extensions/lib/gauntlet-settings*.ts`); no code reads `pi.settings` by hand.
236
-
237
- **Closure-review gate.** `complete verify` is rejected unless a successful
238
- `conformance-reviewer` dispatch (a `subagent` result whose `results[]` contains
239
- `agent: "conformance-reviewer"` with `exitCode: 0`) has been observed since the
240
- last `reset`. Management calls (`action: "list"` etc.) and async dispatches never
241
- qualify. A user waiver is recorded via `skip` with a reason — there is no `force`
242
- bypass on `complete`. Disable per preset with
243
- `settings.json#piGauntlet.closureReview.enforce: false` (default: enforced).
244
-
245
- **Flow guards.** Two guards, on by default, disabled per
246
- preset with `settings.json#piGauntlet.flowGuards.enforce: false`:
247
-
248
- - **Worktree discipline (blocks).** During `brainstorm`/`plan`/`implement`, an in-place
249
- `git switch` / `git checkout -b`/`-B` is blocked — the bash call does not run.
250
- Active **only when pi was launched in the primary checkout** (not a linked
251
- worktree); `git worktree ...` and plain `git checkout <file>` never trip it.
252
- Override via `piGauntlet.flowGuards.enforce: false`.
253
- - **Spec-phase confinement (advisory).** During `brainstorm`, a `write`/`edit` (or a bash
254
- mutation: `>`/`>>`/`tee`/`sed -i`/`git apply`) outside the spec dir warns that
255
- brainstorming may only touch the spec. Spec dirs come from
256
- `flowGuards.specDirs` (default `["doc/specs"]`). Redirects to scratch paths
257
- (`/tmp`, `/var/folders`, `/dev`) are exempt. Warns once per brainstorm.
258
-
259
- ### `verify-before-ship`
260
-
261
- A hook on `git commit` / `git push` / `gh pr create`. If you haven't run a passing test command since your last source-file write in this session, an advisory warning is injected into the tool result. The warning clears automatically after a passing test run.
262
-
263
- Default test-command regex matches: `make ci`, `make test`, `npm test`, `pnpm test`, `yarn test`, `pytest`, `rspec`, `cargo test`, `go test`.
264
-
265
- Override in `.pi/settings.json`:
266
-
267
- ```json
268
- {
269
- "piGauntlet": {
270
- "verifyBeforeShip": {
271
- "testCommands": ["make ci", "bundle exec rspec"],
272
- "warningReference": "doc/testing.md"
273
- }
274
- }
275
- }
276
- ```
277
-
278
- `testCommands` entries are regex fragments (anchored with `\b` automatically). `warningReference` is a doc path appended to the warning text — useful for pointing engineers at your testing conventions.
279
-
280
- ## Versioning
281
-
282
- Bump explicitly:
283
-
284
- ```bash
285
- pi install -l npm:pi-gauntlet@X.Y.Z
286
- ```
146
+ ## Roadmap
287
147
 
288
- See [`CHANGELOG.md`](./CHANGELOG.md) for what changed in each release. Semver: minor for new skill/agent/extension, major for renames or breaking config changes. pi-gauntlet and its dispatch peer [pi-cohort](https://github.com/jjuraszek/pi-cohort) version independently but ship together whenever dispatch semantics change; pin compatible versions of both.
148
+ Nothing committed beyond what's shipped. Changes land via [CHANGELOG.md](./CHANGELOG.md).
289
149
 
290
150
  ## Lineage
291
151
 
@@ -19,6 +19,7 @@ Your output is judged on whether a busy supervisor can decide from it alone. A t
19
19
 
20
20
  Optimize for "what does a supervisor need to approve **this** spec." The list below is a recommended checklist, not a rigid template:
21
21
 
22
+ - **Scale length to the spec.** The summary is proportional to the spec's size and complexity - a short or simple spec gets a short summary. Do not expand every recommended section to full depth to look thorough; the summary should be a fraction of the spec, not a near-copy of it. This is proportionality, **not** aggressive compression - never drop a decision-relevant point, rejected-alternative, risk, or gap-footer entry to hit a length target. When in doubt, keep the point and cut the words around it.
22
23
  - **Omit any section that is empty.** A bugfix has no new endpoint; a refactor has no algorithm. Write nothing for an empty section - never "N/A" or filler.
23
24
  - **Order decision-layer-first** (problem -> decisions -> scope -> risk), then the descriptive layer, so the reader can stop early once confident.
24
25
  - **Add a section the spec demands** if it carries decision-relevant content none of the below captures.
@@ -39,4 +40,4 @@ Recommended sections:
39
40
 
40
41
  Tight, human-readable, no obvious statements. If the topic is complex, an example with explanation beats prose.
41
42
 
42
- Output the summary as your final text response. Do not write any file.
43
+ Output the summary as your final text response. You have no write tool. If your task instructs you to write your findings to a file path, do **not** attempt to write, create, or edit any file and do **not** treat the inability to write as a failure - just emit the full summary as your final text response. The harness persists that response to the requested path for you.
@@ -0,0 +1,120 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ CONTEXT_DRAFT_MARKER,
5
+ checkSubstep,
6
+ phaseLabel,
7
+ parseGitCommit,
8
+ resolveRepoDir,
9
+ findMarkerFile,
10
+ markerBlockReason,
11
+ transitionPhaseState,
12
+ markerGuardApplies,
13
+ } from "./phase-tracker-helpers.ts";
14
+
15
+ test("checkSubstep: in_progress -> ok", () => {
16
+ assert.deepEqual(checkSubstep("in_progress"), { ok: true });
17
+ });
18
+
19
+ test("checkSubstep: non-in_progress statuses -> error naming actual status", () => {
20
+ for (const s of ["pending", "complete", "skipped"]) {
21
+ const r = checkSubstep(s);
22
+ assert.equal(r.ok, false);
23
+ if (!r.ok) assert.match(r.error, new RegExp(s));
24
+ }
25
+ });
26
+
27
+ test("phaseLabel: with and without substep", () => {
28
+ assert.equal(phaseLabel("brainstorm", "gather"), "brainstorm(gather)");
29
+ assert.equal(phaseLabel("brainstorm", undefined), "brainstorm");
30
+ });
31
+
32
+ test("parseGitCommit: plain, -am, chained after &&", () => {
33
+ assert.ok(parseGitCommit('git commit -m "x"'));
34
+ assert.ok(parseGitCommit("git commit -am 'x'"));
35
+ assert.ok(parseGitCommit('git add -A && git commit -m "x"'));
36
+ });
37
+
38
+ test("parseGitCommit: -C path and cd prefix are captured", () => {
39
+ assert.deepEqual(parseGitCommit('git -C /wt commit -m "x"'), { cPath: "/wt", cdPath: undefined });
40
+ assert.deepEqual(parseGitCommit('cd /wt && git commit -m "x"'), { cPath: undefined, cdPath: "/wt" });
41
+ });
42
+
43
+ test("parseGitCommit: non-commit git and non-git -> undefined", () => {
44
+ assert.equal(parseGitCommit("git log --oneline"), undefined);
45
+ assert.equal(parseGitCommit("git commitish"), undefined);
46
+ assert.equal(parseGitCommit('echo "git commit"'), undefined); // statement-start anchor: a quote is not a statement boundary
47
+ assert.equal(parseGitCommit("npm test"), undefined);
48
+ });
49
+
50
+ test("parseGitCommit: -c config flags between git and commit don't bypass the guard", () => {
51
+ assert.deepEqual(parseGitCommit('git -c user.email=x commit -m "y"'), { cPath: undefined, cdPath: undefined });
52
+ assert.deepEqual(parseGitCommit("git -C /wt -c user.email=x commit"), { cPath: "/wt", cdPath: undefined });
53
+ });
54
+
55
+ test("parseGitCommit: commit-graph / commit-tree are not commit", () => {
56
+ assert.equal(parseGitCommit("git commit-graph write"), undefined);
57
+ assert.equal(parseGitCommit("git commit-tree HEAD^{tree}"), undefined);
58
+ });
59
+
60
+ test("parseGitCommit: cd path in subshell (cd /a) captures /a without trailing paren", () => {
61
+ assert.deepEqual(parseGitCommit("(cd /a) && git commit -m x"), { cPath: undefined, cdPath: "/a" });
62
+ });
63
+
64
+ test("parseGitCommit: last cd before the commit wins over an earlier one", () => {
65
+ assert.deepEqual(parseGitCommit("cd /a && cd /b && git commit -m x"), { cPath: undefined, cdPath: "/b" });
66
+ });
67
+
68
+ test("parseGitCommit: cd recognized before non-&& statement separators", () => {
69
+ assert.deepEqual(parseGitCommit("cd /wt; git commit -m x"), { cPath: undefined, cdPath: "/wt" });
70
+ assert.deepEqual(parseGitCommit("cd /wt\ngit commit -m x"), { cPath: undefined, cdPath: "/wt" });
71
+ assert.deepEqual(parseGitCommit("cd /wt || git commit -m x"), { cPath: undefined, cdPath: "/wt" });
72
+ });
73
+
74
+ test("resolveRepoDir: -C wins over cd, cd wins over session cwd, relative -C resolves against cd", () => {
75
+ assert.equal(resolveRepoDir({ cPath: "/b", cdPath: "/a" }, "/s"), "/b");
76
+ assert.equal(resolveRepoDir({ cPath: undefined, cdPath: "/a" }, "/s"), "/a");
77
+ assert.equal(resolveRepoDir({ cPath: undefined, cdPath: undefined }, "/s"), "/s");
78
+ assert.equal(resolveRepoDir({ cPath: "wt", cdPath: "/a" }, "/s"), "/a/wt");
79
+ });
80
+
81
+ test("findMarkerFile: line-1 hit found, quoted-in-body miss, missing dir -> undefined", () => {
82
+ const files: Record<string, string> = {
83
+ "/r/doc/specs/a.md": CONTEXT_DRAFT_MARKER + "\n\nbody",
84
+ "/r/doc/specs/b.md": "# Real spec\n\n`" + CONTEXT_DRAFT_MARKER + "` quoted in body",
85
+ };
86
+ const listFiles = (dir: string) =>
87
+ dir === "/r/doc/specs" ? Object.keys(files) : [];
88
+ const readFirstLine = (f: string) => files[f]?.split("\n", 1)[0];
89
+ assert.equal(findMarkerFile("/r", ["doc/specs"], listFiles, readFirstLine), "/r/doc/specs/a.md");
90
+ delete files["/r/doc/specs/a.md"];
91
+ assert.equal(
92
+ findMarkerFile("/r", ["doc/specs"], (d) => (d === "/r/doc/specs" ? Object.keys(files) : []), readFirstLine),
93
+ undefined,
94
+ );
95
+ assert.equal(findMarkerFile("/r", ["nope"], () => [], readFirstLine), undefined);
96
+ });
97
+
98
+ test("markerBlockReason names the file and the enforce escape hatch", () => {
99
+ const r = markerBlockReason("/r/doc/specs/a.md");
100
+ assert.match(r, /\/r\/doc\/specs\/a\.md/);
101
+ assert.match(r, /flowGuards\.enforce/);
102
+ });
103
+
104
+ test("transitionPhaseState: complete/skipped/pending drop any prior substep", () => {
105
+ for (const status of ["complete", "skipped", "pending"]) {
106
+ const r = transitionPhaseState(status);
107
+ assert.equal(r.status, status);
108
+ assert.ok(!("substep" in r));
109
+ }
110
+ });
111
+
112
+ test("transitionPhaseState: reason propagates when provided", () => {
113
+ assert.deepEqual(transitionPhaseState("skipped", "why"), { status: "skipped", reason: "why" });
114
+ });
115
+
116
+ test("markerGuardApplies: gated by flowGuards.enforce and brainstorm in_progress", () => {
117
+ assert.equal(markerGuardApplies(false, "in_progress"), false);
118
+ assert.equal(markerGuardApplies(true, "in_progress"), true);
119
+ assert.equal(markerGuardApplies(true, "pending"), false);
120
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Pure logic for phase-tracker's substep action and marker commit guard.
3
+ * No fs/git access here — file access is injected so node --test covers it
4
+ * (registered in scripts/ci.mjs alongside the settings resolvers).
5
+ */
6
+
7
+ import { resolve } from "node:path";
8
+
9
+ export const CONTEXT_DRAFT_MARKER = "# CONTEXT DRAFT - NOT A SPEC - fully replaced at spec-writing";
10
+
11
+ export type SubstepCheck = { ok: true } | { ok: false; error: string };
12
+
13
+ export function checkSubstep(phaseStatus: string): SubstepCheck {
14
+ if (phaseStatus === "in_progress") return { ok: true };
15
+ return { ok: false, error: `substep requires an in_progress phase (status is ${phaseStatus})` };
16
+ }
17
+
18
+ export function phaseLabel(name: string, substep?: string): string {
19
+ return substep ? `${name}(${substep})` : name;
20
+ }
21
+
22
+ // Same statement-start anchor as the branch guards in phase-tracker.ts. Exported
23
+ // so Wave 2 wiring in phase-tracker.ts can drop its duplicate copy.
24
+ export const STMT_START = "(?:^|[\\n;&|(])\\s*";
25
+ // `git commit`, tolerating any global flags between `git` and `commit` (e.g.
26
+ // `-c user.email=x`, common in CI per-commit identity). Group 1 captures the
27
+ // whole flags span as ONE opaque block rather than -C directly: nesting a
28
+ // capture inside a repeated alternation resets it to undefined on iterations
29
+ // that take the other branch (JS regex semantics), so -C after a later flag
30
+ // (e.g. `-C /wt -c user.email=x commit`) would silently lose its capture.
31
+ // DASH_C below re-extracts -C from that span once, outside any repetition.
32
+ // `commit` must be followed by whitespace or end-of-string, not just a word
33
+ // boundary, so `commit-graph` / `commit-tree` don't false-positive.
34
+ const GIT_COMMIT = new RegExp(
35
+ STMT_START + "git\\s+((?:-\\S+(?:\\s+\\S+)?\\s+)*)commit(?=\\s|$)",
36
+ );
37
+ const DASH_C = /(?:^|\s)-C\s+(\S+)/;
38
+ // Global, lookaround-delimited (not consuming) so adjacent `cd a && cd b &&`
39
+ // statements don't eat each other's anchor/`&&` and hide the second match.
40
+ // parseGitCommit picks the LAST cd before the matched git-commit position, so
41
+ // `cd /a && cd /b && git commit` resolves against /b, not the first cd found.
42
+ const LEADING_CD = /(?<=^|[\n;&|(])\s*cd\s+([^\s)]+)\s*(?=&&|\|\||[;)\n]|$)/g;
43
+
44
+ export interface CommitForm {
45
+ cPath: string | undefined;
46
+ cdPath: string | undefined;
47
+ }
48
+
49
+ // Textual match anchored at statement starts (^ ; & | ( or newline). Quoted text
50
+ // can still match when preceded by such a char (e.g. sh -c 'x; git commit') —
51
+ // accepted heuristic, same tolerance as the existing Guard 3 mutation checks.
52
+ export function parseGitCommit(command: string): CommitForm | undefined {
53
+ const m = GIT_COMMIT.exec(command);
54
+ if (!m) return undefined;
55
+ let cdPath: string | undefined;
56
+ for (const cd of command.matchAll(LEADING_CD)) {
57
+ if (cd.index! < m.index!) cdPath = cd[1];
58
+ }
59
+ return { cPath: DASH_C.exec(m[1])?.[1], cdPath };
60
+ }
61
+
62
+ export function resolveRepoDir(form: CommitForm, sessionCwd: string): string {
63
+ const base = form.cdPath ? resolve(sessionCwd, form.cdPath) : sessionCwd;
64
+ return form.cPath ? resolve(base, form.cPath) : base;
65
+ }
66
+
67
+ // Line-1 anchoring prevents false positives on specs that QUOTE the marker.
68
+ // Working-tree read (not index) is an accepted false-negative window for a
69
+ // backstop whose primary check lives in the brainstorming skill.
70
+ export function findMarkerFile(
71
+ repoDir: string,
72
+ specDirs: string[],
73
+ listFiles: (dir: string) => string[],
74
+ readFirstLine: (file: string) => string | undefined,
75
+ ): string | undefined {
76
+ for (const dir of specDirs) {
77
+ for (const file of listFiles(resolve(repoDir, dir))) {
78
+ if (readFirstLine(file) === CONTEXT_DRAFT_MARKER) return file;
79
+ }
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ // Transitions always build fresh state: a substep never survives start/complete/skip/reset.
85
+ export function transitionPhaseState(status: string, reason?: string): { status: string; reason?: string } {
86
+ return reason === undefined ? { status } : { status, reason };
87
+ }
88
+
89
+ export function markerGuardApplies(flowGuardsEnforced: boolean, brainstormStatus: string): boolean {
90
+ return flowGuardsEnforced && brainstormStatus === "in_progress";
91
+ }
92
+
93
+ export const markerBlockReason = (file: string): string =>
94
+ `Blocked: ${file} still begins with the context-draft marker - the spec-writing ` +
95
+ `overwrite has not happened. Overwrite the draft with the real spec (write tool, ` +
96
+ `full replacement) before committing. ` +
97
+ `To override, set piGauntlet.flowGuards.enforce: false.`;
@@ -10,6 +10,8 @@
10
10
  */
11
11
 
12
12
  import { execSync } from "node:child_process";
13
+ import { readdirSync, readFileSync } from "node:fs";
14
+ import { join } from "node:path";
13
15
  import { StringEnum } from "@earendil-works/pi-ai";
14
16
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
15
17
  import { Text } from "@earendil-works/pi-tui";
@@ -21,6 +23,16 @@ import {
21
23
  settingsErrorWarning,
22
24
  } from "./lib/gauntlet-settings.ts";
23
25
  import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
26
+ import {
27
+ checkSubstep,
28
+ findMarkerFile,
29
+ markerBlockReason,
30
+ parseGitCommit,
31
+ phaseLabel,
32
+ resolveRepoDir,
33
+ STMT_START,
34
+ transitionPhaseState,
35
+ } from "./lib/phase-tracker-helpers.ts";
24
36
 
25
37
  const PHASES = ["brainstorm", "plan", "implement", "verify", "ship"] as const;
26
38
  type Phase = (typeof PHASES)[number];
@@ -29,12 +41,13 @@ type PhaseStatus = "pending" | "in_progress" | "complete" | "skipped";
29
41
  interface PhaseState {
30
42
  status: PhaseStatus;
31
43
  reason?: string;
44
+ substep?: string;
32
45
  }
33
46
 
34
47
  type PhaseMap = Record<Phase, PhaseState>;
35
48
 
36
49
  interface PhaseTrackerDetails {
37
- action: "start" | "complete" | "skip" | "status" | "reset";
50
+ action: "start" | "complete" | "skip" | "status" | "reset" | "substep";
38
51
  phases: PhaseMap;
39
52
  error?: string;
40
53
  }
@@ -72,7 +85,6 @@ const GUARD_PHASES: Phase[] = ["brainstorm", "plan", "implement"];
72
85
  // Guard 2 — branch ops in place. `git switch` never targets a file path;
73
86
  // `git checkout -b/-B` is explicit branch creation. Bare `git checkout <x>`
74
87
  // is excluded (ambiguous with file checkout). `git worktree ...` is exempt.
75
- const STMT_START = "(?:^|[\\n;&|(])\\s*";
76
88
  const BRANCH_SWITCH = new RegExp(STMT_START + "git\\s+switch\\b");
77
89
  const BRANCH_CHECKOUT = new RegExp(STMT_START + "git\\s+checkout\\s+-[bB]\\b");
78
90
  const GIT_WORKTREE = /\bgit\s+worktree\b/;
@@ -155,7 +167,7 @@ const pathInSpecDirs = (rawPath: string, specDirs: string[]): boolean => {
155
167
  };
156
168
 
157
169
  const PhaseTrackerParams = Type.Object({
158
- action: StringEnum(["start", "complete", "skip", "status", "reset"] as const, {
170
+ action: StringEnum(["start", "complete", "skip", "status", "reset", "substep"] as const, {
159
171
  description: "Action to perform",
160
172
  }),
161
173
  phase: Type.Optional(
@@ -173,6 +185,11 @@ const PhaseTrackerParams = Type.Object({
173
185
  description: "Reset and re-start a phase that is already complete or skipped (rare; default false)",
174
186
  }),
175
187
  ),
188
+ substep: Type.Optional(
189
+ Type.Union([Type.String(), Type.Null()], {
190
+ description: "Substep label for action=substep on an in_progress phase; null or omitted clears it",
191
+ }),
192
+ ),
176
193
  });
177
194
 
178
195
  export type PhaseTrackerInput = Static<typeof PhaseTrackerParams>;
@@ -201,7 +218,8 @@ function hasActivity(phases: PhaseMap): boolean {
201
218
  function formatWidget(phases: PhaseMap, theme: Theme): string {
202
219
  const parts = PHASES.map((p) => {
203
220
  const icon = phaseIcon(phases[p].status, theme);
204
- const name = phases[p].status === "skipped" ? theme.fg("dim", p) : p;
221
+ const labeled = phaseLabel(p, phases[p].status === "in_progress" ? phases[p].substep : undefined);
222
+ const name = phases[p].status === "skipped" ? theme.fg("dim", labeled) : labeled;
205
223
  return `${icon} ${name}`;
206
224
  });
207
225
  return `${theme.fg("muted", "Phases:")} ${parts.join(theme.fg("dim", " → "))}`;
@@ -213,7 +231,7 @@ function formatStatus(phases: PhaseMap): string {
213
231
  const s = phases[p];
214
232
  const icon = s.status === "complete" ? "✓" : s.status === "in_progress" ? "→" : s.status === "skipped" ? "⊘" : "○";
215
233
  const suffix = s.reason ? ` (${s.reason})` : "";
216
- lines.push(` ${icon} ${p}${suffix}`);
234
+ lines.push(` ${icon} ${phaseLabel(p, s.status === "in_progress" ? s.substep : undefined)}${suffix}`);
217
235
  }
218
236
  return lines.join("\n");
219
237
  }
@@ -386,6 +404,41 @@ export default function (pi: ExtensionAPI) {
386
404
  return { block: true, reason: branchBlockReason(gphase) };
387
405
  }
388
406
 
407
+ // Marker commit guard — the context draft (brainstorming gather step) must be
408
+ // overwritten by the real spec before any commit lands. Backstop to the skill's
409
+ // own post-write check. Blocked, like Guard 2; skipped entirely when
410
+ // flowGuards.enforce is false (checked above).
411
+ // gating contract: markerGuardApplies (see helpers) - enforce checked once above
412
+ // (flowGuardsEnforced()); re-checking it here would be belt-and-suspenders.
413
+ if (phases.brainstorm.status === "in_progress") {
414
+ const commit = parseGitCommit(command);
415
+ if (commit) {
416
+ const repoDir = resolveRepoDir(commit, ctx.cwd);
417
+ const listFiles = (dir: string): string[] => {
418
+ const walk = (d: string): string[] => {
419
+ return readdirSync(d, { withFileTypes: true }).flatMap((e) => {
420
+ const p = join(d, e.name);
421
+ return e.isDirectory() ? walk(p) : e.isFile() ? [p] : [];
422
+ });
423
+ };
424
+ try {
425
+ return walk(dir);
426
+ } catch {
427
+ return [];
428
+ }
429
+ };
430
+ const readFirstLine = (file: string): string | undefined => {
431
+ try {
432
+ return readFileSync(file, "utf8").split("\n", 1)[0];
433
+ } catch {
434
+ return undefined;
435
+ }
436
+ };
437
+ const hit = findMarkerFile(repoDir, specDirs(), listFiles, readFirstLine);
438
+ if (hit) return { block: true, reason: markerBlockReason(hit) };
439
+ }
440
+ }
441
+
389
442
  // Guard 3 — bash mutation outside the spec dir during brainstorm.
390
443
  if (phases.brainstorm.status === "in_progress" && !firedGuards.get("brainstorm-write")) {
391
444
  // Redirect target is cleanly extractable: judge it directly against the spec dirs,
@@ -474,7 +527,7 @@ export default function (pi: ExtensionAPI) {
474
527
  description:
475
528
  "Track workflow phase progress (brainstorm → plan → implement → verify → ship). " +
476
529
  "Actions: start (mark phase in_progress), complete (mark phase complete), " +
477
- "skip (mark phase skipped with reason), status (show all phases), reset (clear all phases).",
530
+ "skip (mark phase skipped with reason), status (show all phases), reset (clear all phases), substep (set/clear a substep label on an in_progress phase).",
478
531
  parameters: PhaseTrackerParams,
479
532
 
480
533
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -518,7 +571,7 @@ export default function (pi: ExtensionAPI) {
518
571
  } as PhaseTrackerDetails,
519
572
  };
520
573
  }
521
- phases = { ...phases, [params.phase]: { status: "in_progress" } };
574
+ phases = { ...phases, [params.phase]: transitionPhaseState("in_progress") as PhaseState };
522
575
  firedGuards.clear();
523
576
  updateWidget(ctx);
524
577
  return {
@@ -559,7 +612,7 @@ export default function (pi: ExtensionAPI) {
559
612
  } as PhaseTrackerDetails,
560
613
  };
561
614
  }
562
- phases = { ...phases, [params.phase]: { status: "complete" } };
615
+ phases = { ...phases, [params.phase]: transitionPhaseState("complete") as PhaseState };
563
616
  firedGuards.clear();
564
617
  updateWidget(ctx);
565
618
  const advisory =
@@ -591,7 +644,7 @@ export default function (pi: ExtensionAPI) {
591
644
  };
592
645
  }
593
646
  const reason = params.reason;
594
- phases = { ...phases, [params.phase]: { status: "skipped", reason } };
647
+ phases = { ...phases, [params.phase]: transitionPhaseState("skipped", reason) as PhaseState };
595
648
  firedGuards.clear();
596
649
  updateWidget(ctx);
597
650
  return {
@@ -602,6 +655,33 @@ export default function (pi: ExtensionAPI) {
602
655
  };
603
656
  }
604
657
 
658
+ case "substep": {
659
+ if (!params.phase) {
660
+ return {
661
+ content: [{ type: "text", text: "Error: phase required for substep" }],
662
+ details: { action: "substep", phases: { ...phases }, error: "phase required" } as PhaseTrackerDetails,
663
+ };
664
+ }
665
+ const check = checkSubstep(phases[params.phase].status);
666
+ if (!check.ok) {
667
+ return {
668
+ content: [{ type: "text", text: `Error: phase "${params.phase}": ${check.error}` }],
669
+ details: { action: "substep", phases: { ...phases }, error: check.error } as PhaseTrackerDetails,
670
+ };
671
+ }
672
+ const entry: PhaseState = { status: "in_progress" };
673
+ if (typeof params.substep === "string" && params.substep.trim()) entry.substep = params.substep.trim();
674
+ phases = { ...phases, [params.phase]: entry };
675
+ updateWidget(ctx);
676
+ const label = entry.substep
677
+ ? `Phase "${params.phase}" substep → ${entry.substep}`
678
+ : `Phase "${params.phase}" substep cleared`;
679
+ return {
680
+ content: [{ type: "text", text: `${label}\n${formatStatus(phases)}` }],
681
+ details: { action: "substep", phases: { ...phases } } as PhaseTrackerDetails,
682
+ };
683
+ }
684
+
605
685
  case "status": {
606
686
  return {
607
687
  content: [{ type: "text", text: formatStatus(phases) }],
@@ -610,7 +690,9 @@ export default function (pi: ExtensionAPI) {
610
690
  }
611
691
 
612
692
  case "reset": {
613
- phases = emptyPhases();
693
+ phases = Object.fromEntries(
694
+ PHASES.map((p) => [p, transitionPhaseState("pending")]),
695
+ ) as PhaseMap;
614
696
  conformanceDispatched = false;
615
697
  firedGuards.clear();
616
698
  updateWidget(ctx);
@@ -667,6 +749,11 @@ export default function (pi: ExtensionAPI) {
667
749
  );
668
750
  case "skip":
669
751
  return new Text(theme.fg("dim", "⊘ ") + theme.fg("muted", "phase skipped"), 0, 0);
752
+ case "substep": {
753
+ const active = PHASES.find((ph) => p[ph].status === "in_progress");
754
+ const label = active ? phaseLabel(active, p[active].substep) : "";
755
+ return new Text(theme.fg("warning", "→ ") + theme.fg("muted", label), 0, 0);
756
+ }
670
757
  case "status": {
671
758
  let text = theme.fg("muted", "Phases:");
672
759
  for (const ph of PHASES) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
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",
@@ -49,15 +49,19 @@ Work through the items below **in order**. This is your own checklist to follow,
49
49
 
50
50
  Re-entering while a brainstorm is already in progress is safe: mid-brainstorm there are no tasks or later phases to lose, so the reset just re-establishes the same clean slate.
51
51
  2. **Set up the worktree** — see [Worktree First](#worktree-first)
52
- 3. **Explore project context** — files, docs, recent commits, current behaviour
53
- 4. **Ask clarifying questions** one at a time
52
+ 3. **Gather context** — follow `gatherer.md` (same directory): set substep `gather`,
53
+ dispatch the builders, assemble the context draft at the spec path, clear the
54
+ substep. Unconditional, foreground, no user interaction — the next thing the user
55
+ sees is a questionary question. This produces the draft; the step below consumes it.
56
+ 4. **Understand the idea against the draft** — `Read` the draft, verify load-bearing
57
+ claims against real code, ask questions one at a time, append citable findings
54
58
  5. **Propose 2-3 approaches** — with trade-offs and a recommendation
55
59
  6. **Present the design** — in sections, get approval after each
56
60
  7. **Write the spec** — to `doc/specs/` (see [Filename Convention](#filename-convention))
57
61
  8. **Spec self-review (lint)** — placeholder scan + internal consistency + documentation named, run inline
58
62
  9. **Critique pass (auto-dispatched)** — scope + ambiguity; the spec council via `/skill:roasting-the-spec` when `gauntlet_setting` returns verdict `council`, else a fresh `worker` (see [Spec Council](#spec-council-optional))
59
63
  10. **Re-run placeholder scan** — after the critique pass returns, first inline any `external-ref:` flags it raised (see [Spec Self-Review](#spec-self-review-before-user-review-gate)), then re-scan for placeholders its edits may have introduced; surface any ambiguity the worker could not safely resolve at the user gate
60
- 11. **Generate spec summary** — dispatch a fresh, spec-only `spec-summarizer` and render its returned text **verbatim** at the top of the gate message — do not paraphrase, condense, re-section, or rewrite it (see [User Review Gate](#user-review-gate)); this is part of the existing gate, not a new one
64
+ 11. **Generate spec summary** — dispatch a fresh, spec-only `spec-summarizer` writing to an absolute temp-dir path via `outputMode: "file-only"`, then `Read` that file back as the **last content-producing** tool call before composing the gate and render its contents **verbatim** at the top of the gate message — do not paraphrase, condense, re-section, or rewrite it (see [User Review Gate](#user-review-gate)); this is part of the existing gate, not a new one
61
65
  12. **User review gate** — user reviews the committed spec
62
66
  13. **Transition** — only after approval, invoke `/skill:writing-plans`
63
67
 
@@ -112,12 +116,26 @@ Don't try to design a multi-subsystem monolith in one spec doc.
112
116
 
113
117
  ### 3. Understand the idea
114
118
 
115
- - Check the current project state first: files, docs, recent commits, neighboring services.
116
- - **Check if the codebase or ecosystem already solves this** before designing from scratch. Grep, read existing AGENTS.md, look at `doc/` and `doc/specs/`.
117
- - Ask questions **one at a time** to refine the idea.
118
- - Prefer multiple-choice questions; open-ended is fine when needed.
119
- - One question per message. If a topic needs more exploration, split into multiple turns.
120
- - Focus on: purpose, constraints, success criteria, who/what it touches.
119
+ The gather step (see `gatherer.md`) has already assembled a context draft at the spec
120
+ path.
121
+
122
+ - `Read` the draft **unconditionally before composing question one**. The on-disk
123
+ copy is canonical this one rule defeats both a turn-boundary prune after assembly
124
+ and a session restart.
125
+ - The draft is a **helper, not a fence**: judgment still drives exploration. Verify
126
+ load-bearing claims (schemas, contracts, the code being changed) against real code
127
+ via targeted reads (`read_symbol`-grade, not scout's paraphrase) before designing
128
+ against them.
129
+ - **Check if the codebase or ecosystem already solves this** — the draft's recon
130
+ section starts that answer; confirm it before designing from scratch.
131
+ - Ask questions **one at a time** to refine the idea. Prefer multiple-choice; one
132
+ question per message. Focus on: purpose, constraints, success criteria, who/what
133
+ it touches.
134
+ - **Append bar:** append to the draft's `## Appended during questionary` only
135
+ findings the spec will cite — schema shapes, hard constraints, ticket-vs-code
136
+ contradictions, user answers that changed scope. Not a log of every grep.
137
+ (Appending uses `edit`; the `edit` prohibition in the spec-writing step applies
138
+ only there.)
121
139
 
122
140
  ### 4. Explore approaches
123
141
 
@@ -197,8 +215,25 @@ Spec lives in the project's `doc/specs/` (see [Project Routing](#project-routing
197
215
 
198
216
  `<topic>` is a short kebab-case slug (3–6 words). Do **not** append `-design` or any other suffix.
199
217
 
218
+ The slug is minted **once, at gather time**, from the initial prompt; the spec-writing
219
+ overwrite reuses the path. If the questionary invalidated the slug, rename at
220
+ spec-writing: write the spec at the new path **and delete the old draft file**
221
+ (nothing was committed, so this is free).
222
+
200
223
  ## Spec Self-Review (Before User Review Gate)
201
224
 
225
+ Spec-writing replaces the context draft, in this exact order:
226
+
227
+ 1. `Read` the draft in full — **immediately before** the overwrite. Without this, a
228
+ pruned questionary plus a full-replacement `write` destroys the only copy of the
229
+ gathered context at the moment it feeds the spec.
230
+ 2. Write the spec with the `write` tool (**full replacement**) at the spec path.
231
+ Using `edit` at this step is a red flag.
232
+ 3. **Immediately after the write**, confirm line 1 of the file is no longer
233
+ `# CONTEXT DRAFT - NOT A SPEC - fully replaced at spec-writing` — before
234
+ dispatching lint, critique, council, or summarizer. The phase-tracker commit
235
+ guard is a backstop, not the primary check.
236
+
202
237
  After writing the spec to `<project>/doc/specs/<filename>.md` (per [Filename Convention](#filename-convention)) and before showing it to the user, run a self-review pass. **Read all five bullets first, then act:** only the **first three** run here at the main loop (the inline lint); the **last two** (scope + ambiguity) do **not** run inline — they are the dispatched critique pass (checklist item 9). Do not apply scope/ambiguity edits yourself.
203
238
 
204
239
  - **Placeholder scan.** Any `TODO`, `TBD`, `<fill in>`, `[example]`, `xxx`? Either resolve them or convert to explicit "Open Questions" with names.
@@ -236,19 +271,33 @@ After the inline lint and before the user review gate, **brainstorming owns the
236
271
 
237
272
  After self-review (and council review, if configured) and after inlining any external-ref flags, dispatch the spec-only summarizer, then commit the spec on the worktree branch and stop. This is the **same** single human gate - the summary is folded into it, not a new gate.
238
273
 
239
- Dispatch the summarizer on a fresh context, reading only the spec (no `output:` path - capture the return inline; no `model:` - it inherits the main loop unless a preset sets `subagents.agentOverrides.spec-summarizer.model`):
274
+ Mint an absolute temp path outside the worktree (so it is never committed), then dispatch the summarizer on a fresh context, reading only the spec, writing to that path via file-only output (no `model:` - it inherits the main loop unless a preset sets `subagents.agentOverrides.spec-summarizer.model`):
275
+
276
+ ```bash
277
+ SUMMARY_PATH=$(mktemp "${TMPDIR:-/tmp}/gauntlet-spec-summary.XXXXXX") # absolute, portable across GNU/BSD mktemp
278
+ ```
240
279
 
241
280
  ```
242
- subagent({ agent: "spec-summarizer", context: "fresh", cwd: "<abs worktree path, from git rev-parse --show-toplevel>", task:
281
+ subagent({ agent: "spec-summarizer", context: "fresh", cwd: "<abs worktree path, from git rev-parse --show-toplevel>",
282
+ output: "<SUMMARY_PATH>", outputMode: "file-only", task:
243
283
  "Summarize the spec at <abs path to doc/specs/...> for the user review gate. Read ONLY that file." })
244
284
  ```
245
285
 
246
- If the dispatch fails, reach the gate anyway with a one-line "summary generation failed" note - the summary is an aid, not a gate.
286
+ `<SUMMARY_PATH>` above is a placeholder in the dispatch object; it means substitute the value of the shell variable `$SUMMARY_PATH` set above. The steps below use `$SUMMARY_PATH` (the shell form) once the value is in hand.
287
+
288
+ Then commit the spec — this commit is **unconditional**: the summary is only a gate aid, so a degraded or missing summary never blocks it. Evaluate the summary in two stages (the **Degrade path** referenced in each is defined just below):
289
+
290
+ 1. **From the dispatch tool result, before the `Read`.** If the result is **not** an `"Output saved to: <path> (<N> KB, <M> lines)"` reference (e.g. an exit-0 save error returns the full inline output plus an "Output file error" line — the prunable shape, no file to read), or the reference reports under ~500 bytes, or a size grossly disproportionate to the spec (under ~2% of its byte size), or over ~45 KB (the `Read` truncates at 50KB / 2000 lines, so a larger file cannot render whole) — skip the `Read` and take the degrade path. Use the reference's reported figures; do not re-derive them.
291
+ 2. **The `Read` itself, as the last content-producing tool call before composing the gate.** `Read` `$SUMMARY_PATH` and paste its contents verbatim at the top of the gate. If the `Read` fails, returns 0 bytes, or reports truncation — take the degrade path. The `Read` must be last: pi-condense does not protect a `/tmp` read, so any turn boundary between the `Read` and the render lets the ~9KB read result be pruned, reproducing the bug.
292
+
293
+ **Degrade path** — reach the gate with a one-line "summary generation failed" note; never paraphrase from the file-only reference, never render a stub as the canonical summary.
294
+
295
+ Either way — summary rendered or degraded — then `rm "$SUMMARY_PATH"` (unconditional cleanup; harmless if the file was never created, since it lives outside the worktree under the OS temp dir).
247
296
 
248
- Render the summarizer's returned text **verbatim** first — paste it as-is, do **not** paraphrase, condense, re-section, drop sections, or merge it with council output. "Fold into the gate" means *place it inside the gate message*, not *rewrite it*. After the verbatim block, append the commit confirmation, then — as their **own** adjacent lines, not edits to the summary — any council outcome, critique-pass-unresolved ambiguities, and every entry from the summarizer's gap/external-context footer (surface **all** of them, not just the top risk):
297
+ Render the temp file's contents **verbatim** first — paste it as-is, do **not** paraphrase, condense, re-section, drop sections, or merge it with council output. "Fold into the gate" means *place it inside the gate message*, not *rewrite it*. After the verbatim block, append the commit confirmation, then — as their **own** adjacent lines, not edits to the summary — any council outcome, critique-pass-unresolved ambiguities, and every entry from the summarizer's gap/external-context footer (surface **all** of them, not just the top risk):
249
298
 
250
299
  ```
251
- <spec-only summary from spec-summarizer — pasted verbatim, unedited>
300
+ <spec-only summary read back from the temp file — pasted verbatim, unedited>
252
301
 
253
302
  Spec written and committed to <project>/doc/specs/<filename>.md (worktree: <path>).
254
303
 
@@ -259,7 +308,7 @@ Please review. Approve to proceed, or tell me what to change in the spec.
259
308
 
260
309
  If you believe the summary needs correcting, do **not** silently rewrite it — re-dispatch the summarizer or note the discrepancy as an adjacent line beneath the verbatim block.
261
310
 
262
- Wait for the user. On a change request, revise and re-present. On approval, proceed immediately to `/skill:writing-plans` with no further prompt — the plan and execution mode are mechanical derivatives, so the only human gate here is spec approval itself. Don't land the spec on `main`; it stays in the worktree and ships in the same squash commit as the implementation.
311
+ Wait for the user. On a change request, revise the spec and re-present — mint a **fresh** temp path for the re-dispatched summarizer (never reuse a prior round's path, so stale content can never be mistaken for the new summary). On approval, proceed immediately to `/skill:writing-plans` with no further prompt — the plan and execution mode are mechanical derivatives, so the only human gate here is spec approval itself. Don't land the spec on `main`; it stays in the worktree and ships in the same squash commit as the implementation.
263
312
 
264
313
  After approval, mark the brainstorm phase complete:
265
314
 
@@ -280,11 +329,16 @@ phase_tracker({ action: "complete", phase: "brainstorm" })
280
329
  ## Red Flags — STOP
281
330
 
282
331
  - About to write code or start a non-spec edit while this skill is active
332
+ - About to dispatch lint, critique, council, or summarizer while the spec file's line 1 is still the context-draft marker
333
+ - About to run the spec-writing overwrite without re-reading the draft in the same turn
334
+ - About to use `edit` instead of `write` for the spec-writing overwrite
335
+ - About to insert a human gate, announcement, or question between gather dispatch and questionary question one
283
336
  - About to run, deploy, or validate the **proposed change** (vs. observing current behaviour) before the user approved the design
284
337
  - About to skip the critique pass (council if configured, else fresh worker)
285
338
  - Critique dispatch (council or worker) failed to complete and you proceeded to the gate anyway
286
339
  - About to reach the user gate without re-running the placeholder scan after the critique returned
287
- - About to reach the user gate without rendering the spec-only summary (dispatch `spec-summarizer` first; a failed dispatch degrades to a one-line note, it is not silently skipped)
340
+ - About to reach the user gate without rendering the spec-only summary (dispatch `spec-summarizer` to a temp file first; a failed/stub/truncated read degrades to a one-line note, it is not silently skipped)
341
+ - About to compose the gate message when the summary `Read` was not the last content-producing tool call before it (a following `rm` of the temp file is fine) — a turn boundary between the `Read` and the render lets pi-condense prune the ~9KB read result, reproducing the original bug
288
342
  - About to present a paraphrased, condensed, or re-sectioned version of the summarizer's output instead of pasting its returned text verbatim — rewriting the summary counts as not rendering it
289
343
  - About to run the scope or ambiguity checks inline yourself instead of dispatching them (those two are the critique pass, not the inline lint)
290
344
  - About to skip the self-review pass
@@ -0,0 +1,117 @@
1
+ # Context Gather (brainstorming supplementary)
2
+
3
+ Consumed only by `SKILL.md` in this directory. Runs unconditionally between worktree
4
+ setup and the questionary. **Foreground, no user interaction** — the first thing the
5
+ operator sees after gather is questionary question one. Do not announce, do not ask.
6
+
7
+ ## Dispatch
8
+
9
+ Mint a temp dir outside the worktree (never committed):
10
+
11
+ ```bash
12
+ GATHER_DIR=$(mktemp -d)
13
+ ```
14
+
15
+ Set the substep, then dispatch one parallel-tasks `subagent` call (foreground — no
16
+ `async:`; no `model:` — pi-cohort `agentOverrides` owns builder models):
17
+
18
+ ```
19
+ phase_tracker({ action: "substep", phase: "brainstorm", substep: "gather" })
20
+ subagent({
21
+ tasks: [
22
+ { agent: "scout", cwd: "<abs worktree path>", phase: "context-gather",
23
+ output: "<GATHER_DIR>/scout.md",
24
+ task: "<scout task, template below>" },
25
+ // include ONLY when the trigger rule below fires:
26
+ { agent: "context-builder", cwd: "<abs worktree path>", phase: "context-gather",
27
+ output: "<GATHER_DIR>/external.md",
28
+ task: "<context-builder task, template below>" }
29
+ ]
30
+ })
31
+ ```
32
+
33
+ Absolute `output:` paths are mandatory: relative paths in parallel mode resolve
34
+ against the worktree and would get committed.
35
+
36
+ ## Task templates
37
+
38
+ Scout (always dispatched):
39
+
40
+ > Recon for an upcoming design discussion. The request: `<initial prompt verbatim>`.
41
+ > Map the territory this change touches: relevant files with line ranges, existing
42
+ > patterns and conventions the change must match, test conventions, integration
43
+ > points, and whether the codebase or ecosystem already solves any of this. Cite
44
+ > exact paths and line ranges. End with an "Open questions that matter for the spec"
45
+ > section. Compact handoff, not a dump.
46
+
47
+ Context-builder (conditional):
48
+
49
+ > Extract external context for an upcoming design discussion. The request:
50
+ > `<initial prompt verbatim>`. Fetch and distill these references:
51
+ > `<detected refs, one per line>`. For each: acceptance criteria, hard constraints,
52
+ > linked discussion that changes scope, and contradictions with the request as
53
+ > stated. Write ONLY the context handoff to your output path; do NOT produce a
54
+ > meta-prompt file. End with an "Open questions that matter for the spec" section.
55
+ > If a ref is unreadable, say so explicitly and continue.
56
+
57
+ (The meta-prompt exclusion matters: in chain mode context-builder emits two files —
58
+ `context.md` + `meta-prompt.md`; this flow consumes only the context handoff.)
59
+
60
+ ## Context-builder trigger rule
61
+
62
+ Dispatch context-builder when the initial prompt (or a file it explicitly references)
63
+ contains any of:
64
+
65
+ - an `http(s)://` URL;
66
+ - a tracker-style ID matching `[A-Z][A-Z0-9]+-\d+` (Linear/Jira form) **when a fetch
67
+ path exists** (a tracker tool/MCP, or a URL pattern in `.pi/gauntlet-overrides.md`);
68
+ - a GitHub-style ref `owner/repo#N`, or a bare `#N` when the repo's tracker is
69
+ GitHub Issues.
70
+
71
+ Examples: "implement ABC-123" with a Linear tool available → trigger; "add rate
72
+ limiting like https://example.com/rfc" → trigger; "rename the settings resolver" →
73
+ scout only. An opaque ID with **no** fetch path → do not dispatch; list it in the
74
+ draft's `## External context` as an unfetched ref instead of guessing.
75
+
76
+ ## Failure and degradation
77
+
78
+ A builder **failed** when its task errored **or** its output file is missing or
79
+ empty (0 bytes). Degradation never blocks and never surfaces to the user at gather
80
+ time:
81
+
82
+ - scout failed → `## Codebase recon` reads: `Scout recon failed (<one-line reason>).
83
+ Draft is thin; exploration falls to the questionary.` followed by the initial prompt.
84
+ - context-builder failed → `## External context` reads: `External refs not fetched
85
+ (<one-line reason>):` followed by the ref list — the critique pass later surfaces
86
+ them as external-ref candidates.
87
+
88
+ ## Draft assembly
89
+
90
+ Read the temp files and write the draft **to the spec path** (normal filename
91
+ convention; slug minted from the initial prompt):
92
+
93
+ ```markdown
94
+ # CONTEXT DRAFT - NOT A SPEC - fully replaced at spec-writing
95
+
96
+ ## Codebase recon
97
+ <scout output, or the degraded text>
98
+
99
+ ## External context
100
+ <context-builder output, degraded text, or unfetched-ref list; omit this section
101
+ only when the trigger rule never fired>
102
+
103
+ ## Appended during questionary
104
+ <starts empty>
105
+ ```
106
+
107
+ The marker line is **line 1, verbatim**. Then clean up and clear the substep:
108
+
109
+ ```bash
110
+ rm -rf "$GATHER_DIR"
111
+ ```
112
+
113
+ ```
114
+ phase_tracker({ action: "substep", phase: "brainstorm", substep: null })
115
+ ```
116
+
117
+ The questionary runs under plain `brainstorm`.
@@ -141,13 +141,13 @@ Auto-selected at handoff by `writing-plans` (any wave with ≥2 tasks) when the
141
141
 
142
142
  1. **Independence check.** Parse the wave's tasks' `Files:` blocks; assert pairwise-disjoint (mechanical). Runtime-resource disjointness (DB/schema, port, fixture, external service, shared temp path) is not machine-checkable here — trust the plan's wave grouping, which `writing-plans`' D5 contract guarantees. Either kind of overlap → the wave is mis-grouped; run those tasks as sequential single-task waves and note it.
143
143
  2. **Fan out.** One parallel dispatch (shape below): `implementer` per task, `context: "fresh"`, `worktree: true`. Each returns a status + a patch.
144
- 3. **Status + spec review per task.** Parse each `DONE`/`BLOCKED`/etc. (see [Implementer Status](#implementer-status)); spec-review each returned patch against its task (read-only, parallelizable). Re-dispatch incomplete tasks (fresh, `worktree: true`) until `DONE` + spec ✅.
144
+ 3. **Status + spec review per task.** Parse each `DONE`/`BLOCKED`/etc. (see [Implementer Status](#implementer-status)) **first**. Then **dispatch a `spec-reviewer` per accepted patch** (`DONE`, or a `DONE_WITH_CONCERNS` you proceeded with) in one parallel fan-out — `context: "fresh"`, `cwd: <this worktree>`, **no `worktree` flag** (read-only) — each passed its task text, the returned **patch diff**, and the absolute spec path. Review is **diff-based**: the diff's hunks carry `file:line`, and test execution is not the reviewer's job here (the wave test gate in step 5 runs the suite). Inline verdicts are fine at normal wave sizes; large waves use `output:` + `outputMode: "file-only"` to keep verdicts out of your context. **Re-dispatch by cause:** `BLOCKED`/`NEEDS_CONTEXT` per the [Implementer Status](#implementer-status) matrix; a **spec gap** re-dispatches the implementer (fresh, `worktree: true`) carrying the prior patch + the reviewer's findings, the new patch superseding the old at step 4. Loop until accepted + spec ✅.
145
145
  4. **Integrate.** `git apply` each task's patch sequentially onto HEAD. Apply fails = textual conflict → drop that task, finish the rest, re-run the dropped task sequentially on the updated HEAD.
146
146
  5. **Test gate.** Run the suite on the integrated tree. Failure = semantic conflict or bug → re-run the offending task sequentially, else fix per [When a Subagent Fails](#when-a-subagent-fails).
147
147
  6. **Quality review.** Code-quality review on the integrated wave diff; loop fixes to ✅.
148
148
  7. **Commit the wave.** Leaves a clean tree; the next wave's children branch from this commit and so see the integrated work.
149
149
 
150
- **Two-stage review is preserved:** spec review per task (pre-integration), quality review per wave (post-integration). Both gates required before the wave commits.
150
+ **Two-stage review is preserved:** spec review per task (pre-integration, dispatched `spec-reviewer` — not inline), quality review per wave (post-integration). Both gates required before the wave commits.
151
151
 
152
152
  **Dependent context across waves:** wave N+1 tasks branch from a HEAD containing wave N, so they see the code; still forward wave N's task summaries into wave N+1 prompts.
153
153
 
@@ -203,6 +203,7 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
203
203
  - Starting code-quality review before spec compliance is ✅
204
204
  - Moving to next task with either review still showing issues
205
205
  - Letting implementer self-review replace external review (both needed)
206
+ - Spec-reviewing wave patches inline instead of dispatching `spec-reviewer` per patch — sequential mode's step 3 dispatches it; wave mode must too
206
207
  - Pausing to "check in" between tasks (continuous execution rule)
207
208
  - Skipping the `Implementer Status` parse — treating every response as DONE
208
209
  - Starting on main without explicit user consent