pi-gauntlet 4.3.1 → 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 +37 -0
- package/README.md +71 -211
- 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 +47 -8
- package/skills/brainstorming/gatherer.md +117 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
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
|
+
|
|
3
40
|
## v4.3.1 - 2026-07-06
|
|
4
41
|
|
|
5
42
|
Fix spec-summary pruning at the brainstorming user-review gate. The gate's
|
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
|
|
|
@@ -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,8 +49,12 @@ 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))
|
|
@@ -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.
|
|
@@ -294,6 +329,10 @@ phase_tracker({ action: "complete", phase: "brainstorm" })
|
|
|
294
329
|
## Red Flags — STOP
|
|
295
330
|
|
|
296
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
|
|
297
336
|
- About to run, deploy, or validate the **proposed change** (vs. observing current behaviour) before the user approved the design
|
|
298
337
|
- About to skip the critique pass (council if configured, else fresh worker)
|
|
299
338
|
- Critique dispatch (council or worker) failed to complete and you proceeded to the gate anyway
|
|
@@ -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`.
|