pi-gauntlet 5.3.0 → 5.3.2
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 +11 -0
- package/README.md +5 -5
- package/extensions/lib/plan-check.test.ts +162 -4
- package/extensions/lib/plan-check.ts +51 -14
- package/extensions/phase-tracker.test.ts +161 -3
- package/extensions/phase-tracker.ts +38 -0
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +8 -2
- package/skills/brainstorming/gatherer.md +4 -2
- package/skills/dispatching-parallel-agents/SKILL.md +5 -1
- package/skills/requesting-code-review/SKILL.md +3 -3
- package/skills/roasting-the-spec/SKILL.md +5 -3
- package/skills/subagent-driven-development/SKILL.md +24 -20
- package/skills/test-driven-development/SKILL.md +1 -1
- package/skills/verification-before-completion/SKILL.md +1 -1
- package/skills/verification-before-completion/reference/conformance-check.md +12 -20
- package/skills/writing-plans/SKILL.md +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v5.3.2 - 2026-09-06
|
|
4
|
+
|
|
5
|
+
- Plan coverage: `## Spec coverage` rows accept the exact owner `Verification` for requirements the plan header's `**Verification:**` command proves. `table-closure` closes such rows (mechanical row, unparseable anchor, or literal-free anchor -> finding; never counted as task coverage); `quote-integrity` resolves their literals by verbatim containment in the backtick-stripped header. Orphan-owner reason now names `Verification`; anchor-parseability is checked once per row. `header-entrypoint` unchanged. `writing-plans` documents the owner form with a scoped-tests / final-suite example. Spec: `doc/specs/2026-09-06-gh-24-final-verification-ownership.md` (closes #24).
|
|
6
|
+
|
|
7
|
+
## v5.3.1 - 2026-09-06
|
|
8
|
+
|
|
9
|
+
- Task tracking: planning initializes the list once; execution starts and completes existing indices explicitly. Execution retries and fix loops reuse their task; conformance adds only new remediation gaps and reuses their `Gn` indices across rounds.
|
|
10
|
+
- `phase_tracker`: explicit implement/verify completion rejects recorded pending or in-progress tasks under the existing flow guards. Terminal-negative `failed` tasks remain failed; existing escalation rules still apply. Sequential tool execution prevents same-batch tracker updates from being read too early. Requires Pi 0.85.1 or newer; no new settings or tracker schema.
|
|
11
|
+
- Foreground flow dispatch: agent calls explicitly set `async: false`, preserving independent implementation, per-patch SR and council fan-outs. Final order is parent full verification -> whole-diff CR -> conformance; removes async review/test overlap and its classification/polling branches. Pi-cohort's `forceTopLevelAsync` setting is incompatible with this policy.
|
|
12
|
+
- Verification: runtime regressions and real Pi batch ordering pass. Behavioral diagnostics retain observed model-following limits: missed task reopening, an omitted async flag and malformed deferred-gap output. These are documented limits, not claims of infallible tracking or runtime enforcement of foreground dispatch. Spec: `doc/specs/2026-09-06-task-tracking-reliability.md`.
|
|
13
|
+
|
|
3
14
|
## v5.3.0 - 2026-09-04
|
|
4
15
|
|
|
5
16
|
- `chase-bug`: the real-bug verdict menu gains a fourth row, "Implement hotfix now", for small, evidenced, urgent fixes - a middle ceremony tier between the trivial carve-out and the full gauntlet. It hands off to the new companion `skills/chase-bug/hotfix.md`: dedicated `hotfix/<slug>` worktree, one implementer (TDD), one code review as the last mutation gate, an advisory conformance pass, then an unpushed squash commit on the default branch with guaranteed worktree/branch cleanup (or a PR when the pick says "as a PR"). Three safety invariants (no schema/persistence change, no public-contract change, one-commit rollback) gate the row's availability; three judgment predicates steer `[recommended]`. Triage itself stays read-only; the row is reachable only through the verdict menu. Zero changes to finishing, SDD, verification, extensions, or ci.mjs. Spec: `doc/specs/2026-09-03-chase-bug-hotfix.md`.
|
package/README.md
CHANGED
|
@@ -38,8 +38,8 @@ Concretely, one change through the gauntlet:
|
|
|
38
38
|
0. *(Optional)* Before there's even a spec, `/skill:shape-ticket` can create or repair a single tracker issue - shaping a raw ask into a Context/Problem/Idea/Acceptance Criteria ticket, gated by an AC integrity check, a cheap council roast, and one human-confirmed write that may include one optional gated Reporter-note comment. A failed roast is retried once, then surfaced inline at the gate if it fails again. It's a tool, not a phase: no worktree, no plan/phase tracker, runs from any repo state. It never activates on its own (`disable-model-invocation: true`) - invoke it explicitly. Similarly, `/skill:chase-bug` triages a raw bug report into an evidenced verdict - and can hand off to shape-ticket, brainstorming, or a bounded hotfix - before any spec exists.
|
|
39
39
|
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. If the spec replaces a known prior spec, brainstorming marks the predecessor with a `> **Superseded by:**` banner under its title (default format, syntax overridable via `.pi/gauntlet-overrides.md`; event-driven only — gauntlet never sweeps historical specs). **You read and approve the spec - human gate 1.** No implementation code exists yet.
|
|
40
40
|
2. **`writing-plans`** decomposes the approved spec into atomic, independently-verifiable tasks, grouped into parallel waves where they don't touch the same files.
|
|
41
|
-
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.
|
|
42
|
-
4. **verify**: a whole-diff code review, then the **conformance gate
|
|
41
|
+
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. Independent implementation, review, and council batches remain parallel, but their dispatches are foreground: the orchestrator waits for terminal results before accepting work or advancing a phase.
|
|
42
|
+
4. **verify**: the parent first runs the plan's full verification command set; only a passing result permits whole-diff code review, then the **conformance gate**. A review fix invalidates that result, so the parent reruns full verification before the next review or conformance gate. The conformance 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. Inside a brainstorming-entered flow this gate is machine-blocked from being skipped. Compatible executable recommendations auto-run through an isolated fix-and-re-audit loop with no prompt; anything still open surfaces as a dense list - one line per decision, plain-language, with its recommended choice inline. Reply `1` to take every recommendation, or `2:` with per-item overrides; a current `CONFORMS` / no-concerns result goes straight to the branch options with no extra conformance sign-off.
|
|
43
43
|
5. **`finishing-a-development-branch`**: squash, PR, keep, or discard. Once a PR exists, run `/skill:gatekeep-pr <pr>` to verify it against its issue before merging. **Human gate 2** - the only other decision you make.
|
|
44
44
|
6. *(Optional)* Once the merge lands, `/skill:check-delivery <ref>` can prove delivery - default-branch landing, delivery target, per-AC evidence - before the tracker status advances. Explicit invocation only, no auto-chain: deploys commonly lag merges by minutes to hours, so an auto-run would routinely check too early.
|
|
45
45
|
|
|
@@ -71,7 +71,7 @@ pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
|
|
|
71
71
|
|
|
72
72
|
- **17 skills** - the workflow logic. Thirteen 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`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `writing-skills`, `linear` (reads/searches/comments on/manages Linear tickets via the `linearis` CLI; owns all linearis mechanics and the `## Issue tracker` overrides schema; tracker-facing skills route to it). Four more are explicit-invocation-only (`disable-model-invocation: true`): `shape-ticket` creates or repairs one tracker issue per run against a Context/Problem/Idea/Acceptance-Criteria template, gated by an AC integrity check, a cheap council roast, and a single human-confirmed write - run it with `/skill:shape-ticket`. `gatekeep-pr` is consent-gated pre-merge verification of a PR against its issue - read-only gathering, verification evidence resolved CI-first (green checks on the exact assessed head count as evidence; the project's verification command runs only as fallback), a rubric-based review, then a deterministic authorship-aware menu with stable finding IDs (P#/L#/C#/F#) and numbered pre-composed courses (fixes execute as a single parallel-safe wave: one gate run, one re-review, one push); nothing mutates (fixes, pushes, reviews, merges) until you pick a row - run it with `/skill:gatekeep-pr <pr>`. `check-delivery` is a post-merge detective control: proves an issue actually shipped (default-branch landing, delivery target, per-AC evidence) before its tracker status advances; it never writes a terminal status - run it with `/skill:check-delivery <ref>`. `chase-bug` is human-only bug triage: read-only root-cause discovery to an evidenced verdict menu (real bug -> ticket/brainstorm/hotfix/respond; five negative verdicts), then a gated response to the reporter for addressable origins (GitHub issue / tracker ticket) and a rendered verdict summary otherwise - it never fixes during triage; the hotfix row hands off to `skills/chase-bug/hotfix.md` after the menu - run it with `/skill:chase-bug`.
|
|
73
73
|
- **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.
|
|
74
|
-
- **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 push or open a PR without a passing test run since your last edit; a phase-tracker flow guard reminds on implement-phase commits missing spec/code review. phase-tracker also registers plan_check, a deterministic plan linter (8 mechanical plan-vs-spec checks) whose pass stamp gates implement-start inside a gauntlet flow. See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
|
|
74
|
+
- **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 push or open a PR without a passing test run since your last edit; a phase-tracker flow guard reminds on implement-phase commits missing spec/code review. In a brainstorming-entered flow, phase-tracker rejects `implement` or `verify` completion while tracker tasks remain pending or in progress; see [its configuration reference](./doc/configuration.md#phase-tracker). phase-tracker also registers plan_check, a deterministic plan linter (8 mechanical plan-vs-spec checks) whose pass stamp gates implement-start inside a gauntlet flow. See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
|
|
75
75
|
|
|
76
76
|
pi-gauntlet is **opinionated**: every non-trivial change is *meant* to ride this one pipeline, entered through `brainstorming`. Enforcement is opt-in by entry, not ambient: once brainstorming starts a flow, the phase-tracker extension mechanically blocks a phase from closing before its gate runs, and warns once if the main loop writes code during implement (subagents own implement-phase edits). A change made *without* entering the flow (a typo, a formatting run, a dependency bump - see "When to use / when NOT to use") is not gated; the discipline of routing real work through the pipeline is a convention the tooling supports, not a trap it springs on every edit.
|
|
77
77
|
|
|
@@ -93,8 +93,8 @@ pi-gauntlet is **opinionated**: every non-trivial change is *meant* to ride this
|
|
|
93
93
|
|
|
94
94
|
## Requirements
|
|
95
95
|
|
|
96
|
-
- [pi-coding-agent](https://github.com/earendil-works/pi) ≥ 0.1.
|
|
97
|
-
- [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`, `shape-ticket`, `roasting-the-spec`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call.
|
|
96
|
+
- [pi-coding-agent](https://github.com/earendil-works/pi) ≥ 0.85.1 - tested minimum for sequential `phase_tracker` tool execution, which persists a tracker update before a later tool call in the same model message.
|
|
97
|
+
- [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`, `shape-ticket`, `roasting-the-spec`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call. Gauntlet execution dispatches explicitly use `async: false`; do not enable pi-cohort's incompatible `forceTopLevelAsync` setting. See pi-cohort's [dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md) for that setting's owner and semantics.
|
|
98
98
|
|
|
99
99
|
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.
|
|
100
100
|
|
|
@@ -16,6 +16,9 @@ const SPEC_TEXT = [
|
|
|
16
16
|
"", // 11
|
|
17
17
|
"## Other", // 12
|
|
18
18
|
"Stuff.", // 13
|
|
19
|
+
"", // 14
|
|
20
|
+
"## Acceptance", // 15
|
|
21
|
+
"Full suite passes: `npm run fixture-verify`.", // 16
|
|
19
22
|
].join("\n");
|
|
20
23
|
|
|
21
24
|
const VALID_PLAN = `# Fixture Plan
|
|
@@ -70,6 +73,7 @@ The literal TODO is intentionally documented here per spec quote-integrity requi
|
|
|
70
73
|
| § "Testing" L9-L9 | banned token literal handling | Task 3 |
|
|
71
74
|
| - | mechanical: wire test into CI | Task 2 |
|
|
72
75
|
| § "Other" L12-L13 | out of scope thing | waived: out of scope per spec |
|
|
76
|
+
| § "Acceptance" L16 | full suite passes | Verification |
|
|
73
77
|
`;
|
|
74
78
|
|
|
75
79
|
function alwaysTruePort(): FsPort {
|
|
@@ -157,7 +161,7 @@ test("check 1 owner-cell grammar: trailing junk after a Task <n> list is malform
|
|
|
157
161
|
assert.ok(
|
|
158
162
|
tc.some(
|
|
159
163
|
(f) =>
|
|
160
|
-
f.reason.includes("owner cell is not a 'Task <n>' list or 'waived: <reason>'") &&
|
|
164
|
+
f.reason.includes("owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'") &&
|
|
161
165
|
f.text.includes("Task 1 (see note)"),
|
|
162
166
|
),
|
|
163
167
|
`expected an owner-cell malformed finding, got: ${JSON.stringify(tc)}`,
|
|
@@ -165,7 +169,7 @@ test("check 1 owner-cell grammar: trailing junk after a Task <n> list is malform
|
|
|
165
169
|
assert.ok(
|
|
166
170
|
tc.some(
|
|
167
171
|
(f) =>
|
|
168
|
-
f.reason.includes("owner cell is not a 'Task <n>' list or 'waived: <reason>'") &&
|
|
172
|
+
f.reason.includes("owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'") &&
|
|
169
173
|
f.line === lineOf(mutated, '| § "Design" L4-L6 | parser grammar basics | Task 1 (see note) |'),
|
|
170
174
|
),
|
|
171
175
|
`expected the owner-cell malformed finding's line to point at the offending row, got: ${JSON.stringify(tc)}`,
|
|
@@ -180,19 +184,173 @@ test("check 1 owner-cell grammar: empty waiver reason is malformed, not accepted
|
|
|
180
184
|
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
181
185
|
const tc = findingsFor(findings, "table-closure");
|
|
182
186
|
assert.ok(
|
|
183
|
-
tc.some((f) => f.reason.includes("owner cell is not a 'Task <n>' list or 'waived: <reason>'")),
|
|
187
|
+
tc.some((f) => f.reason.includes("owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'")),
|
|
184
188
|
`expected an owner-cell malformed finding for empty waiver reason, got: ${JSON.stringify(tc)}`,
|
|
185
189
|
);
|
|
186
190
|
assert.ok(
|
|
187
191
|
tc.some(
|
|
188
192
|
(f) =>
|
|
189
|
-
f.reason.includes("owner cell is not a 'Task <n>' list or 'waived: <reason>'") &&
|
|
193
|
+
f.reason.includes("owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'") &&
|
|
190
194
|
f.line === lineOf(mutated, '| § "Other" L12-L13 | out of scope thing | waived: |'),
|
|
191
195
|
),
|
|
192
196
|
`expected the owner-cell malformed finding's line to point at the offending row, got: ${JSON.stringify(tc)}`,
|
|
193
197
|
);
|
|
194
198
|
});
|
|
195
199
|
|
|
200
|
+
const VERIFICATION_ROW = '| § "Acceptance" L16 | full suite passes | Verification |';
|
|
201
|
+
|
|
202
|
+
function withRow(plan: string, row: string): string {
|
|
203
|
+
return plan.replace(
|
|
204
|
+
'| § "Other" L12-L13 | out of scope thing | waived: out of scope per spec |',
|
|
205
|
+
`| § "Other" L12-L13 | out of scope thing | waived: out of scope per spec |\n${row}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const owner of ["verification", "Verify", "VERIFICATION", "Task 1, Verification"]) {
|
|
210
|
+
test(`Verification owner: '${owner}' is a malformed owner -> table-closure`, () => {
|
|
211
|
+
const row = `| § "Acceptance" L16 | full suite passes | ${owner} |`;
|
|
212
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
213
|
+
const tc = findingsFor(findings, "table-closure");
|
|
214
|
+
assert.ok(
|
|
215
|
+
tc.some(
|
|
216
|
+
(f) =>
|
|
217
|
+
f.reason === "owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'" &&
|
|
218
|
+
f.text === row,
|
|
219
|
+
),
|
|
220
|
+
`expected orphan-owner finding for ${owner}, got: ${JSON.stringify(tc)}`,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
test("Verification owner: unparseable anchor ('-') -> table-closure parseability reason", () => {
|
|
226
|
+
const row = "| - | full suite passes | Verification |";
|
|
227
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
228
|
+
const tc = findingsFor(findings, "table-closure");
|
|
229
|
+
assert.ok(
|
|
230
|
+
tc.some(
|
|
231
|
+
(f) =>
|
|
232
|
+
f.reason === 'requirement row anchor is not a parseable § "heading" L<n>-L<n> anchor' && f.text === row,
|
|
233
|
+
),
|
|
234
|
+
`expected parseability finding, got: ${JSON.stringify(tc)}`,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("Verification owner: anchored lines without a backtick literal -> table-closure", () => {
|
|
239
|
+
const row = '| § "Other" L13 | stuff | Verification |';
|
|
240
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
241
|
+
const tc = findingsFor(findings, "table-closure");
|
|
242
|
+
assert.ok(
|
|
243
|
+
tc.some(
|
|
244
|
+
(f) => f.reason === "Verification row has no backtick literal to check against the header" && f.text === row,
|
|
245
|
+
),
|
|
246
|
+
`expected no-literal finding, got: ${JSON.stringify(tc)}`,
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("Verification owner: mechanical row -> table-closure", () => {
|
|
251
|
+
const row = "| - | mechanical: run the suite | Verification |";
|
|
252
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
253
|
+
const tc = findingsFor(findings, "table-closure");
|
|
254
|
+
assert.ok(
|
|
255
|
+
tc.some((f) => f.reason === "mechanical row owner must be a Task <n>" && f.text === row),
|
|
256
|
+
`expected mechanical-owner finding, got: ${JSON.stringify(tc)}`,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("table-closure: multi-owner row with unparseable anchor yields one parseability finding", () => {
|
|
261
|
+
const row = "| L999 | naming details | Task 1, Task 2 |";
|
|
262
|
+
const findings = checkPlan(withRow(VALID_PLAN, row), SPEC_TEXT, alwaysTruePort());
|
|
263
|
+
const parseability = findingsFor(findings, "table-closure").filter(
|
|
264
|
+
(f) => f.text === row && f.reason.includes("not a parseable"),
|
|
265
|
+
);
|
|
266
|
+
assert.equal(parseability.length, 1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("Verification owner: does not satisfy a task's coverage requirement", () => {
|
|
270
|
+
const mutated = withRow(VALID_PLAN, VERIFICATION_ROW).replace(
|
|
271
|
+
'| § "Testing" L9-L9 | banned token literal handling | Task 3 |\n',
|
|
272
|
+
"",
|
|
273
|
+
);
|
|
274
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
275
|
+
assert.ok(
|
|
276
|
+
findingsFor(findings, "table-closure").some((f) =>
|
|
277
|
+
f.reason.includes("Task 3 does not appear as an owner"),
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("Verification quote-integrity: literal missing from header -> quote-integrity on the row", () => {
|
|
283
|
+
const mutated = VALID_PLAN.replace("**Verification:** npm run fixture-verify", "**Verification:** npm run other");
|
|
284
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
285
|
+
const qi = findingsFor(findings, "quote-integrity");
|
|
286
|
+
const row = '| § "Acceptance" L16 | full suite passes | Verification |';
|
|
287
|
+
assert.ok(
|
|
288
|
+
qi.some(
|
|
289
|
+
(f) =>
|
|
290
|
+
f.reason === "verification header does not contain the required verbatim literal `npm run fixture-verify`" &&
|
|
291
|
+
f.text === row &&
|
|
292
|
+
f.line === lineOf(mutated, row),
|
|
293
|
+
),
|
|
294
|
+
`expected header-containment finding, got: ${JSON.stringify(qi)}`,
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("Verification quote-integrity: multi-command header contains the literal", () => {
|
|
299
|
+
const mutated = VALID_PLAN.replace(
|
|
300
|
+
"**Verification:** npm run fixture-verify",
|
|
301
|
+
"**Verification:** `npm run fixture-verify && npm run lint`",
|
|
302
|
+
);
|
|
303
|
+
assert.deepEqual(findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()), "quote-integrity"), []);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("Verification quote-integrity: two-span header contains both literals", () => {
|
|
307
|
+
const spec = SPEC_TEXT.replace(
|
|
308
|
+
"Full suite passes: `npm run fixture-verify`.",
|
|
309
|
+
"Full suite passes: `npm run fixture-verify` and `npm run lint`.",
|
|
310
|
+
);
|
|
311
|
+
const mutated = VALID_PLAN.replace(
|
|
312
|
+
"**Verification:** npm run fixture-verify",
|
|
313
|
+
"**Verification:** `npm run fixture-verify`, `npm run lint`",
|
|
314
|
+
);
|
|
315
|
+
assert.deepEqual(findingsFor(checkPlan(mutated, spec, alwaysTruePort()), "quote-integrity"), []);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("Verification quote-integrity: missing header -> quote-integrity per literal plus header-entrypoint", () => {
|
|
319
|
+
const mutated = VALID_PLAN.replace("**Verification:** npm run fixture-verify\n", "");
|
|
320
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
321
|
+
assert.equal(
|
|
322
|
+
findingsFor(findings, "quote-integrity").filter((f) => f.reason.includes("`npm run fixture-verify`")).length,
|
|
323
|
+
1,
|
|
324
|
+
);
|
|
325
|
+
assert.ok(findingsFor(findings, "header-entrypoint").some((f) => f.reason.includes("missing header")));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("Verification quote-integrity: task body containing the full header string still fails header-entrypoint", () => {
|
|
329
|
+
const mutated = VALID_PLAN.replace(
|
|
330
|
+
"This task handles naming details.",
|
|
331
|
+
"This task handles naming details. Run npm run fixture-verify here.",
|
|
332
|
+
);
|
|
333
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
334
|
+
assert.ok(findingsFor(findings, "header-entrypoint").some((f) => f.text.includes("Run npm run fixture-verify here")));
|
|
335
|
+
assert.deepEqual(findingsFor(findings, "quote-integrity"), []);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("Verification quote-integrity: task body containing only a sub-command of a multi-command header is not caught by header-entrypoint", () => {
|
|
339
|
+
const mutated = VALID_PLAN.replace(
|
|
340
|
+
"**Verification:** npm run fixture-verify",
|
|
341
|
+
"**Verification:** `npm run fixture-verify && npm run lint`",
|
|
342
|
+
).replace("This task handles naming details.", "This task handles naming details. Run npm run fixture-verify here.");
|
|
343
|
+
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
344
|
+
assert.deepEqual(findingsFor(findings, "header-entrypoint"), []);
|
|
345
|
+
assert.deepEqual(findingsFor(findings, "quote-integrity"), []);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("Verification quote-integrity: task-owned literal check unchanged", () => {
|
|
349
|
+
const mutated = VALID_PLAN.replace("This task implements helperFn() for parsing.", "This task implements the helper.");
|
|
350
|
+
const qi = findingsFor(checkPlan(mutated, SPEC_TEXT, alwaysTruePort()), "quote-integrity");
|
|
351
|
+
assert.ok(qi.some((f) => f.reason.includes("Task 1 body does not contain the required verbatim literal `helperFn()`")));
|
|
352
|
+
});
|
|
353
|
+
|
|
196
354
|
test("check 3 anchor-resolution: ambiguous heading match (duplicate spec heading)", () => {
|
|
197
355
|
const dupSpec = SPEC_TEXT.replace('## Testing', '## Design\n\nduplicate section body.\n\n## Testing');
|
|
198
356
|
const findings = checkPlan(VALID_PLAN, dupSpec, alwaysTruePort());
|
|
@@ -69,6 +69,7 @@ interface CoverageRow {
|
|
|
69
69
|
isWaived: boolean;
|
|
70
70
|
ownerTasks: number[];
|
|
71
71
|
ownerMalformed: boolean;
|
|
72
|
+
isVerification: boolean;
|
|
72
73
|
anchor: Anchor | undefined;
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -270,6 +271,7 @@ function parsePlan(planText: string): ParsedPlan {
|
|
|
270
271
|
let isWaived = false;
|
|
271
272
|
let ownerTasks: number[] = [];
|
|
272
273
|
let ownerMalformed = false;
|
|
274
|
+
const isVerification = ownerCell === "Verification";
|
|
273
275
|
if (waivedMatch) {
|
|
274
276
|
if (waivedMatch[1].trim().length === 0) {
|
|
275
277
|
ownerMalformed = true;
|
|
@@ -278,7 +280,7 @@ function parsePlan(planText: string): ParsedPlan {
|
|
|
278
280
|
}
|
|
279
281
|
} else if (isTaskList) {
|
|
280
282
|
ownerTasks = [...ownerCell.matchAll(/Task (\d+)/g)].map((mm) => Number(mm[1]));
|
|
281
|
-
} else {
|
|
283
|
+
} else if (!isVerification) {
|
|
282
284
|
ownerMalformed = true;
|
|
283
285
|
}
|
|
284
286
|
let anchor: Anchor | undefined;
|
|
@@ -296,6 +298,7 @@ function parsePlan(planText: string): ParsedPlan {
|
|
|
296
298
|
isWaived,
|
|
297
299
|
ownerTasks,
|
|
298
300
|
ownerMalformed,
|
|
301
|
+
isVerification,
|
|
299
302
|
anchor,
|
|
300
303
|
});
|
|
301
304
|
p++;
|
|
@@ -380,7 +383,7 @@ function computeRequiredLiteralsPerTask(parsed: ParsedPlan, specLines: string[])
|
|
|
380
383
|
return map;
|
|
381
384
|
}
|
|
382
385
|
|
|
383
|
-
function checkTableClosure(parsed: ParsedPlan): PlanCheckFinding[] {
|
|
386
|
+
function checkTableClosure(parsed: ParsedPlan, specLines: string[]): PlanCheckFinding[] {
|
|
384
387
|
const findings: PlanCheckFinding[] = [];
|
|
385
388
|
if (!parsed.coverageTableFound) {
|
|
386
389
|
findings.push({ check: "table-closure", line: 0, text: "", reason: "no '## Spec coverage' table found" });
|
|
@@ -396,34 +399,54 @@ function checkTableClosure(parsed: ParsedPlan): PlanCheckFinding[] {
|
|
|
396
399
|
check: "table-closure",
|
|
397
400
|
line: row.line,
|
|
398
401
|
text: row.text,
|
|
399
|
-
reason: "owner cell is not a 'Task <n>' list or 'waived: <reason>'",
|
|
402
|
+
reason: "owner cell is not a 'Task <n>' list, 'Verification', or 'waived: <reason>'",
|
|
403
|
+
});
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (row.isVerification && row.isMechanical) {
|
|
407
|
+
findings.push({
|
|
408
|
+
check: "table-closure",
|
|
409
|
+
line: row.line,
|
|
410
|
+
text: row.text,
|
|
411
|
+
reason: "mechanical row owner must be a Task <n>",
|
|
400
412
|
});
|
|
401
413
|
continue;
|
|
402
414
|
}
|
|
403
415
|
if (row.isWaived) continue;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
416
|
+
const anchorUnparseable = !row.isMechanical && !row.anchor;
|
|
417
|
+
if (anchorUnparseable) {
|
|
418
|
+
findings.push({
|
|
419
|
+
check: "table-closure",
|
|
420
|
+
line: row.line,
|
|
421
|
+
text: row.text,
|
|
422
|
+
reason: 'requirement row anchor is not a parseable § "heading" L<n>-L<n> anchor',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (row.isVerification) {
|
|
426
|
+
if (!anchorUnparseable && requiredLiteralsForRow(row, specLines).length === 0) {
|
|
409
427
|
findings.push({
|
|
410
428
|
check: "table-closure",
|
|
411
429
|
line: row.line,
|
|
412
430
|
text: row.text,
|
|
413
|
-
reason:
|
|
431
|
+
reason: "Verification row has no backtick literal to check against the header",
|
|
414
432
|
});
|
|
415
|
-
continue;
|
|
416
433
|
}
|
|
417
|
-
|
|
418
|
-
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
for (const n of row.ownerTasks) {
|
|
437
|
+
coveredTasks.add(n);
|
|
438
|
+
if (row.isMechanical) mechanicalCoveredTasks.add(n);
|
|
439
|
+
const task = taskByNumber.get(n);
|
|
440
|
+
if (!task) {
|
|
419
441
|
findings.push({
|
|
420
442
|
check: "table-closure",
|
|
421
443
|
line: row.line,
|
|
422
444
|
text: row.text,
|
|
423
|
-
reason:
|
|
445
|
+
reason: `row references Task ${n} but no such task exists`,
|
|
424
446
|
});
|
|
425
447
|
continue;
|
|
426
448
|
}
|
|
449
|
+
if (row.isMechanical || anchorUnparseable) continue;
|
|
427
450
|
const contained = task.anchors.some(
|
|
428
451
|
(a) => a.heading === row.anchor!.heading && a.start === row.anchor!.start && a.end === row.anchor!.end,
|
|
429
452
|
);
|
|
@@ -465,10 +488,24 @@ function checkQuoteIntegrity(parsed: ParsedPlan, specLines: string[]): PlanCheck
|
|
|
465
488
|
const findings: PlanCheckFinding[] = [];
|
|
466
489
|
if (!parsed.coverageTableFound) return findings;
|
|
467
490
|
const taskByNumber = new Map(parsed.tasks.map((t) => [t.number, t]));
|
|
491
|
+
const headerText = (parsed.header.verificationText ?? "").replaceAll("`", "");
|
|
468
492
|
for (const row of parsed.coverageRows) {
|
|
469
493
|
if (row.ownerMalformed || row.isWaived || row.isMechanical) continue;
|
|
470
494
|
const literals = requiredLiteralsForRow(row, specLines);
|
|
471
495
|
if (literals.length === 0) continue;
|
|
496
|
+
if (row.isVerification) {
|
|
497
|
+
for (const lit of literals) {
|
|
498
|
+
if (!headerText.includes(lit)) {
|
|
499
|
+
findings.push({
|
|
500
|
+
check: "quote-integrity",
|
|
501
|
+
line: row.line,
|
|
502
|
+
text: row.text,
|
|
503
|
+
reason: `verification header does not contain the required verbatim literal \`${lit}\``,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
472
509
|
for (const n of row.ownerTasks) {
|
|
473
510
|
const task = taskByNumber.get(n);
|
|
474
511
|
if (!task) continue;
|
|
@@ -805,7 +842,7 @@ export function checkPlan(planText: string, specText: string, fs: FsPort): PlanC
|
|
|
805
842
|
}
|
|
806
843
|
|
|
807
844
|
const specLines = specText.split("\n");
|
|
808
|
-
findings.push(...checkTableClosure(parsed));
|
|
845
|
+
findings.push(...checkTableClosure(parsed, specLines));
|
|
809
846
|
findings.push(...checkQuoteIntegrity(parsed, specLines));
|
|
810
847
|
findings.push(...checkAnchorResolution(parsed, specLines));
|
|
811
848
|
findings.push(...checkPathsExist(parsed, fs));
|
|
@@ -56,11 +56,12 @@ function harness(options: { cwd?: string; branch?: unknown[]; idle?: boolean; be
|
|
|
56
56
|
const tools: { name: string; execute: (...args: any[]) => unknown }[] = [];
|
|
57
57
|
const sent: { message: any; options: any }[] = [];
|
|
58
58
|
let idle = options.idle ?? true;
|
|
59
|
+
let branch = options.branch ?? [];
|
|
59
60
|
const ctx = {
|
|
60
61
|
cwd: options.cwd ?? tempCwd(),
|
|
61
62
|
hasUI: false,
|
|
62
63
|
isIdle: () => idle,
|
|
63
|
-
sessionManager: { getBranch: () =>
|
|
64
|
+
sessionManager: { getBranch: () => branch },
|
|
64
65
|
};
|
|
65
66
|
const pi = {
|
|
66
67
|
on(event: string, handler: (event: unknown, context: unknown) => unknown) {
|
|
@@ -68,7 +69,7 @@ function harness(options: { cwd?: string; branch?: unknown[]; idle?: boolean; be
|
|
|
68
69
|
registered.push(handler);
|
|
69
70
|
handlers.set(event, registered);
|
|
70
71
|
},
|
|
71
|
-
registerTool(tool: { name: string; execute: (...args: any[]) => unknown }) {
|
|
72
|
+
registerTool(tool: { name: string; executionMode?: string; execute: (...args: any[]) => unknown }) {
|
|
72
73
|
tools.push(tool);
|
|
73
74
|
},
|
|
74
75
|
sendMessage(message: unknown, sendOptions: unknown) {
|
|
@@ -86,7 +87,7 @@ function harness(options: { cwd?: string; branch?: unknown[]; idle?: boolean; be
|
|
|
86
87
|
for (const handler of handlers.get(name) ?? []) results.push(await handler(event, ctx));
|
|
87
88
|
return results;
|
|
88
89
|
};
|
|
89
|
-
return { emit, emitEvent, sent, tools, ctx, setIdle: (next: boolean) => (idle = next) };
|
|
90
|
+
return { emit, emitEvent, sent, tools, ctx, setIdle: (next: boolean) => (idle = next), setBranch: (next: unknown[]) => (branch = next) };
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
const settle = async (h: ReturnType<typeof harness>) => {
|
|
@@ -289,6 +290,163 @@ test("resumed session: closure gate blocks complete verify without a conformance
|
|
|
289
290
|
assert.equal(res.details.error, "no conformance-reviewer dispatch observed");
|
|
290
291
|
});
|
|
291
292
|
|
|
293
|
+
const taskSnapshot = (tasks: { name: string; status: string }[], isError = false) => ({
|
|
294
|
+
type: "message",
|
|
295
|
+
message: { role: "toolResult", toolName: "plan_tracker", isError, details: { tasks } },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const completePhase = async (h: ReturnType<typeof harness>, phase: "implement" | "verify") => {
|
|
299
|
+
const tool = h.tools.find((t) => t.name === "phase_tracker")!;
|
|
300
|
+
return (await tool.execute("complete", { action: "complete", phase }, undefined, undefined, h.ctx)) as {
|
|
301
|
+
content: { text: string }[];
|
|
302
|
+
details: { error?: string; phases: Record<Phase, { status: string }> };
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
test("phase_tracker registration requests sequential execution", () => {
|
|
307
|
+
const h = harness();
|
|
308
|
+
assert.equal(h.tools.find((t) => t.name === "phase_tracker")!.executionMode, "sequential");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("all-complete plan activity auto-completes an active implement phase", async () => {
|
|
312
|
+
const h = harness({ branch: implementBranch() });
|
|
313
|
+
await h.emit("session_start");
|
|
314
|
+
await h.emitEvent("tool_execution_end", {
|
|
315
|
+
toolName: "plan_tracker",
|
|
316
|
+
isError: false,
|
|
317
|
+
result: { details: { tasks: [{ status: "complete" }, { status: "complete" }] } },
|
|
318
|
+
});
|
|
319
|
+
const tool = h.tools.find((t) => t.name === "phase_tracker")!;
|
|
320
|
+
const status = (await tool.execute("t1", { action: "status" }, undefined, undefined, h.ctx)) as {
|
|
321
|
+
details: { phases: { implement: { status: string } } };
|
|
322
|
+
};
|
|
323
|
+
assert.equal(status.details.phases.implement.status, "complete");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("cold implement and verify completions ignore unfinished snapshots", async () => {
|
|
327
|
+
for (const phase of ["implement", "verify"] as const) {
|
|
328
|
+
const h = harness({
|
|
329
|
+
branch: [
|
|
330
|
+
phaseResult("start", phases({ [phase]: "in_progress" })),
|
|
331
|
+
taskSnapshot([{ name: "standalone task", status: "pending" }]),
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
await h.emit("session_start");
|
|
335
|
+
const completed = await completePhase(h, phase);
|
|
336
|
+
assert.equal(completed.details.error, undefined, phase);
|
|
337
|
+
assert.equal(completed.details.phases[phase].status, "complete", phase);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("completion backstop rejects unfinished snapshot indices, preserves state, and permits same-index retry", async () => {
|
|
342
|
+
const branch: unknown[] = [
|
|
343
|
+
...resumedBranch({ plan: "complete", implement: "complete", verify: "in_progress" }),
|
|
344
|
+
subagentResult(["conformance-reviewer"]),
|
|
345
|
+
taskSnapshot([
|
|
346
|
+
{ name: "T1 implementation", status: "complete" },
|
|
347
|
+
{ name: "G1 conformance", status: "pending" },
|
|
348
|
+
{ name: "G2 conformance", status: "in_progress" },
|
|
349
|
+
]),
|
|
350
|
+
];
|
|
351
|
+
const h = harness({ branch });
|
|
352
|
+
await h.emit("session_start");
|
|
353
|
+
const rejected = await completePhase(h, "verify");
|
|
354
|
+
assert.equal(rejected.details.error, "unfinished tasks");
|
|
355
|
+
assert.equal(rejected.details.phases.verify.status, "in_progress");
|
|
356
|
+
assert.match(rejected.content[0].text, /1: G1 conformance \(pending\)/);
|
|
357
|
+
assert.match(rejected.content[0].text, /2: G2 conformance \(in_progress\)/);
|
|
358
|
+
|
|
359
|
+
branch.push(taskSnapshot([
|
|
360
|
+
{ name: "T1 implementation", status: "complete" },
|
|
361
|
+
{ name: "G1 conformance", status: "complete" },
|
|
362
|
+
{ name: "G2 conformance", status: "complete" },
|
|
363
|
+
]));
|
|
364
|
+
const completed = await completePhase(h, "verify");
|
|
365
|
+
assert.equal(completed.details.error, undefined);
|
|
366
|
+
assert.equal(completed.details.phases.verify.status, "complete");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("completion backstop uses the latest successful current-branch snapshot", async () => {
|
|
370
|
+
const branch: unknown[] = [
|
|
371
|
+
...resumedBranch({ plan: "complete", implement: "in_progress" }),
|
|
372
|
+
taskSnapshot([{ name: "old", status: "pending" }]),
|
|
373
|
+
taskSnapshot([{ name: "errored", status: "pending" }], true),
|
|
374
|
+
];
|
|
375
|
+
const h = harness({ branch });
|
|
376
|
+
await h.emit("session_start");
|
|
377
|
+
const rejected = await completePhase(h, "implement");
|
|
378
|
+
assert.equal(rejected.details.error, "unfinished tasks");
|
|
379
|
+
assert.match(rejected.content[0].text, /0: old \(pending\)/);
|
|
380
|
+
|
|
381
|
+
branch.push(taskSnapshot([])); // clear/init supersedes the old snapshot
|
|
382
|
+
assert.equal((await completePhase(h, "implement")).details.phases.implement.status, "complete");
|
|
383
|
+
|
|
384
|
+
const resetOnlyBranch: unknown[] = [
|
|
385
|
+
...resumedBranch({ plan: "complete", implement: "in_progress" }),
|
|
386
|
+
taskSnapshot([{ name: "retained through reset", status: "in_progress" }]),
|
|
387
|
+
phaseResult("reset", phases()),
|
|
388
|
+
...resumedBranch({ plan: "complete", implement: "in_progress" }),
|
|
389
|
+
];
|
|
390
|
+
const resetOnly = harness({ branch: resetOnlyBranch });
|
|
391
|
+
await resetOnly.emit("session_start");
|
|
392
|
+
assert.equal((await completePhase(resetOnly, "implement")).details.error, "unfinished tasks");
|
|
393
|
+
resetOnlyBranch.push(taskSnapshot([{ name: "retained through reset", status: "complete" }]));
|
|
394
|
+
assert.equal((await completePhase(resetOnly, "implement")).details.phases.implement.status, "complete");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("completion backstop preserves exclusions and closure-error precedence", async () => {
|
|
398
|
+
const unfinished = taskSnapshot([{ name: "failed", status: "failed" }]);
|
|
399
|
+
const explicitImplement = harness({ branch: [...resumedBranch({ plan: "complete", implement: "in_progress" }), unfinished] });
|
|
400
|
+
await explicitImplement.emit("session_start");
|
|
401
|
+
assert.equal((await completePhase(explicitImplement, "implement")).details.phases.implement.status, "complete");
|
|
402
|
+
|
|
403
|
+
const closureFirst = harness({
|
|
404
|
+
branch: [...resumedBranch({ plan: "complete", implement: "complete", verify: "in_progress" }), taskSnapshot([{ name: "T1", status: "pending" }])],
|
|
405
|
+
});
|
|
406
|
+
await closureFirst.emit("session_start");
|
|
407
|
+
assert.equal((await completePhase(closureFirst, "verify")).details.error, "no conformance-reviewer dispatch observed");
|
|
408
|
+
|
|
409
|
+
const disabled = harness({
|
|
410
|
+
cwd: tempCwd({ piGauntlet: { flowGuards: { enforce: false } } }),
|
|
411
|
+
branch: [...resumedBranch({ plan: "complete", implement: "in_progress" }), taskSnapshot([{ name: "T1", status: "pending" }])],
|
|
412
|
+
});
|
|
413
|
+
await disabled.emit("session_start");
|
|
414
|
+
assert.equal((await completePhase(disabled, "implement")).details.phases.implement.status, "complete");
|
|
415
|
+
|
|
416
|
+
const adHoc = harness({ branch: [taskSnapshot([{ name: "T1", status: "pending" }])] });
|
|
417
|
+
await adHoc.emit("session_start");
|
|
418
|
+
const tool = adHoc.tools.find((t) => t.name === "phase_tracker")!;
|
|
419
|
+
assert.equal((await tool.execute("x", { action: "complete", phase: "plan" }, undefined, undefined, adHoc.ctx)).details.error, undefined);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("completion backstop leaves no/empty snapshots and skip alone, and uses the switched branch", async () => {
|
|
423
|
+
const noSnapshot = harness({ branch: resumedBranch({ plan: "complete", implement: "in_progress" }) });
|
|
424
|
+
await noSnapshot.emit("session_start");
|
|
425
|
+
assert.equal((await completePhase(noSnapshot, "implement")).details.phases.implement.status, "complete");
|
|
426
|
+
|
|
427
|
+
const skipped = harness({
|
|
428
|
+
branch: [...resumedBranch({ plan: "complete", implement: "in_progress" }), taskSnapshot([{ name: "T1", status: "pending" }])],
|
|
429
|
+
});
|
|
430
|
+
await skipped.emit("session_start");
|
|
431
|
+
const skipTool = skipped.tools.find((t) => t.name === "phase_tracker")!;
|
|
432
|
+
assert.equal((await skipTool.execute("skip", { action: "skip", phase: "implement", reason: "waived" }, undefined, undefined, skipped.ctx)).details.error, undefined);
|
|
433
|
+
|
|
434
|
+
const switched = harness({
|
|
435
|
+
branch: [...resumedBranch({ plan: "complete", implement: "in_progress" }), taskSnapshot([{ name: "off branch", status: "pending" }])],
|
|
436
|
+
});
|
|
437
|
+
await switched.emit("session_start");
|
|
438
|
+
switched.setBranch([...resumedBranch({ plan: "complete", implement: "in_progress" }), taskSnapshot([{ name: "active", status: "complete" }])]);
|
|
439
|
+
await switched.emit("session_switch");
|
|
440
|
+
assert.equal((await completePhase(switched, "implement")).details.phases.implement.status, "complete");
|
|
441
|
+
|
|
442
|
+
const closureOff = harness({
|
|
443
|
+
cwd: tempCwd({ piGauntlet: { closureReview: { enforce: false } } }),
|
|
444
|
+
branch: [...resumedBranch({ plan: "complete", implement: "complete", verify: "in_progress" }), taskSnapshot([{ name: "T1", status: "pending" }])],
|
|
445
|
+
});
|
|
446
|
+
await closureOff.emit("session_start");
|
|
447
|
+
assert.equal((await completePhase(closureOff, "verify")).details.error, "unfinished tasks");
|
|
448
|
+
});
|
|
449
|
+
|
|
292
450
|
const subagentResult = (agents: string[]) => ({
|
|
293
451
|
type: "message",
|
|
294
452
|
message: {
|
|
@@ -774,6 +774,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
774
774
|
"Track workflow phase progress (brainstorm → plan → implement → verify → ship); " +
|
|
775
775
|
"ad-hoc calls do not arm gates. Not for ad-hoc use.",
|
|
776
776
|
parameters: PhaseTrackerParams,
|
|
777
|
+
executionMode: "sequential",
|
|
777
778
|
|
|
778
779
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
779
780
|
switch (params.action) {
|
|
@@ -891,6 +892,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
891
892
|
} as PhaseTrackerDetails,
|
|
892
893
|
};
|
|
893
894
|
}
|
|
895
|
+
if (
|
|
896
|
+
gauntletEntered &&
|
|
897
|
+
(params.phase === "implement" || params.phase === "verify") &&
|
|
898
|
+
resolveFlowGuards(loadGauntletSettings(ctx.cwd).gauntlet).enforce
|
|
899
|
+
) {
|
|
900
|
+
let tasks: { name: string; status: string }[] = [];
|
|
901
|
+
for (const entry of [...ctx.sessionManager.getBranch()].reverse()) {
|
|
902
|
+
if (
|
|
903
|
+
entry.type !== "message" ||
|
|
904
|
+
entry.message.role !== "toolResult" ||
|
|
905
|
+
entry.message.toolName !== "plan_tracker" ||
|
|
906
|
+
entry.message.isError
|
|
907
|
+
) {
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const details = entry.message.details as { tasks?: { name: string; status: string }[]; error?: string } | undefined;
|
|
911
|
+
if (!details || details.error || !details.tasks) continue;
|
|
912
|
+
tasks = details.tasks;
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
const unfinished = tasks.flatMap((task, index) =>
|
|
916
|
+
task.status === "pending" || task.status === "in_progress"
|
|
917
|
+
? [`${index}: ${task.name} (${task.status})`]
|
|
918
|
+
: [],
|
|
919
|
+
);
|
|
920
|
+
if (unfinished.length) {
|
|
921
|
+
return {
|
|
922
|
+
content: [
|
|
923
|
+
{
|
|
924
|
+
type: "text",
|
|
925
|
+
text: `Cannot complete ${params.phase}: unfinished tasks:\n${unfinished.join("\n")}\nReconcile these same indices against acceptance evidence, update them and retry.`,
|
|
926
|
+
},
|
|
927
|
+
],
|
|
928
|
+
details: { action: "complete", phases: { ...phases }, error: "unfinished tasks" } as PhaseTrackerDetails,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
894
932
|
phases = { ...phases, [params.phase]: transitionPhaseState("complete") as PhaseState };
|
|
895
933
|
firedGuards.clear();
|
|
896
934
|
updateWidget(ctx);
|
package/package.json
CHANGED
|
@@ -36,6 +36,12 @@ The line: exercising the system **as it is today** is research; exercising the *
|
|
|
36
36
|
|
|
37
37
|
This skill ends with a **written, user-reviewed spec inside a worktree**. Nothing else.
|
|
38
38
|
|
|
39
|
+
## Foreground dispatch policy
|
|
40
|
+
|
|
41
|
+
Flow-owned execution dispatches run in the foreground: set top-level `async: false` on gather, critique, council, summary, implementation, review, conformance, and retry calls. `forceTopLevelAsync` must remain unset or false; it is incompatible with this flow. See [pi-cohort dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md). If a dispatch returns an async handle despite `async: false`, stop and report the configuration error: do not poll it, relaunch work, or advance the flow. An intercom-detached child is likewise incomplete work; use the existing coordination path and never accept or duplicate it.
|
|
42
|
+
|
|
43
|
+
Foreground does not serialize independent work: preserve existing isolated parallel `tasks` batches and await their terminal results before acceptance or tracker/phase advancement.
|
|
44
|
+
|
|
39
45
|
## Checklist
|
|
40
46
|
|
|
41
47
|
Work through the items below **in order**. This is your own checklist to follow, not a `plan_tracker` plan — brainstorming is open-ended exploration, and `plan_tracker` is execution-only (the implement phase). The terminal state is the user review gate; after approval the **only** next skill is `/skill:writing-plans`. Do not jump to implementation, and do not silently drop the critique pass.
|
|
@@ -280,7 +286,7 @@ The first three checks — **placeholder scan**, **internal consistency**, and *
|
|
|
280
286
|
- **Otherwise** → dispatch one fresh `worker` that applies the scope + ambiguity checks and fixes them in place:
|
|
281
287
|
|
|
282
288
|
```
|
|
283
|
-
subagent({ agent: "worker", context: "fresh", cwd: "<abs worktree path, from git rev-parse --show-toplevel>", task:
|
|
289
|
+
subagent({ agent: "worker", context: "fresh", async: false, cwd: "<abs worktree path, from git rev-parse --show-toplevel>", task:
|
|
284
290
|
"Problem statement: <the problem the spec addresses + the user's stated intent>.\n" +
|
|
285
291
|
"Read the spec at <abs path to doc/specs/...>. Edit ONLY that file. Apply two checks and\n" +
|
|
286
292
|
"fix what you find in place: (1) Scope — does every paragraph serve the goal? Cut filler;\n" +
|
|
@@ -311,7 +317,7 @@ SUMMARY_PATH=$(mktemp "${TMPDIR:-/tmp}/gauntlet-spec-summary.XXXXXX") # absolu
|
|
|
311
317
|
```
|
|
312
318
|
|
|
313
319
|
```
|
|
314
|
-
subagent({ agent: "spec-summarizer", context: "fresh", cwd: "<abs worktree path, from git rev-parse --show-toplevel>",
|
|
320
|
+
subagent({ agent: "spec-summarizer", context: "fresh", async: false, cwd: "<abs worktree path, from git rev-parse --show-toplevel>",
|
|
315
321
|
output: "<SUMMARY_PATH>", outputMode: "file-only", task:
|
|
316
322
|
"Summarize the spec at <abs path to doc/specs/...> for the user review gate. Read ONLY that file." })
|
|
317
323
|
```
|
|
@@ -12,12 +12,14 @@ Mint a temp dir outside the worktree (never committed):
|
|
|
12
12
|
GATHER_DIR=$(mktemp -d)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Set the substep, then dispatch one parallel-tasks `subagent` call (
|
|
16
|
-
`
|
|
15
|
+
Set the substep, then dispatch one foreground parallel-tasks `subagent` call (`async: false`; no
|
|
16
|
+
`model:` — pi-cohort `agentOverrides` owns builder models). Preserve the parallel batch and await
|
|
17
|
+
its terminal result before assembling the draft:
|
|
17
18
|
|
|
18
19
|
```
|
|
19
20
|
phase_tracker({ action: "substep", phase: "brainstorm", substep: "gather" })
|
|
20
21
|
subagent({
|
|
22
|
+
async: false,
|
|
21
23
|
tasks: [
|
|
22
24
|
{ agent: "scout", cwd: "<abs worktree path>", phase: "context-gather",
|
|
23
25
|
output: "<GATHER_DIR>/scout.md",
|
|
@@ -15,6 +15,8 @@ When you have multiple **independent tasks** — unrelated test failures, or imp
|
|
|
15
15
|
|
|
16
16
|
This skill is the **mechanic home** for parallel fan-out: fresh-context isolation, `worktree: true` filesystem isolation, and serial patch integration. `subagent-driven-development`'s Parallel-Wave Mode builds its per-wave dispatch on this skill — debugging is the worked example below, but the mechanics are identical for implementation tasks.
|
|
17
17
|
|
|
18
|
+
Gauntlet flow callers dispatch foreground: explicitly set top-level `async: false`, including retries and prose-described dispatches. `forceTopLevelAsync` is incompatible; if an async handle is returned anyway, stop and report rather than polling or relaunching. See [pi-cohort dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md).
|
|
19
|
+
|
|
18
20
|
**Why parallel subagents:** each agent gets a fresh context window with only its problem domain. No cross-contamination between investigations, smaller diffs, faster wall-clock time. You stay the orchestrator — you read the summaries, resolve any file overlap, and run the integrated tests.
|
|
19
21
|
|
|
20
22
|
**Fresh context is not the default.** Some packaged subagents (including `worker`) fork the parent context unless you opt out. Always pass `context: "fresh"` on every task entry — if it's missing, you're getting forked agents and losing the isolation that makes parallel dispatch worth doing in the first place.
|
|
@@ -78,6 +80,7 @@ Use the `subagent` tool in parallel mode, with explicit fresh context per task:
|
|
|
78
80
|
```ts
|
|
79
81
|
subagent({
|
|
80
82
|
context: "fresh",
|
|
83
|
+
async: false,
|
|
81
84
|
tasks: [
|
|
82
85
|
{ agent: "worker", task: "Fix agent-tool-abort.test.ts failures" },
|
|
83
86
|
{ agent: "worker", task: "Fix batch-completion-behavior.test.ts failures" },
|
|
@@ -123,7 +126,7 @@ Grammar (identical across producers, modulo id prefix — `F` for code/spec revi
|
|
|
123
126
|
|
|
124
127
|
**After the fix wave:** integrate patches serially per "Review and Integrate" above (mis-partition is self-healing: integrate the successes, re-run the conflicting finding sequentially on integrated HEAD); run the consuming loop's scoped test gate on the integrated tree; then one re-review of the integrated fix delta, per the consuming loop's own rules. The fan-out counts as one fix round against the consuming loop's budget — it grants no extra rounds.
|
|
125
128
|
|
|
126
|
-
**Progress:** `plan_tracker({ action: "add" })` one task per fixed finding, named mechanically — `"<prefix>fix F<n>: <finding's first line verbatim>"
|
|
129
|
+
**Progress:** In a gauntlet flow, fix work reuses the consuming task/wave index (or durable `Gn:` gap index) and preserves it through retries, fan-out, tests, and re-review; do not append a fix task or replace an active flow list. Mark the existing index `in_progress` before dispatch and `complete` only after its consuming acceptance point. For standalone non-flow consumers with no owning task, preserve the generic append behavior: `plan_tracker({ action: "add" })` one task per fixed finding, named mechanically — `"<prefix>fix F<n>: <finding's first line verbatim>"`; mark it `in_progress` at dispatch and `complete` at integration.
|
|
127
130
|
|
|
128
131
|
## Agent Prompt Structure
|
|
129
132
|
|
|
@@ -192,6 +195,7 @@ REPORT_DIR=$(mktemp -d)
|
|
|
192
195
|
```ts
|
|
193
196
|
subagent({
|
|
194
197
|
context: "fresh",
|
|
198
|
+
async: false,
|
|
195
199
|
worktree: true, // isolate edits; omit for read-only investigations
|
|
196
200
|
concurrency: 3,
|
|
197
201
|
tasks: [
|
|
@@ -37,10 +37,10 @@ Fill the template at `code-reviewer.md` in this skill directory, then dispatch a
|
|
|
37
37
|
|
|
38
38
|
**How to dispatch:**
|
|
39
39
|
|
|
40
|
-
Use the `subagent` tool with the code-reviewer template filled in:
|
|
40
|
+
Use the `subagent` tool with the code-reviewer template filled in. This foreground dispatch must return a terminal result before acting on review feedback:
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
|
-
subagent({ agent: "code-reviewer", task: "... filled template ..." })
|
|
43
|
+
subagent({ agent: "code-reviewer", async: false, task: "... filled template ..." })
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
**Placeholders:**
|
|
@@ -57,7 +57,7 @@ subagent({ agent: "code-reviewer", task: "... filled template ..." })
|
|
|
57
57
|
- Note Minor issues for later
|
|
58
58
|
- Push back if reviewer is wrong (with reasoning)
|
|
59
59
|
|
|
60
|
-
**Fix rounds.** Critical and Moderate findings trigger a fix round; when dispatched from an orchestrating skill, fixes go to `implementer` subagents (per the orchestrator's no-self-coding rule), fanned out per `dispatching-parallel-agents` "Fix fan-out" when the review's `Parallel-safe:` line certifies a `disjoint` group of ≥ 2 findings. Before fanning out, validate the review's `Parallel-safe:` line with the structural probe in `dispatching-parallel-agents` § Fix fan-out (exactly-one-line grammar check, one re-ask, then explicit sequential fallback). After integration and the project's test command, re-dispatch the reviewer once on the integrated delta. If Critical or Moderate findings remain, run one more fix round and one more re-review; still failing → escalate to the user. Minor findings never trigger the fan-out.
|
|
60
|
+
**Fix rounds.** Critical and Moderate findings trigger a fix round; when dispatched from an orchestrating skill, fixes go to `implementer` subagents (per the orchestrator's no-self-coding rule), fanned out per `dispatching-parallel-agents` "Fix fan-out" when the review's `Parallel-safe:` line certifies a `disjoint` group of ≥ 2 findings. Before fanning out, validate the review's `Parallel-safe:` line with the structural probe in `dispatching-parallel-agents` § Fix fan-out (exactly-one-line grammar check, one re-ask, then explicit sequential fallback). After integration and the project's test command, re-dispatch the reviewer once on the integrated delta in the foreground with top-level `async: false`; await its terminal result. If Critical or Moderate findings remain, run one more fix round and one more foreground re-review; still failing → escalate to the user. Minor findings never trigger the fan-out.
|
|
61
61
|
|
|
62
62
|
## Example
|
|
63
63
|
|
|
@@ -58,6 +58,7 @@ Capture the worktree path once (`git rev-parse --show-toplevel`, run from inside
|
|
|
58
58
|
|
|
59
59
|
```
|
|
60
60
|
subagent({
|
|
61
|
+
async: false,
|
|
61
62
|
control: { needsAttentionAfterMs: 300000, inFlightSilenceCeilingMs: 300000, inFlightSilenceKillMs: 600000 },
|
|
62
63
|
tasks: members.map((model, i) => ({
|
|
63
64
|
agent: "spec-council-member",
|
|
@@ -76,7 +77,7 @@ subagent({
|
|
|
76
77
|
|
|
77
78
|
**Usable-critique test (mechanical structural probe).** After the fanout returns - success or failure of the tool call itself - probe the expected output paths on disk; judge by files, not by the tool result's failed/succeeded labels (a killed member may have written a usable critique first). A member file is usable iff it is non-empty AND contains both a `^verdict:\s*(sound|needs-work|unsound)` line and an `^addresses-problem:` line. A `findings:` header with zero bullets is a valid, usable sound critique. Existence plus header regex only - never read or weigh findings content.
|
|
78
79
|
|
|
79
|
-
**Targeted retry.** Members whose file is missing or not usable are re-dispatched **once**, together, in a second parallel call carrying the same `control` block, with fresh output paths that preserve the `member-<i>-<slug>` basename under a `retry/` subdir of the same temp dir (the chair recovers `raised-by` attribution from that filename pattern). Members with usable files are never re-run.
|
|
80
|
+
**Targeted retry.** Members whose file is missing or not usable are re-dispatched **once**, together, in a second foreground parallel call carrying `async: false` and the same `control` block, with fresh output paths that preserve the `member-<i>-<slug>` basename under a `retry/` subdir of the same temp dir (the chair recovers `raised-by` attribution from that filename pattern). Await its terminal result. Members with usable files are never re-run.
|
|
80
81
|
|
|
81
82
|
**Quorum.** At least one usable file after retry -> dispatch the chair over the usable files only (next section). Zero usable files -> abort the council, say so, and return to the user gate.
|
|
82
83
|
|
|
@@ -87,6 +88,7 @@ Dispatch the chair once. It reads the member files (not you), the spec, and the
|
|
|
87
88
|
```
|
|
88
89
|
subagent({
|
|
89
90
|
agent: "spec-council-synthesizer",
|
|
91
|
+
async: false,
|
|
90
92
|
model: <chair from config, else omit to inherit>,
|
|
91
93
|
cwd: "<abs worktree path>",
|
|
92
94
|
control: { needsAttentionAfterMs: 300000, inFlightSilenceCeilingMs: 600000, inFlightSilenceKillMs: 900000 },
|
|
@@ -99,11 +101,11 @@ subagent({
|
|
|
99
101
|
})
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
The chair runs one long single-turn synthesis;
|
|
104
|
+
The chair runs one long foreground single-turn synthesis; await its terminal result before applying findings. The control block sets an effective silence-kill of max(900s, 600+300) = 900s. Margin rationale: one observed healthy chair turn ran 506s of silence, so a 600s kill would leave under 2 minutes of margin - the chair gets 900s. In the coverage line, use pi-cohort's kill diagnostic as the reason when present (e.g. "Likely wedged in a tool call"), else "no output produced"; omit per-member reasons at full coverage. With one usable member, use singular wording ("synthesize the single member critique").
|
|
103
105
|
|
|
104
106
|
List the exact member paths in the task text. The `reads:` array injects their contents, but the chair's prompt expects the paths explicitly; without them it scans the tree for `*.md` and stalls.
|
|
105
107
|
|
|
106
|
-
A chair synthesis is usable iff it contains a `^consensus:` line. If the configured `chair` model is unreachable, retry once with the inherited model; a wedge-killed or unusable chair retries once with the same model. Second failure -> abort the council, say so, and return to the user gate.
|
|
108
|
+
A chair synthesis is usable iff it contains a `^consensus:` line. If the configured `chair` model is unreachable, retry once with the inherited model; a wedge-killed or unusable chair retries once with the same model. Each retry remains foreground with top-level `async: false` and is awaited to a terminal result. Second failure -> abort the council, say so, and return to the user gate.
|
|
107
109
|
|
|
108
110
|
### 3 — Decide and apply
|
|
109
111
|
|
|
@@ -13,6 +13,8 @@ Execute a plan by dispatching a **fresh subagent per task**, with two-stage revi
|
|
|
13
13
|
|
|
14
14
|
If a tool result contains a ⚠️ workflow warning, stop immediately and address it before continuing.
|
|
15
15
|
|
|
16
|
+
Gauntlet execution dispatches are foreground: set top-level `async: false` on every implementation, review, conformance, retry, and prose-described dispatch. Preserve parallel `tasks` batches and chain groups. `forceTopLevelAsync` is incompatible; an unexpected async handle means stop and report, never poll or relaunch. See [pi-cohort dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md).
|
|
17
|
+
|
|
16
18
|
## Why Subagents
|
|
17
19
|
|
|
18
20
|
Your context window holds the full plan, prior decisions, and conversation history. Each subagent gets a fresh window with only the current task's text and context.
|
|
@@ -39,7 +41,7 @@ Periodic "should I continue?" prompts add latency without adding safety. The pla
|
|
|
39
41
|
|
|
40
42
|
- Running inside a dedicated worktree (the same one the spec and plan were authored in). If you're on `main` in the primary checkout, stop and invoke `/skill:using-git-worktrees` first — implementation never lands directly on `main`.
|
|
41
43
|
- Approved plan or clear task scope
|
|
42
|
-
- `plan_tracker` initialized with the full task list
|
|
44
|
+
- `plan_tracker` list initialized at writing-plans handoff with the full wave-ordered task list. Consume and preserve it on continuation; only direct recovery with no tracker initializes the full plan list once, before dispatch, and never over an existing list.
|
|
43
45
|
|
|
44
46
|
## Sequential vs. Parallel-Wave
|
|
45
47
|
|
|
@@ -53,17 +55,17 @@ Before the first task, enter the implement phase: `phase_tracker({ action: "star
|
|
|
53
55
|
|
|
54
56
|
For each task in `plan_tracker`:
|
|
55
57
|
|
|
56
|
-
1. **
|
|
58
|
+
1. **Start, then dispatch implementer.** Mark the task's existing `plan_tracker` index `in_progress` before dispatch. Pass the full task text + scene-setting context + the task's plan-declared test commands as `SCOPED_TEST_COMMANDS` (or `none`). Don't make the subagent re-read the plan.
|
|
57
59
|
2. **Handle implementer status** (see below).
|
|
58
60
|
3. **Dispatch spec reviewer.** Pass the task text, the patch diff, the absolute spec path, and the task's `**Spec:**` anchors — SR reads the anchored ranges from the spec file itself; never inline spec excerpts. The spec wins every dispute; the authority hierarchy and finding labels live in `./spec-reviewer-prompt.md`. Anchor-less tasks (no `**Spec:**` line): task text alone is the contract. Verify the change satisfies the anchored spec — nothing missing, nothing extra.
|
|
59
61
|
4. If spec reviewer finds gaps → re-dispatch implementer to fix → re-review. Loop until ✅, within [Fix-Loop Rounds](#fix-loop-rounds).
|
|
60
62
|
5. **Dispatch code-quality reviewer.** Only after spec is ✅. Skip for doc-only tasks (every file in the task's `Files:` block documentation-only) — SR-only, same exemption as doc-only waves. Pass `SCOPED_TEST_COMMANDS` = the task's plan-declared commands.
|
|
61
63
|
6. If quality reviewer finds issues → re-dispatch implementer → re-review. Loop until ✅, within [Fix-Loop Rounds](#fix-loop-rounds).
|
|
62
|
-
7.
|
|
64
|
+
7. After its existing reviews accept the work (and its required commit point), mark that same task index `complete` in `plan_tracker`. A subagent exit or green tests alone are not acceptance.
|
|
63
65
|
|
|
64
66
|
The spec is frozen at plan time and the orchestrator is its only writer during execution; if you do edit it mid-run, re-run writing-plans' anchor-resolution check before the next wave. An SR unable to read the spec at a cited anchor (missing file, unresolvable heading/range) returns a blocking finding — the contract is spec+task or stop, never a silent fallback to task-only review.
|
|
65
67
|
|
|
66
|
-
After all tasks: proceed to [After All Tasks](#after-all-tasks-complete) - it owns
|
|
68
|
+
After all tasks: proceed to [After All Tasks](#after-all-tasks-complete) - it owns parent full verification, then whole-diff code review, then conformance.
|
|
67
69
|
|
|
68
70
|
## Fix-Loop Rounds
|
|
69
71
|
|
|
@@ -121,6 +123,7 @@ Pi-subagents accepts a per-task `model` override. Use it.
|
|
|
121
123
|
```ts
|
|
122
124
|
subagent({
|
|
123
125
|
agent: "implementer",
|
|
126
|
+
async: false,
|
|
124
127
|
task: "...",
|
|
125
128
|
model: "anthropic/claude-haiku-4" // cheap tier
|
|
126
129
|
})
|
|
@@ -132,18 +135,18 @@ When in doubt, default. Don't downgrade reviewers — false negatives are expens
|
|
|
132
135
|
|
|
133
136
|
```ts
|
|
134
137
|
// implementer
|
|
135
|
-
subagent({ agent: "implementer", task: "<task text + context + SCOPED_TEST_COMMANDS + status protocol>" })
|
|
138
|
+
subagent({ agent: "implementer", async: false, task: "<task text + context + SCOPED_TEST_COMMANDS + status protocol>" })
|
|
136
139
|
|
|
137
140
|
// spec compliance
|
|
138
|
-
subagent({ agent: "spec-reviewer", task: "<task text + patch diff + absolute spec path + task's Spec: anchors>" })
|
|
141
|
+
subagent({ agent: "spec-reviewer", async: false, task: "<task text + patch diff + absolute spec path + task's Spec: anchors>" })
|
|
139
142
|
|
|
140
143
|
// code quality
|
|
141
|
-
subagent({ agent: "code-reviewer", task: "<diff range + SCOPED_TEST_COMMANDS (task commands; wave: union; whole-diff: none) + ask: production-ready?>" })
|
|
144
|
+
subagent({ agent: "code-reviewer", async: false, task: "<diff range + SCOPED_TEST_COMMANDS (task commands; wave: union; whole-diff: none) + ask: production-ready?>" })
|
|
142
145
|
|
|
143
146
|
// closing-loop conformance (origin vs deliverable) — its OWN dispatch, never fused with code quality
|
|
144
147
|
// model: call gauntlet_setting({ key: "closureReview" }) first; use the returned model (omit model: if undefined to inherit) and maxFixRounds
|
|
145
148
|
// If gauntlet_setting is unavailable, stop and report - never fall back to a manual bash/JSON settings merge.
|
|
146
|
-
subagent({ agent: "conformance-reviewer", model: /* gauntlet_setting({ key: "closureReview" }).model, else omit to inherit */, task: "<spec path + verbatim original prompt + full diff vs main; per conformance-check.md>" })
|
|
149
|
+
subagent({ agent: "conformance-reviewer", async: false, model: /* gauntlet_setting({ key: "closureReview" }).model, else omit to inherit */, task: "<spec path + verbatim original prompt + full diff vs main; per conformance-check.md>" })
|
|
147
150
|
```
|
|
148
151
|
|
|
149
152
|
Prompt templates live alongside this SKILL.md:
|
|
@@ -160,22 +163,22 @@ Auto-selected at handoff by `writing-plans` (any wave with ≥2 tasks) when the
|
|
|
160
163
|
|
|
161
164
|
**Progress tracking (`plan_tracker`).** `plan_tracker` is a flat list with no native group concept, so waves are *encoded*, not modeled:
|
|
162
165
|
|
|
163
|
-
- **
|
|
164
|
-
- **Wave fan-out → `in_progress`:** mark every task index in the wave `in_progress
|
|
165
|
-
- **Wave commit → `complete`:** after the wave's gate passes and it commits, mark all
|
|
166
|
+
- **Consume, preserve, recover only if absent:** consume the wave-ordered list initialized at writing-plans handoff; indices are positional and stable, so never re-init on continuation or mid-run. Only direct recovery with no tracker initializes the full plan list once before dispatch.
|
|
167
|
+
- **Wave fan-out → `in_progress`:** unconditionally mark every task index in the wave `in_progress` before dispatch. Multiple simultaneous entries are expected (sequential mode has one).
|
|
168
|
+
- **Wave commit → `complete`:** after the wave's gate passes and it commits, unconditionally mark all those same indices `complete`. `complete` = durably committed, so a task in conflict fallback stays `in_progress` until its wave commits.
|
|
166
169
|
- **Lifecycle per task:** `pending → in_progress (wave fan-out) → complete (wave commit)`.
|
|
167
170
|
- **Widget caveat (known, deliberately unfixed).** The persistent `plan_tracker` widget's icon strip (`○ → ✓`) and `(c/total)` count reflect every task, but its trailing *name* shows only the **first** `in_progress` task. In parallel mode the icon strip and the `status` action are the full in-flight view; a richer multi-task widget is a separate extension change, out of scope (YAGNI).
|
|
168
|
-
- **Sequential mode
|
|
171
|
+
- **Sequential mode:** consume the same existing full list, one `in_progress` index at a time; wave prefixes are harmless.
|
|
169
172
|
|
|
170
173
|
**Per-wave loop:**
|
|
171
174
|
|
|
172
175
|
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.
|
|
173
|
-
2. **
|
|
176
|
+
2. **Start, then fan out.** Mark every wave index `in_progress` before one parallel foreground dispatch (shape below): `implementer` per task, `context: "fresh"`, `worktree: true`. Each returns a status + a patch.
|
|
174
177
|
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**, the absolute spec path, and the task's `**Spec:**` anchors — SR reads the anchored ranges itself (never inline excerpts; authority hierarchy in `./spec-reviewer-prompt.md`; anchor-less tasks are task-text-only). Review starts from the diff (its hunks carry `file:line`) and reads each touched file in full, and test execution is never the reviewer's job - in either mode (persona rule; the wave test gate in step 5 runs the wave's declared test commands). 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 ✅, within [Fix-Loop Rounds](#fix-loop-rounds), same as sequential.
|
|
175
178
|
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.
|
|
176
179
|
5. **Test gate.** Run the union of the wave's tasks' declared test commands on the integrated tree — the full verification set is the verify phase's job, run once. Failure = semantic conflict or bug → re-run the offending task sequentially, else fix per [When a Subagent Fails](#when-a-subagent-fails).
|
|
177
180
|
6. **Quality review.** CR binds to the wave: exactly one **initial** code-review dispatch per code-touching wave, over the integrated wave diff - never per task within a wave, never batched across waves. Subsequent dispatches within the wave are re-reviews triggered only by findings, per Fix-Loop Rounds. Pass `SCOPED_TEST_COMMANDS` = the union of the wave's tasks' declared commands. Code-quality review on the integrated wave diff; loop fixes to ✅ within [Fix-Loop Rounds](#fix-loop-rounds), same as sequential. Skip for doc-only waves (SR-only per the commit precondition below).
|
|
178
|
-
7. **Commit the wave.** Leaves a clean tree; the next wave's children branch from this commit and so see the integrated work.
|
|
181
|
+
7. **Commit and complete the wave.** After the gate passes and the wave commits, mark all of its existing indices `complete`. Leaves a clean tree; the next wave's children branch from this commit and so see the integrated work.
|
|
179
182
|
|
|
180
183
|
**Two-stage review is preserved:** spec review per task (pre-integration, dispatched `spec-reviewer` — not inline), quality review per wave (post-integration). A wave commit requires one spec-review verdict per accepted task, plus one code-review verdict on the integrated diff for waves that touch code. A doc-only wave (every task's `Files:` block documentation-only, per `writing-plans`' Wave Grouping) is SR-only — the CR gate does not apply.
|
|
181
184
|
|
|
@@ -192,6 +195,7 @@ REPORT_DIR=$(mktemp -d)
|
|
|
192
195
|
```ts
|
|
193
196
|
subagent({
|
|
194
197
|
context: "fresh",
|
|
198
|
+
async: false,
|
|
195
199
|
cwd: "/abs/path/to/this/worktree", // REQUIRED: the worktree you're in, else children branch from main
|
|
196
200
|
worktree: true, // each task in its own git worktree, branched from cwd's HEAD
|
|
197
201
|
concurrency: 4, // default; cap = wave size
|
|
@@ -223,11 +227,9 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
|
|
|
223
227
|
## After All Tasks Complete
|
|
224
228
|
|
|
225
229
|
0. Call `phase_tracker({ action: "start", phase: "verify" })`. (The `implement` phase was started at execution start and auto-completes from `plan_tracker` once all tasks are done; this flow runs its own verify gate instead of `/skill:verification-before-completion`, so it must mark verify itself.)
|
|
226
|
-
1. **
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
2. **Join, then disposition.** Never busy-wait: after verification returns, check the review at most once via `subagent({ action: "status", id: <run id> })`; if still running, end the turn with no disposition — pi delivers the async completion, and the review output file is read only after terminal completion. If the async dispatch errored or the child died, re-dispatch the review serially (the verification result is already in hand). **No disposition of either result — no fix dispatch, no finding triage, no verify-complete claim, no `conformance-reviewer` dispatch — before both results are in hand and any fix-triggered re-run is green.** Post-join, address Critical and Moderate findings before handoff. Verification failed, review clean → dispatch fixes, then re-run the full set before any subsequent gate. Both audits dirty → strictly ordered, never merged: the review's certified `Parallel-safe:` fan-out first (when present, else sequential fixes), then the remaining verification failures sequentially, then one re-review and one full verification re-run — verification failures never join a `Parallel-safe:` group (they carry no finding IDs and no disjointness certification, and `dispatching-parallel-agents` forbids orchestrator-invented partitions). Both audits bind to the committed HEAD: any post-join fix commit — review-derived or conformance-derived — invalidates the verification result; re-run the full set before re-dispatching any gate. (Consumers wanting an in-flow project-specific audit re-add it via the gauntlet overrides file (see Project overrides), or run `/self-audit` manually.)
|
|
230
|
-
3. **Close the loop — conformance check.** The review in step 1 is plan-vs-code (single-step); it inherits any requirement the plan already dropped. Before marking verify complete, dispatch a fresh-context **`conformance-reviewer`** — its **own** dispatch, never fused into the step-1 review — to confront the deliverable (code **and** docs) against the *origin* — the spec **and** the original prompt — per `verification-before-completion/reference/conformance-check.md`. Pass the spec path, the verbatim original prompt, and the full diff. Follow that reference for the partition rule, concern decomposition, and fix-loop mechanics; do not reimplement them here. The fix loop may drive `plan_tracker` to surface fix-wave progress (task naming and lifecycle per conformance-check.md's fix loop / the Fix fan-out Progress rule); it never calls `phase_tracker`. Call `phase_tracker({ action: "complete", phase: "verify" })` only when the reference says the handoff is durably complete: either a current `CONFORMS` result, or a current `## Closure / conformance` inventory whose carried-open concerns all come from valid deferred gaps, including `recommended: fix` gaps carried open because a declared precondition made the fix loop unavailable (`maxFixRounds: 0`, or no eligible named-branch worktree). A started positive-cap fix loop that blocks, fails, or exhausts its rounds with an open `fix` gap is escalation, not completion; on escalation, do not complete verify, stop and report.
|
|
230
|
+
1. **Parent full verification.** Run the complete plan-header `**Verification:**` command set foreground: tests plus every declared lint, type, format, and build check. A failure must be repaired and the full set rerun successfully before the next step. Before dispatching a verification repair, reopen (`in_progress`) every existing plan-task index whose `Files:` ownership includes its touched files; leave unowned cross-cutting repair work in the verification report. Those indices stay `in_progress` through the successful full rerun **and** step 2's whole-diff review accepting the repair — that acceptance is their completion point, not the passing rerun. Commit any verification-produced tracked changes; use the resulting `HEAD_SHA` in the review task and include the commands/results in its existing `DESCRIPTION`.
|
|
231
|
+
2. **Whole-diff code review.** Only after passing full verification, dispatch one foreground whole-diff `code-reviewer` per `/skill:requesting-code-review` against that committed HEAD, with `SCOPED_TEST_COMMANDS: none`; the reviewer does not repeat the full suite. Address Critical and Moderate findings. Before dispatching a review repair, reopen (`in_progress`) every existing plan-task index whose `Files:` ownership includes its touched files; leave unowned cross-cutting repair work in the review report. Mark each reopened index `complete` only once the repair is re-verified and the re-review accepts it — this is the same completion point step 1's reopened indices wait for, not an extra gate, and the gate order stays full verification -> whole-diff CR -> conformance. Any repair invalidates prior full verification, so rerun the full set successfully before the next gate.
|
|
232
|
+
3. **Close the loop — conformance check.** The review in step 2 is plan-vs-code (single-step); it inherits any requirement the plan already dropped. Before marking verify complete, dispatch a fresh-context **`conformance-reviewer`** — its **own** dispatch, never fused into the step-2 review — to confront the deliverable (code **and** docs) against the *origin* — the spec **and** the original prompt — per `verification-before-completion/reference/conformance-check.md`. Pass the spec path, the verbatim original prompt, and the full diff. Follow that reference for the partition rule, concern decomposition, and fix-loop mechanics; do not reimplement them here. The fix loop reuses durable `Gn` gap indices as defined in conformance-check.md; it never calls `phase_tracker`. Call `phase_tracker({ action: "complete", phase: "verify" })` only when the reference says the handoff is durably complete: either a current `CONFORMS` result, or a current `## Closure / conformance` inventory whose carried-open concerns all come from valid deferred gaps, including `recommended: fix` gaps carried open because a declared precondition made the fix loop unavailable (`maxFixRounds: 0`, or no eligible named-branch worktree). A started positive-cap fix loop that blocks, fails, or exhausts its rounds with an open `fix` gap is escalation, not completion; on escalation, do not complete verify, stop and report.
|
|
231
233
|
4. Summarize what was implemented (tasks completed, files changed, test counts, code-review verdict). Emit the `## Closure / conformance` block exactly as defined in `verification-before-completion/reference/conformance-check.md`: it must open with the two-line sentinel (`status: CONFORMS (0 open)` or `status: GAPS (N open)`, then `audited-base: <full HEAD SHA>`), then carry the exact durable concern schema by reference with no renamed or reformatted fields. `finishing-a-development-branch` Step 3.5 consumes that block verbatim.
|
|
232
234
|
5. **Proceed to finishing — no confirmation prompt.** Once verify is complete per step 3's criterion, invoke `/skill:finishing-a-development-branch` immediately. Its Step 4 menu (squash / PR / keep / discard) is the human gate; a separate "ready to finish?" prompt only stacks a second stop in front of it. Carried-open concerns are resolved there per concern via the `## Closure / conformance` block from step 4. Manual testing is a follow-up after the finishing choice (on `<base-branch>` after a squash-merge, or on the PR branch), never a reason to hold this gate.
|
|
233
235
|
|
|
@@ -251,7 +253,9 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
|
|
|
251
253
|
- Dispatching `code-reviewer` per task inside a wave (CR binds to the integrated wave diff)
|
|
252
254
|
- Dispatching an implementer or code-reviewer without a `SCOPED_TEST_COMMANDS` value (commands or `none`)
|
|
253
255
|
- About to run the full verification entrypoint during the implement phase — task and wave gates run scoped, plan-declared commands only; the full set belongs to verify
|
|
254
|
-
-
|
|
256
|
+
- Dispatching a verification or review repair before reopening (`in_progress`) the plan-task indices that own its touched files, or completing them on the passing rerun instead of on the accepting whole-diff review
|
|
257
|
+
- Dispatching whole-diff CR before parent full verification passes, or conformance before the foreground CR result and any invalidating repair re-verification/re-review are accepted
|
|
258
|
+
- Polling, joining, or relaunching an unexpectedly asynchronous gauntlet dispatch instead of stopping and reporting
|
|
255
259
|
|
|
256
260
|
## Integration
|
|
257
261
|
|
|
@@ -219,7 +219,7 @@ Otherwise → not TDD
|
|
|
219
219
|
|
|
220
220
|
No exceptions without your human partner's permission.
|
|
221
221
|
|
|
222
|
-
When the TDD implementation cycle is complete (all tests green, code committed), mark the implement phase complete:
|
|
222
|
+
When the TDD implementation cycle is complete (all tests green, code committed), mark the implement phase complete only for a direct, standalone task. In an orchestrated gauntlet task or review loop, green tests and a commit do not accept the parent task or complete its phase: the parent retains tracker and phase ownership through its required reviews.
|
|
223
223
|
|
|
224
224
|
```
|
|
225
225
|
phase_tracker({ action: "complete", phase: "implement" })
|
|
@@ -160,7 +160,7 @@ Defaults recognise `make ci`, `make test`, `npm test`, `pnpm test`, `yarn test`,
|
|
|
160
160
|
|
|
161
161
|
Before running the verification gate, call `phase_tracker({ action: "start", phase: "verify" })`.
|
|
162
162
|
|
|
163
|
-
When all verification passes,
|
|
163
|
+
When all verification passes, finish and await the required `plan_tracker` updates before marking verify complete. Reconcile accepted work on its existing indices; never clear or reinitialize to obtain closure. In a direct verification path, complete only after those updates have actually returned.
|
|
164
164
|
|
|
165
165
|
```
|
|
166
166
|
phase_tracker({ action: "complete", phase: "verify" })
|
|
@@ -130,7 +130,7 @@ Mirrors `subagent-driven-development` Parallel-Wave Mode and reuses its
|
|
|
130
130
|
`plan_tracker` progress surface. Runs entirely inside the gate — it invokes
|
|
131
131
|
**no** `phase_tracker` calls (`phase_tracker({ phase: "implement" })` errors
|
|
132
132
|
while verify is `in_progress`) and does **not** enter SDD's phase machinery.
|
|
133
|
-
Only the fan-out/integrate/review shape and `plan_tracker` are reused.
|
|
133
|
+
Only the fan-out/integrate/review shape and `plan_tracker` are reused. Every execution dispatch is foreground with top-level `async: false`, including retries and prose-described dispatches; an unexpected async handle is a configuration failure: stop and report, never poll or relaunch. `forceTopLevelAsync` is incompatible; see [pi-cohort dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md).
|
|
134
134
|
|
|
135
135
|
**Precondition — worktree required.** The loop needs a worktree HEAD to branch
|
|
136
136
|
fixes from. On the ad-hoc `finishing-a-development-branch` paths that run in a
|
|
@@ -143,23 +143,16 @@ prerequisites hold.
|
|
|
143
143
|
|
|
144
144
|
Per round:
|
|
145
145
|
|
|
146
|
-
1.
|
|
147
|
-
clause verbatim, truncated>`; carry the gap's requirement text mechanically,
|
|
148
|
-
no orchestrator-authored summaries); never `init`, which would wipe the
|
|
149
|
-
implement phase's completed task list. Lifecycle per gap: `pending` →
|
|
150
|
-
`in_progress` → `complete`. The widget now shows fix-wave progress during
|
|
151
|
-
verify.
|
|
146
|
+
1. **Synchronize gap tasks** — append only a genuinely new gap that is entering remediation, named `Gn: <gap origin clause verbatim, truncated>`; never `init`. Find existing gaps by their exact `Gn:` prefix and reuse that index even if origin wording changes. Carried-OPEN inventory-only gaps add nothing. Before dispatch, mark every remediated gap's existing index `in_progress`; a re-audit needing more work reopens that same `Gn` index. The lifecycle traces `[T1,T2]`, then `[T1,T2,G1]`, then `[T1,T2,G1,G2]`; no test-retry or review-round wrapper task.
|
|
152
147
|
2. **Fix dispatch** — per `dispatching-parallel-agents` "Fix fan-out": a `disjoint`
|
|
153
148
|
group of ≥ 2 gaps (per the report's `Parallel-safe:` line) fixes in one parallel
|
|
154
|
-
dispatch — one `implementer` per gap (fresh context, `
|
|
155
|
-
the conformance worktree, task = the gap block verbatim
|
|
156
|
-
the ownership boundary). The dispatch adds `SCOPED_TEST_COMMANDS`
|
|
157
|
-
block: the gap-relevant plan-declared commands, or `none` (the round's
|
|
158
|
-
gate owns execution). `conflicts` pairs serialize. Gaps outside any ≥ 2-ID
|
|
159
|
-
`disjoint` group run sequentially as before. Then dispatch `spec-reviewer`
|
|
160
|
-
gap on the gap-block reference contract below.
|
|
161
|
-
dispatch; `complete` is deferred until the gap's patch is successfully
|
|
162
|
-
integrated in step 3 below.
|
|
149
|
+
foreground dispatch — one `implementer` per gap (fresh context, `async: false`,
|
|
150
|
+
`worktree: true`, `cwd` = the conformance worktree, task = the gap block verbatim
|
|
151
|
+
with `touched-files` as the ownership boundary). The dispatch adds `SCOPED_TEST_COMMANDS`
|
|
152
|
+
to the gap block: the gap-relevant plan-declared commands, or `none` (the round's
|
|
153
|
+
test gate owns execution). `conflicts` pairs serialize. Gaps outside any ≥ 2-ID
|
|
154
|
+
`disjoint` group run sequentially as before. Then dispatch foreground `spec-reviewer`
|
|
155
|
+
per gap on the gap-block reference contract below.
|
|
163
156
|
3. **Integrate** serially via `git apply` onto the worktree HEAD, one gap's
|
|
164
157
|
patch at a time. Failure handling is inherited verbatim from
|
|
165
158
|
`dispatching-parallel-agents` "Review and Integrate": textual conflict →
|
|
@@ -168,10 +161,9 @@ Per round:
|
|
|
168
161
|
offending task sequentially on integrated HEAD; a failed agent → integrate
|
|
169
162
|
the successes, then retry the failure with fresh context including the
|
|
170
163
|
integrated changes. A `BLOCKED`/`NEEDS_CONTEXT` return surfaces to the user.
|
|
171
|
-
4. **Test gate** on the integrated tree,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
6. **Re-audit**: re-dispatch `conformance-reviewer` over the fixes **plus** the
|
|
164
|
+
4. **Test gate** on the integrated tree. In a plan flow, run the full plan-header `Verification` set once here; on an ad-hoc no-plan path, use the project's canonical test command. A failure re-enters the failure-handling rules above.
|
|
165
|
+
5. **Round CR and completion** — run `code-reviewer` once on the round's cumulative fix delta (not per gap), foreground with `async: false` and `SCOPED_TEST_COMMANDS` = the round's gap-relevant commands, or `none` (the round's test gate owns execution). After integration, tests, and this CR accept the work, explicitly mark every remediated gap's same `Gn` index `complete`, before re-audit.
|
|
166
|
+
6. **Re-audit**: foreground re-dispatch `conformance-reviewer` with `async: false` over the fixes **plus** the
|
|
175
167
|
regression guard (any prior-`DELIVERED` requirement whose `evidence` file
|
|
176
168
|
the fix diff touched). Pass the full prior conformance report (every row,
|
|
177
169
|
including DELIVERED rows and their `evidence` `file:line`) and the round's
|
|
@@ -85,7 +85,7 @@ Otherwise one plan. Service, contract, or schema count is not a split signal - o
|
|
|
85
85
|
Before mapping files, dispatch a scout to build the implementation map. Foreground, no announcement, no user interaction. The task template below is fixed — fill exactly **one** variable, the absolute spec path; compose nothing else:
|
|
86
86
|
|
|
87
87
|
```
|
|
88
|
-
subagent({ agent: "scout", context: "fresh", cwd: "<abs worktree path>",
|
|
88
|
+
subagent({ agent: "scout", context: "fresh", async: false, cwd: "<abs worktree path>",
|
|
89
89
|
phase: "plan-recon", output: "<abs plan path — same filename as the spec, per the table above>",
|
|
90
90
|
task: <the fixed template below, with the spec path filled> })
|
|
91
91
|
```
|
|
@@ -264,11 +264,14 @@ Every plan ends with a `## Spec coverage` section — authored last, placed afte
|
|
|
264
264
|
|---|---|---|
|
|
265
265
|
| § "Design" L34-L37 | anchor line in task template | Task 2 |
|
|
266
266
|
| § "Edge cases" L120 | stale anchor = blocking SR finding | Task 4, Task 5 |
|
|
267
|
+
| § "Testing" L84 | checker fixtures: `node --test extensions/lib/plan-check.test.ts` | Task 3 |
|
|
268
|
+
| § "Acceptance" L88 | full suite passes: `npm test` | Verification |
|
|
267
269
|
| § "Out of scope" L131 | fix-round anchoring | waived: out of scope per spec |
|
|
268
270
|
| - | mechanical: release commit | Task 7 |
|
|
269
271
|
```
|
|
270
272
|
|
|
271
|
-
- **Requirement rows:** anchor + short requirement + owner = task-ID list, or `waived: <reason>` **only when the spec itself marks the item out of scope**. A waiver on an in-scope normative requirement is a Self-Review failure — there is no human plan-review gate to catch it downstream.
|
|
273
|
+
- **Requirement rows:** anchor + short requirement + owner = task-ID list, or `Verification`, or `waived: <reason>` **only when the spec itself marks the item out of scope**. A waiver on an in-scope normative requirement is a Self-Review failure — there is no human plan-review gate to catch it downstream.
|
|
274
|
+
- **`Verification` owner:** use for a requirement the header `**Verification:**` command proves. Write the exact string `Verification`, alone. Quote only literals contained in that header. Anchor the single requirement line. Keep scoped commands task-owned.
|
|
272
275
|
- **Mechanical-task rows:** anchor `-`, requirement `mechanical: <short>`, owner = the task ID. One such row per anchor-less task.
|
|
273
276
|
- The table is plan-authoring-time only — never passed to implementer or reviewer dispatches.
|
|
274
277
|
|
|
@@ -293,12 +296,13 @@ If a decision is genuinely open, put it in an explicit **Open Questions** sectio
|
|
|
293
296
|
After drafting the plan and before announcing it complete, run the deterministic checker, then the judgment checks yourself — not a subagent dispatch.
|
|
294
297
|
|
|
295
298
|
- **Deterministic checker.** Run `plan_check({ planPath })` on the saved plan. Assess and fix every finding yourself (no human involvement), then re-run until it passes — a pass writes the execution stamp that implement-start verifies mechanically. If the same finding survives 3 fix rounds, convert it to an explicit Open Question and stop (the pre-existing Open-Questions halt, resolved by the human in-session — not a new gate). The checker covers table closure, quote integrity, anchor resolution, path existence, placeholder scan, wave file-disjointness, solo-line presence, and header-only entrypoint.
|
|
296
|
-
- **Code-vs-anchor sanity.** For each
|
|
299
|
+
- **Code-vs-anchor sanity.** For each task-owned requirement row, re-read the anchored spec lines and confirm the owner tasks' bodies do what they say - mechanism present, not just the quoted literal. For each `Verification` row, confirm the header command exercises the anchored requirement. Fix the task, don't annotate.
|
|
297
300
|
- **Type / API consistency.** Function signatures and field names that appear in multiple tasks must match exactly. The plan is its own contract — internal contradictions surface as bugs during execution.
|
|
298
301
|
- **Scoped-test coverage.** Every code-touching wave declares at least one scoped test command; only doc-only waves may have none.
|
|
299
302
|
- **Runtime-resource disjointness.** For every multi-task wave, confirm no two tasks contend on a shared mutable runtime resource (DB/schema, port, fixture, external service, shared temp path) — `Files:` overlap is checked mechanically, resource contention is not. Contention = mis-grouped wave; split or re-order before handoff.
|
|
300
303
|
- **Solo-reason validity.** Every single-task wave's `Solo:` line (presence is checked mechanically) must name its specific blocker — the blocking task/wave, the contended resource, or `lone remaining task`. Category-only justifications are under-justified; merge or justify before handoff.
|
|
301
304
|
- **Waiver authorization.** Every `waived: <reason>` owner in `## Spec coverage` is authorized by the spec itself marking the item out of scope. A waiver on an in-scope normative requirement is a Self-Review failure — there is no human plan-review gate to catch it downstream.
|
|
305
|
+
- **Verification-ownership authorization.** `Verification` on a requirement no header command exercises is a Self-Review failure.
|
|
302
306
|
- **Documentation-impact mapping.** Each Documentation impact entry maps to a plan task (or explicit "none").
|
|
303
307
|
|
|
304
308
|
Fix what this review finds before handoff.
|
|
@@ -316,10 +320,11 @@ Fix what this review finds before handoff.
|
|
|
316
320
|
|
|
317
321
|
## Execution Handoff
|
|
318
322
|
|
|
319
|
-
After saving the plan, mark the planning phase complete:
|
|
323
|
+
After saving the plan, mark the planning phase complete, then initialize `plan_tracker` once with every plan task in wave order. Name each entry `W<k>: <title>` using its containing wave and task title. This is the execution list: do not initialize it again on continuation.
|
|
320
324
|
|
|
321
325
|
```
|
|
322
326
|
phase_tracker({ action: "complete", phase: "plan" })
|
|
327
|
+
plan_tracker({ action: "init", tasks: ["W1: <title>", "W1: <title>", "W2: <title>"] })
|
|
323
328
|
```
|
|
324
329
|
|
|
325
330
|
Then auto-select the execution mode and proceed — no pause, no picker. The mode is a pure function of the plan's wave structure:
|