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 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** - a subagent reads the finished code and docs against your *original words* from step 1, not the plan, and reports per-requirement: delivered, partial, missing, drifted, or unauthorized. 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.
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.0
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
- for (const n of row.ownerTasks) {
405
- coveredTasks.add(n);
406
- if (row.isMechanical) mechanicalCoveredTasks.add(n);
407
- const task = taskByNumber.get(n);
408
- if (!task) {
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: `row references Task ${n} but no such task exists`,
431
+ reason: "Verification row has no backtick literal to check against the header",
414
432
  });
415
- continue;
416
433
  }
417
- if (row.isMechanical) continue;
418
- if (!row.anchor) {
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: 'requirement row anchor is not a parseable \u00a7 "heading" L<n>-L<n> anchor',
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: () => options.branch ?? [] },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "5.3.0",
3
+ "version": "5.3.2",
4
4
  "description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
5
5
  "author": "Jacek Juraszek",
6
6
  "type": "module",
@@ -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 (foreground no
16
- `async:`; no `model:` — pi-cohort `agentOverrides` owns builder models):
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>"`, where `<prefix>` is `"W<k>-"` inside an execution wave and empty elsewhere. Fix tasks always extend the tracker, never re-init. Mark `in_progress` at dispatch, `complete` at integration.
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; 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").
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. **Dispatch implementer.** 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.
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. Mark task complete in `plan_tracker`.
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 the whole-diff code review dispatch and the full verification run (concurrent when the verification set classifies safe).
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
- - **Init once, wave-ordered:** `init` with every task across all waves in wave order, each name prefixed with its wave (`"W1: <title>"`, `"W2: <title>"`, …). Indices are positional and stable; never re-init mid-run (it drops statuses).
164
- - **Wave fan-out → `in_progress`:** mark every task index in the wave `in_progress`. Multiple simultaneous `in_progress` entries is expected (sequential mode has one).
165
- - **Wave commit → `complete`:** after the wave's gate passes and it commits, mark all that wave's indices `complete`. `complete` = durably committed, so a task in conflict-fallback stays `in_progress` until its wave commits.
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 is unchanged:** init the full list, one `in_progress` at a time; wave prefixes are harmless if present.
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. **Fan out.** One parallel dispatch (shape below): `implementer` per task, `context: "fresh"`, `worktree: true`. Each returns a status + a patch.
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. **Classify, then dispatch both audits.** Classify the plan header's `**Verification:**` commands once per verify entry (post-fix re-runs are serial by nature no re-classification): **unsafe** = anything that can rewrite tracked files write-mode formatters (`--write`, `-w`), autofixers (`--fix`, `-u` snapshot updates), codegen, migrations regenerating checked-in artifacts; **safe** = commands that write only untracked/ignored paths test runners, check-mode linters/formatters (`--check`, `--diff`), type checkers, builds only when their outputs are untracked; wrappers (`script/verify`, `package.json` aliases, `Makefile` targets) read one level in, safe iff every invoked command classifies safe; deeper nesting or an undeterminable write destination unsafe. The rule is the write destination; examples are illustrative, not authoritative. Unclear serial. Orchestrator judgment, not config no settings key, never a prompt.
227
- - **Safe concurrent (default).** Dispatch the whole-diff review per `/skill:requesting-code-review` against the worktree's full diff vs `main`, passing `SCOPED_TEST_COMMANDS: none` (the full run below is the orchestrator's): `subagent({ agent: "code-reviewer", context: "fresh", async: true, cwd: <worktree>, output: <absolute non-colliding $TMPDIR path>, task: <review template> })` capture the returned run id; never a relative `output:` path (it lands untracked in the worktree). In the same turn, run the full `**Verification:**` set foreground in your own bash: tests + style + format (a single bundling entrypoint, or the listed individual commands). This is the only full run before conformance task and wave gates ran scoped commands only.
228
- - **Unsafe/unclear serial.** Same two audits, review first (a plain synchronous dispatch), then the verification set, with one declarative notice naming the offender e.g. `Serial review->verify: 'npm run fmt' writes tracked files.` Only the start order changes; step 2's join invariant is identical.
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
- - Dispositioning either after-all-tasks audit fix dispatch, finding triage, verify-complete claim, or `conformance-reviewer` dispatch before both the whole-diff review and the full verification run have completed
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, mark the verify phase complete:
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. **`plan_tracker` add** — append the round's gaps as tasks (`Gn: <gap origin
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, `worktree: true`, `cwd` =
155
- the conformance worktree, task = the gap block verbatim with `touched-files` as
156
- the ownership boundary). The dispatch adds `SCOPED_TEST_COMMANDS` to the gap
157
- block: the gap-relevant plan-declared commands, or `none` (the round's test
158
- gate owns execution). `conflicts` pairs serialize. Gaps outside any ≥ 2-ID
159
- `disjoint` group run sequentially as before. Then dispatch `spec-reviewer` per
160
- gap on the gap-block reference contract below. Task lifecycle: mark `in_progress` at
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, using the project's canonical test
172
- command. A failure re-enters the failure-handling rules above.
173
- 5. **`code-reviewer` once** on the round's cumulative fix delta (not per gap), with `SCOPED_TEST_COMMANDS` = the round's gap-relevant commands, or `none` (the round's test gate owns execution).
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 non-waived 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. Fix the task, don't annotate.
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: