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 +64 -0
- package/README.md +71 -211
- package/agents/spec-summarizer.md +2 -1
- package/extensions/lib/phase-tracker-helpers.test.ts +120 -0
- package/extensions/lib/phase-tracker-helpers.ts +97 -0
- package/extensions/phase-tracker.ts +97 -10
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +70 -16
- package/skills/brainstorming/gatherer.md +117 -0
- package/skills/subagent-driven-development/SKILL.md +3 -2
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
|
[](https://buymeacoffee.com/jjurasszek)
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
##
|
|
11
|
+
## The problem
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
## Why pi-gauntlet exists
|
|
18
18
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
23
|
+
## Part of the pi agent toolkit
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
## Architecture
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
|
|
42
64
|
|
|
43
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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`.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
138
|
+
## Configuring the gates
|
|
228
139
|
|
|
229
|
-
|
|
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
|
-
|
|
142
|
+
## Relationship to the other repos
|
|
232
143
|
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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]:
|
|
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]:
|
|
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]:
|
|
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 =
|
|
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
|
@@ -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. **
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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>",
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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))
|
|
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
|