pi-gauntlet 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/agents/implementer.md +1 -1
- package/extensions/phase-tracker.test.ts +189 -0
- package/extensions/phase-tracker.ts +57 -0
- package/extensions/verify-before-ship.test.ts +92 -0
- package/extensions/verify-before-ship.ts +4 -3
- package/package.json +1 -1
- package/skills/subagent-driven-development/SKILL.md +11 -8
- package/skills/verification-before-completion/SKILL.md +5 -3
- package/skills/writing-plans/SKILL.md +38 -2
- package/skills/writing-skills/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v4.8.0 - 2026-08-12
|
|
4
|
+
|
|
5
|
+
- `writing-plans`: mandatory plan-time recon (fixed one-variable scout template writing a draft to the plan path); plan header `**Verification:**` command set (tests + style + format, header-only); per-task format-and-lint step; scoped-test and header-only-entrypoint Self-Review checks.
|
|
6
|
+
- `subagent-driven-development`: wave test gate runs the wave's declared commands only; SR-per-task + CR-per-wave stated as a commit precondition with a doc-only (SR-only) exemption; full verification set runs once at verify before the conformance dispatch; new Red Flags (SR+CR fusion; full run during implement).
|
|
7
|
+
- `phase-tracker`: advisory review-cadence flow guard - implement-phase parent commits warn when the last implementer postdates both reviewers. Presence-only, never blocks, honors `flowGuards.enforce`.
|
|
8
|
+
- **Behavior change** - `verify-before-ship` no longer watches `git commit` (still watches `git push` / `gh pr create`): local wave commits are not ship events, and warning on them trained per-wave full-CI runs.
|
|
9
|
+
- `verification-before-completion`: evidence-proportionality rule (scoped commands prove scoped claims; the full entrypoint proves phase completion, once at verify).
|
|
10
|
+
- `implementer`: REFACTOR runs task-declared tests, not "the full relevant test suite".
|
|
11
|
+
|
|
3
12
|
## v4.7.0 - 2026-08-07
|
|
4
13
|
|
|
5
14
|
Sanction the spec-in-hand resume gesture and guard the implement phase (#6).
|
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
|
|
|
66
66
|
|
|
67
67
|
- **13 skills** - the workflow logic. They activate automatically when pi sees the matching kind of task, and each one gates the next: `brainstorming`, `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `dispatching-parallel-agents`, `verification-before-completion`, `systematic-debugging`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `writing-skills`.
|
|
68
68
|
- **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.
|
|
69
|
-
- **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
|
|
69
|
+
- **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. See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
|
|
70
70
|
|
|
71
71
|
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.
|
|
72
72
|
|
package/agents/implementer.md
CHANGED
|
@@ -16,7 +16,7 @@ You are an implementation specialist. You execute an approved plan using strict
|
|
|
16
16
|
|
|
17
17
|
1. **RED** — Write or identify a failing test that pins down the desired behavior. Run it. Confirm it fails for the right reason (not a typo, not an import error).
|
|
18
18
|
2. **GREEN** — Write the minimum code that makes the test pass. Run it.
|
|
19
|
-
3. **REFACTOR** — Clean up without changing behavior. Run the
|
|
19
|
+
3. **REFACTOR** — Clean up without changing behavior. Run the tests the task declares (its `Test:` files and stated commands).
|
|
20
20
|
|
|
21
21
|
## Three-scenario TDD
|
|
22
22
|
|
|
@@ -287,3 +287,192 @@ test("resumed session: closure gate blocks complete verify without a conformance
|
|
|
287
287
|
};
|
|
288
288
|
assert.equal(res.details.error, "no conformance-reviewer dispatch observed");
|
|
289
289
|
});
|
|
290
|
+
|
|
291
|
+
const subagentResult = (agents: string[]) => ({
|
|
292
|
+
type: "message",
|
|
293
|
+
message: {
|
|
294
|
+
role: "toolResult",
|
|
295
|
+
toolName: "subagent",
|
|
296
|
+
details: { results: agents.map((agent) => ({ agent, exitCode: 0 })) },
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const implementBranch = (extra: unknown[] = []) => [
|
|
301
|
+
phaseResult("start", phases({ brainstorm: "in_progress" })),
|
|
302
|
+
phaseResult("complete", phases({ brainstorm: "complete" })),
|
|
303
|
+
phaseResult("start", phases({ brainstorm: "complete", plan: "in_progress" })),
|
|
304
|
+
phaseResult("complete", phases({ brainstorm: "complete", plan: "complete" })),
|
|
305
|
+
phaseResult("start", phases({ brainstorm: "complete", plan: "complete", implement: "in_progress" })),
|
|
306
|
+
...extra,
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const commitCall = (id: string) => ({
|
|
310
|
+
toolName: "bash",
|
|
311
|
+
toolCallId: id,
|
|
312
|
+
input: { command: "git commit -m 'integrate wave'" },
|
|
313
|
+
});
|
|
314
|
+
const commitResult = (id: string) => ({
|
|
315
|
+
toolName: "bash",
|
|
316
|
+
toolCallId: id,
|
|
317
|
+
isError: false,
|
|
318
|
+
content: [{ type: "text", text: "ok" }],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("implement-phase commit with implementer newer than both reviewers warns", async () => {
|
|
322
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
323
|
+
delete process.env.PI_SUBAGENT_DEPTH; // isolate from an ambient subagent depth in the test-runner's own process
|
|
324
|
+
try {
|
|
325
|
+
const h = harness({ branch: implementBranch([subagentResult(["implementer"])]) });
|
|
326
|
+
await h.emit("session_start");
|
|
327
|
+
await h.emitEvent("tool_call", commitCall("c1"));
|
|
328
|
+
const warned = (await h.emitEvent("tool_result", commitResult("c1")))[0] as { content: { text: string }[] };
|
|
329
|
+
assert.match(warned.content[0].text, /no spec-reviewer or code-reviewer observed/);
|
|
330
|
+
} finally {
|
|
331
|
+
if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("cadence guard silent in subagent children", async () => {
|
|
336
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
337
|
+
process.env.PI_SUBAGENT_DEPTH = "1";
|
|
338
|
+
try {
|
|
339
|
+
const branch = implementBranch([subagentResult(["implementer"])]);
|
|
340
|
+
const h = harness({ branch });
|
|
341
|
+
await h.emit("session_start");
|
|
342
|
+
await h.emitEvent("tool_call", commitCall("c1"));
|
|
343
|
+
assert.equal((await h.emitEvent("tool_result", commitResult("c1")))[0], undefined);
|
|
344
|
+
} finally {
|
|
345
|
+
if (priorDepth === undefined) delete process.env.PI_SUBAGENT_DEPTH;
|
|
346
|
+
else process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("fresh SR and CR after the implementer keep the commit silent", async () => {
|
|
351
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
352
|
+
delete process.env.PI_SUBAGENT_DEPTH;
|
|
353
|
+
try {
|
|
354
|
+
const h = harness({
|
|
355
|
+
branch: implementBranch([
|
|
356
|
+
subagentResult(["implementer"]),
|
|
357
|
+
subagentResult(["spec-reviewer"]),
|
|
358
|
+
subagentResult(["code-reviewer"]),
|
|
359
|
+
]),
|
|
360
|
+
});
|
|
361
|
+
await h.emit("session_start");
|
|
362
|
+
await h.emitEvent("tool_call", commitCall("c1"));
|
|
363
|
+
assert.equal((await h.emitEvent("tool_result", commitResult("c1")))[0], undefined);
|
|
364
|
+
} finally {
|
|
365
|
+
if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("AND-logic: fresh SR with stale CR stays silent (doc-only wave shape)", async () => {
|
|
370
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
371
|
+
delete process.env.PI_SUBAGENT_DEPTH;
|
|
372
|
+
try {
|
|
373
|
+
const h = harness({
|
|
374
|
+
branch: implementBranch([subagentResult(["implementer"]), subagentResult(["spec-reviewer"])]),
|
|
375
|
+
});
|
|
376
|
+
await h.emit("session_start");
|
|
377
|
+
await h.emitEvent("tool_call", commitCall("c1"));
|
|
378
|
+
assert.equal((await h.emitEvent("tool_result", commitResult("c1")))[0], undefined);
|
|
379
|
+
} finally {
|
|
380
|
+
if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("fused implementer+reviewer results in one dispatch stay silent", async () => {
|
|
385
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
386
|
+
delete process.env.PI_SUBAGENT_DEPTH;
|
|
387
|
+
try {
|
|
388
|
+
const h = harness({
|
|
389
|
+
branch: implementBranch([subagentResult(["implementer", "spec-reviewer", "code-reviewer"])]),
|
|
390
|
+
});
|
|
391
|
+
await h.emit("session_start");
|
|
392
|
+
await h.emitEvent("tool_call", commitCall("c1"));
|
|
393
|
+
assert.equal((await h.emitEvent("tool_result", commitResult("c1")))[0], undefined);
|
|
394
|
+
} finally {
|
|
395
|
+
if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("no implementer observed: commit stays silent", async () => {
|
|
400
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
401
|
+
delete process.env.PI_SUBAGENT_DEPTH;
|
|
402
|
+
try {
|
|
403
|
+
const h = harness({ branch: implementBranch() });
|
|
404
|
+
await h.emit("session_start");
|
|
405
|
+
await h.emitEvent("tool_call", commitCall("c1"));
|
|
406
|
+
assert.equal((await h.emitEvent("tool_result", commitResult("c1")))[0], undefined);
|
|
407
|
+
} finally {
|
|
408
|
+
if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("guard silent outside implement and when flowGuards.enforce is false", async () => {
|
|
413
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
414
|
+
delete process.env.PI_SUBAGENT_DEPTH;
|
|
415
|
+
try {
|
|
416
|
+
const planOnly = harness({
|
|
417
|
+
branch: [
|
|
418
|
+
phaseResult("start", phases({ brainstorm: "in_progress" })),
|
|
419
|
+
phaseResult("complete", phases({ brainstorm: "complete" })),
|
|
420
|
+
phaseResult("start", phases({ brainstorm: "complete", plan: "in_progress" })),
|
|
421
|
+
subagentResult(["implementer"]),
|
|
422
|
+
],
|
|
423
|
+
});
|
|
424
|
+
await planOnly.emit("session_start");
|
|
425
|
+
await planOnly.emitEvent("tool_call", commitCall("c1"));
|
|
426
|
+
assert.equal((await planOnly.emitEvent("tool_result", commitResult("c1")))[0], undefined);
|
|
427
|
+
|
|
428
|
+
const off = harness({
|
|
429
|
+
cwd: tempCwd({ piGauntlet: { flowGuards: { enforce: false } } }),
|
|
430
|
+
branch: implementBranch([subagentResult(["implementer"])]),
|
|
431
|
+
});
|
|
432
|
+
await off.emit("session_start");
|
|
433
|
+
await off.emitEvent("tool_call", commitCall("c1"));
|
|
434
|
+
assert.equal((await off.emitEvent("tool_result", commitResult("c1")))[0], undefined);
|
|
435
|
+
} finally {
|
|
436
|
+
if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("live tool_result observation updates the ledger", async () => {
|
|
441
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
442
|
+
delete process.env.PI_SUBAGENT_DEPTH;
|
|
443
|
+
try {
|
|
444
|
+
const h = harness({ branch: implementBranch([subagentResult(["implementer"])]) });
|
|
445
|
+
await h.emit("session_start");
|
|
446
|
+
await h.emitEvent("tool_result", {
|
|
447
|
+
toolName: "subagent",
|
|
448
|
+
toolCallId: "s1",
|
|
449
|
+
content: [],
|
|
450
|
+
details: { results: [{ agent: "spec-reviewer", exitCode: 0 }, { agent: "code-reviewer", exitCode: 0 }] },
|
|
451
|
+
});
|
|
452
|
+
await h.emitEvent("tool_call", commitCall("c1"));
|
|
453
|
+
assert.equal((await h.emitEvent("tool_result", commitResult("c1")))[0], undefined);
|
|
454
|
+
} finally {
|
|
455
|
+
if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("second implementer after reviews re-arms the warning", async () => {
|
|
460
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
461
|
+
delete process.env.PI_SUBAGENT_DEPTH;
|
|
462
|
+
try {
|
|
463
|
+
const h = harness({
|
|
464
|
+
branch: implementBranch([
|
|
465
|
+
subagentResult(["implementer"]),
|
|
466
|
+
subagentResult(["spec-reviewer"]),
|
|
467
|
+
subagentResult(["code-reviewer"]),
|
|
468
|
+
subagentResult(["implementer"]),
|
|
469
|
+
]),
|
|
470
|
+
});
|
|
471
|
+
await h.emit("session_start");
|
|
472
|
+
await h.emitEvent("tool_call", commitCall("c1"));
|
|
473
|
+
const warned = (await h.emitEvent("tool_result", commitResult("c1")))[0] as { content: { text: string }[] };
|
|
474
|
+
assert.match(warned.content[0].text, /no spec-reviewer or code-reviewer observed/);
|
|
475
|
+
} finally {
|
|
476
|
+
if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
477
|
+
}
|
|
478
|
+
});
|
|
@@ -67,6 +67,30 @@ const qualifiesAsClosureDispatch = (details: unknown): boolean => {
|
|
|
67
67
|
return d.results.some((r) => r?.agent === "conformance-reviewer" && r?.exitCode === 0);
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
// Review-cadence guard (spec 2026-08-12-execution-fidelity-hardening): presence-only
|
|
71
|
+
// advisory ledger of the most recent completed implementer / spec-reviewer /
|
|
72
|
+
// code-reviewer dispatch. Agents completing in the SAME dispatch share a sequence
|
|
73
|
+
// number, so a fused implementer+reviewer result never reads as reviewer-stale.
|
|
74
|
+
const CADENCE_AGENTS = ["implementer", "spec-reviewer", "code-reviewer"] as const;
|
|
75
|
+
type CadenceAgent = (typeof CADENCE_AGENTS)[number];
|
|
76
|
+
|
|
77
|
+
const completedCadenceAgents = (details: unknown): CadenceAgent[] => {
|
|
78
|
+
const d = details as { results?: { agent?: unknown; exitCode?: unknown }[] } | undefined;
|
|
79
|
+
if (!d || !Array.isArray(d.results)) return [];
|
|
80
|
+
const seen = new Set<CadenceAgent>();
|
|
81
|
+
for (const r of d.results) {
|
|
82
|
+
if (r?.exitCode === 0 && (CADENCE_AGENTS as readonly unknown[]).includes(r?.agent)) {
|
|
83
|
+
seen.add(r.agent as CadenceAgent);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...seen];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const REVIEW_CADENCE_WARNING =
|
|
90
|
+
"⚠️ Reminder: no spec-reviewer or code-reviewer observed since the last implementer.\n" +
|
|
91
|
+
"SR is required per task; CR per wave for code waves (doc-only waves are SR-only).\n" +
|
|
92
|
+
"Dispatch the missing review(s) before committing integrated work.";
|
|
93
|
+
|
|
70
94
|
const CLOSURE_GATE_ERROR =
|
|
71
95
|
"Error: cannot complete 'verify': no conformance-reviewer dispatch observed.\n" +
|
|
72
96
|
"The closing loop is required before verify completes. Either:\n" +
|
|
@@ -266,6 +290,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
266
290
|
let gauntletEntered = false;
|
|
267
291
|
const attemptedRecoveryEdges = new Set<RecoveryEdge>();
|
|
268
292
|
|
|
293
|
+
let cadenceSeq = 0;
|
|
294
|
+
const cadenceLastSeen: Record<CadenceAgent, number> = {
|
|
295
|
+
implementer: 0,
|
|
296
|
+
"spec-reviewer": 0,
|
|
297
|
+
"code-reviewer": 0,
|
|
298
|
+
};
|
|
299
|
+
const observeCadence = (details: unknown) => {
|
|
300
|
+
const agents = completedCadenceAgents(details);
|
|
301
|
+
if (agents.length === 0) return;
|
|
302
|
+
cadenceSeq++;
|
|
303
|
+
for (const a of agents) cadenceLastSeen[a] = cadenceSeq;
|
|
304
|
+
};
|
|
305
|
+
|
|
269
306
|
// Warn-once-per-phase ledger; cleared on every phase transition and on reconstruct.
|
|
270
307
|
const firedGuards = new Map<string, boolean>();
|
|
271
308
|
// Warnings stashed at tool_call, prepended at tool_result (verify-before-ship pattern).
|
|
@@ -329,6 +366,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
329
366
|
attemptedRecoveryEdges.clear();
|
|
330
367
|
firedGuards.clear();
|
|
331
368
|
pendingGuardWarnings.clear();
|
|
369
|
+
cadenceSeq = 0;
|
|
370
|
+
cadenceLastSeen.implementer = 0;
|
|
371
|
+
cadenceLastSeen["spec-reviewer"] = 0;
|
|
372
|
+
cadenceLastSeen["code-reviewer"] = 0;
|
|
332
373
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
333
374
|
if (entry.type === "custom_message") {
|
|
334
375
|
if (entry.customType === RECOVERY_CUSTOM_TYPE) {
|
|
@@ -349,6 +390,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
349
390
|
}
|
|
350
391
|
} else if (msg.toolName === "subagent") {
|
|
351
392
|
if (qualifiesAsClosureDispatch(msg.details)) conformanceDispatched = true;
|
|
393
|
+
observeCadence(msg.details);
|
|
352
394
|
} else if (msg.toolName === "plan_tracker") {
|
|
353
395
|
const details = msg.details as { tasks?: { status: string }[]; error?: string } | undefined;
|
|
354
396
|
if (details && !details.error) applyPlanActivity(details.tasks);
|
|
@@ -522,6 +564,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
522
564
|
}
|
|
523
565
|
}
|
|
524
566
|
|
|
567
|
+
// Guard 5 — review-cadence reminder (presence-only, advisory). Parent-session
|
|
568
|
+
// commits during implement only; child sessions (PI_SUBAGENT_DEPTH >= 1) are
|
|
569
|
+
// the implementers' own task commits and are exempt by design.
|
|
570
|
+
if (
|
|
571
|
+
!isSubagentChild &&
|
|
572
|
+
phases.implement.status === "in_progress" &&
|
|
573
|
+
parseGitCommit(command) &&
|
|
574
|
+
cadenceLastSeen.implementer > 0 &&
|
|
575
|
+
cadenceLastSeen.implementer > cadenceLastSeen["spec-reviewer"] &&
|
|
576
|
+
cadenceLastSeen.implementer > cadenceLastSeen["code-reviewer"]
|
|
577
|
+
) {
|
|
578
|
+
warnings.push(REVIEW_CADENCE_WARNING);
|
|
579
|
+
}
|
|
580
|
+
|
|
525
581
|
// Guard 3 — bash mutation outside the spec dir during brainstorm.
|
|
526
582
|
if (phases.brainstorm.status === "in_progress" && !firedGuards.get("brainstorm-write")) {
|
|
527
583
|
// Redirect target is cleanly extractable: judge it directly against the spec dirs,
|
|
@@ -548,6 +604,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
548
604
|
pi.on("tool_result", async (event) => {
|
|
549
605
|
if (event.toolName === "subagent") {
|
|
550
606
|
if (qualifiesAsClosureDispatch(event.details)) conformanceDispatched = true;
|
|
607
|
+
observeCadence(event.details);
|
|
551
608
|
const warning = pendingGuardWarnings.get(event.toolCallId);
|
|
552
609
|
if (warning) {
|
|
553
610
|
pendingGuardWarnings.delete(event.toolCallId);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { after, test } from "node:test";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import registerVerifyBeforeShip from "./verify-before-ship.ts";
|
|
7
|
+
|
|
8
|
+
const tempDirs: string[] = [];
|
|
9
|
+
after(() => {
|
|
10
|
+
for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function harness() {
|
|
14
|
+
const handlers = new Map<string, ((event: unknown, ctx: unknown) => unknown)[]>();
|
|
15
|
+
const dir = mkdtempSync(join(tmpdir(), "verify-before-ship-test-"));
|
|
16
|
+
tempDirs.push(dir);
|
|
17
|
+
const ctx = { cwd: dir };
|
|
18
|
+
const pi = {
|
|
19
|
+
on(event: string, handler: (event: unknown, context: unknown) => unknown) {
|
|
20
|
+
const registered = handlers.get(event) ?? [];
|
|
21
|
+
registered.push(handler);
|
|
22
|
+
handlers.set(event, registered);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
registerVerifyBeforeShip(pi as any);
|
|
26
|
+
const emitEvent = async (name: string, event: unknown) => {
|
|
27
|
+
const results: unknown[] = [];
|
|
28
|
+
for (const handler of handlers.get(name) ?? []) results.push(await handler(event, ctx));
|
|
29
|
+
return results;
|
|
30
|
+
};
|
|
31
|
+
return { emitEvent };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const bashCall = (id: string, command: string) => ({ toolName: "bash", toolCallId: id, input: { command } });
|
|
35
|
+
const bashResult = (id: string, isError = false) => ({
|
|
36
|
+
toolName: "bash",
|
|
37
|
+
toolCallId: id,
|
|
38
|
+
isError,
|
|
39
|
+
content: [{ type: "text", text: "ok" }],
|
|
40
|
+
});
|
|
41
|
+
const writeCall = (id: string, path: string) => ({ toolName: "write", toolCallId: id, input: { path } });
|
|
42
|
+
|
|
43
|
+
const editSource = async (h: ReturnType<typeof harness>) => {
|
|
44
|
+
await h.emitEvent("tool_call", writeCall("w1", "src/x.ts"));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const warningOf = (results: unknown[]): string | undefined => {
|
|
48
|
+
const r = results[0] as { content?: { text?: string }[] } | undefined;
|
|
49
|
+
return r?.content?.[0]?.text;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
test("git commit is not watched: no warning even when unverified", async () => {
|
|
53
|
+
const h = harness();
|
|
54
|
+
await editSource(h);
|
|
55
|
+
await h.emitEvent("tool_call", bashCall("c1", "git commit -m 'wave 1'"));
|
|
56
|
+
assert.equal((await h.emitEvent("tool_result", bashResult("c1")))[0], undefined);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("git push warns when unverified", async () => {
|
|
60
|
+
const h = harness();
|
|
61
|
+
await editSource(h);
|
|
62
|
+
await h.emitEvent("tool_call", bashCall("p1", "git push origin main"));
|
|
63
|
+
const warning = warningOf(await h.emitEvent("tool_result", bashResult("p1")));
|
|
64
|
+
assert.match(warning ?? "", /ran without verification/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("gh pr create warns when unverified", async () => {
|
|
68
|
+
const h = harness();
|
|
69
|
+
await editSource(h);
|
|
70
|
+
await h.emitEvent("tool_call", bashCall("pr1", "gh pr create --fill"));
|
|
71
|
+
const warning = warningOf(await h.emitEvent("tool_result", bashResult("pr1")));
|
|
72
|
+
assert.match(warning ?? "", /ran without verification/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a passing recognised run clears the warning for push", async () => {
|
|
76
|
+
const h = harness();
|
|
77
|
+
await editSource(h);
|
|
78
|
+
await h.emitEvent("tool_call", bashCall("t1", "npm test"));
|
|
79
|
+
await h.emitEvent("tool_result", bashResult("t1"));
|
|
80
|
+
await h.emitEvent("tool_call", bashCall("p1", "git push"));
|
|
81
|
+
assert.equal((await h.emitEvent("tool_result", bashResult("p1")))[0], undefined);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a failing recognised run does not clear", async () => {
|
|
85
|
+
const h = harness();
|
|
86
|
+
await editSource(h);
|
|
87
|
+
await h.emitEvent("tool_call", bashCall("t1", "npm test"));
|
|
88
|
+
await h.emitEvent("tool_result", bashResult("t1", true));
|
|
89
|
+
await h.emitEvent("tool_call", bashCall("p1", "git push"));
|
|
90
|
+
const warning = warningOf(await h.emitEvent("tool_result", bashResult("p1")));
|
|
91
|
+
assert.match(warning ?? "", /ran without verification/);
|
|
92
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Verify-before-ship extension
|
|
3
3
|
*
|
|
4
|
-
* Single-session verification gate for shipping commands (git
|
|
5
|
-
*
|
|
4
|
+
* Single-session verification gate for shipping commands (git push / gh pr create).
|
|
5
|
+
* Local commits are not ship events; commit-time review-cadence advisories live in
|
|
6
|
+
* phase-tracker. Tracks whether a recognised verification command has succeeded
|
|
6
7
|
* since the last source-file write; injects an advisory warning into the tool
|
|
7
8
|
* result of any ship command when verification is stale.
|
|
8
9
|
*
|
|
@@ -39,7 +40,7 @@ const DEFAULT_TEST_COMMANDS = [
|
|
|
39
40
|
"go\\s+test",
|
|
40
41
|
];
|
|
41
42
|
|
|
42
|
-
const SHIP_CMD = /\b(git\s+
|
|
43
|
+
const SHIP_CMD = /\b(git\s+push|gh\s+pr\s+create)\b/;
|
|
43
44
|
|
|
44
45
|
const SOURCE_EXT = /\.(ts|tsx|js|jsx|py|rb|go|rs|java|swift|kt)$/;
|
|
45
46
|
const TEST_PATH = /(^|\/)(tests?|__tests__)\/|\.(test|spec)\.|_test\.(py|go|rb)$/;
|
package/package.json
CHANGED
|
@@ -57,7 +57,7 @@ For each task in `plan_tracker`:
|
|
|
57
57
|
2. **Handle implementer status** (see below).
|
|
58
58
|
3. **Dispatch spec reviewer.** Verify the diff matches the spec — nothing missing, nothing extra.
|
|
59
59
|
4. If spec reviewer finds gaps → re-dispatch implementer to fix → re-review. Loop until ✅.
|
|
60
|
-
5. **Dispatch code-quality reviewer.** Only after spec is ✅.
|
|
60
|
+
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.
|
|
61
61
|
6. If quality reviewer finds issues → re-dispatch implementer → re-review. Loop until ✅.
|
|
62
62
|
7. Mark task complete in `plan_tracker`.
|
|
63
63
|
|
|
@@ -142,13 +142,13 @@ Auto-selected at handoff by `writing-plans` (any wave with ≥2 tasks) when the
|
|
|
142
142
|
|
|
143
143
|
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.
|
|
144
144
|
2. **Fan out.** One parallel dispatch (shape below): `implementer` per task, `context: "fresh"`, `worktree: true`. Each returns a status + a patch.
|
|
145
|
-
3. **Status + spec review per task.** Parse each `DONE`/`BLOCKED`/etc. (see [Implementer Status](#implementer-status)) **first**. Then **dispatch a `spec-reviewer` per accepted patch** (`DONE`, or a `DONE_WITH_CONCERNS` you proceeded with) in one parallel fan-out — `context: "fresh"`, `cwd: <this worktree>`, **no `worktree` flag** (read-only) — each passed its task text, the returned **patch diff**, and the absolute spec path. Review is **diff-based**: the diff's hunks carry `file:line`, and test execution is not the reviewer's job here (the wave test gate in step 5 runs the
|
|
145
|
+
3. **Status + spec review per task.** Parse each `DONE`/`BLOCKED`/etc. (see [Implementer Status](#implementer-status)) **first**. Then **dispatch a `spec-reviewer` per accepted patch** (`DONE`, or a `DONE_WITH_CONCERNS` you proceeded with) in one parallel fan-out — `context: "fresh"`, `cwd: <this worktree>`, **no `worktree` flag** (read-only) — each passed its task text, the returned **patch diff**, and the absolute spec path. Review is **diff-based**: the diff's hunks carry `file:line`, and test execution is not the reviewer's job here (the wave test gate in step 5 runs the 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 ✅.
|
|
146
146
|
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.
|
|
147
|
-
5. **Test gate.** Run the
|
|
148
|
-
6. **Quality review.** Code-quality review on the integrated wave diff; loop fixes to ✅.
|
|
147
|
+
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).
|
|
148
|
+
6. **Quality review.** Code-quality review on the integrated wave diff; loop fixes to ✅. Skip for doc-only waves (SR-only per the commit precondition below).
|
|
149
149
|
7. **Commit the wave.** Leaves a clean tree; the next wave's children branch from this commit and so see the integrated work.
|
|
150
150
|
|
|
151
|
-
**Two-stage review is preserved:** spec review per task (pre-integration, dispatched `spec-reviewer` — not inline), quality review per wave (post-integration).
|
|
151
|
+
**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.
|
|
152
152
|
|
|
153
153
|
**Dependent context across waves:** wave N+1 tasks branch from a HEAD containing wave N, so they see the code; still forward wave N's task summaries into wave N+1 prompts.
|
|
154
154
|
|
|
@@ -191,9 +191,10 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
|
|
|
191
191
|
|
|
192
192
|
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.)
|
|
193
193
|
1. **Run the whole-diff code review.** Dispatch `/skill:requesting-code-review` against the worktree's full diff vs `main` (already covered in [The Process](#the-process) step "After all tasks"). Address Critical and Moderate findings before handoff. (Consumers wanting an in-flow project-specific audit re-add it as an explicit step in `.pi/gauntlet-overrides.md`, or run `/self-audit` manually.)
|
|
194
|
-
2. **
|
|
195
|
-
3.
|
|
196
|
-
4.
|
|
194
|
+
2. **Run the full verification set — once.** Read the plan header's `**Verification:**` line and run it: tests + style + format (a single bundling entrypoint, or the listed individual commands). Green output is the fresh evidence verify requires; this is the only full run before conformance — task and wave gates ran scoped commands only. After conformance fix rounds land, re-run the set before re-dispatching the gate.
|
|
195
|
+
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 name = `Gn: <short desc>`, lifecycle `pending → in_progress → complete` per gap); 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.
|
|
196
|
+
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.
|
|
197
|
+
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.
|
|
197
198
|
|
|
198
199
|
## Red Flags — STOP
|
|
199
200
|
|
|
@@ -208,6 +209,8 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
|
|
|
208
209
|
- Pausing to "check in" between tasks (continuous execution rule)
|
|
209
210
|
- Skipping the `Implementer Status` parse — treating every response as DONE
|
|
210
211
|
- Starting on main without explicit user consent
|
|
212
|
+
- Dispatching `code-reviewer` before every one of the wave's spec-review verdicts has landed (including fusing SR+CR into one parallel call)
|
|
213
|
+
- 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
|
|
211
214
|
|
|
212
215
|
## Integration
|
|
213
216
|
|
|
@@ -36,7 +36,7 @@ If you haven't run the verification command in this message, you cannot claim it
|
|
|
36
36
|
BEFORE claiming any status or expressing satisfaction:
|
|
37
37
|
|
|
38
38
|
1. IDENTIFY: What command proves this claim?
|
|
39
|
-
2. RUN: Execute the
|
|
39
|
+
2. RUN: Execute the full command for the claim from step 1 (fresh, complete, unabridged)
|
|
40
40
|
3. READ: Full output, check exit code, count failures
|
|
41
41
|
4. VERIFY: Does output confirm the claim?
|
|
42
42
|
- If NO: State actual status with evidence
|
|
@@ -46,6 +46,8 @@ BEFORE claiming any status or expressing satisfaction:
|
|
|
46
46
|
Skip any step = lying, not verifying
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Evidence is proportionate to the claim: a task or wave claim is proven by the commands the plan declares for it — a scoped run is **complete** evidence for a scoped claim. Phase-completion and ship claims require the project's full verification entrypoint (tests + style + format), run once at verify before the conformance gate.
|
|
50
|
+
|
|
49
51
|
## Common Failures
|
|
50
52
|
|
|
51
53
|
| Claim | Requires | Not Sufficient |
|
|
@@ -69,7 +71,7 @@ Skip any step = lying, not verifying
|
|
|
69
71
|
| "Linter passed" | Linter ≠ compiler |
|
|
70
72
|
| "Agent said success" | Verify independently |
|
|
71
73
|
| "I'm tired" | Exhaustion ≠ excuse |
|
|
72
|
-
| "Partial check is enough" | Partial
|
|
74
|
+
| "Partial check is enough" | Partial = less than the claim requires; scoped-for-scoped is complete |
|
|
73
75
|
| "Different words so rule doesn't apply" | Spirit over letter |
|
|
74
76
|
|
|
75
77
|
## Red Flags - STOP
|
|
@@ -152,7 +154,7 @@ From recurring failure modes:
|
|
|
152
154
|
|
|
153
155
|
## Enforcement
|
|
154
156
|
|
|
155
|
-
The `verify-before-ship` extension shipped by pi-gauntlet watches `git
|
|
157
|
+
The `verify-before-ship` extension shipped by pi-gauntlet watches `git push` and `gh pr create`. If you have not run a passing recognised verification command since your last source-file edit in this session, an advisory warning is injected into the tool result. The warning clears automatically after a fresh passing run.
|
|
156
158
|
|
|
157
159
|
Defaults recognise `make ci`, `make test`, `npm test`, `pnpm test`, `yarn test`, `pytest`, `rspec`, `cargo test`, `go test`. Projects can override (or narrow) the list via `settings.json#piGauntlet.verifyBeforeShip.testCommands`.
|
|
158
160
|
|
|
@@ -55,7 +55,7 @@ On a fresh resume:
|
|
|
55
55
|
|
|
56
56
|
This is the existing arming mechanism, not new mechanics: the `start` arms `gauntletEntered`, `skip` preserves it, session replay reconstructs it, `reset` disarms. The sequence already performed this skill's own `start plan` call — **do not** issue a second one (a repeat `start` on the in_progress phase is a no-op reset that re-clears the warn-once guard ledger).
|
|
57
57
|
|
|
58
|
-
Then continue with the normal flow below (Scope Check onward).
|
|
58
|
+
Then continue with the normal flow below (Scope Check onward, including Recon).
|
|
59
59
|
|
|
60
60
|
**Writing a handoff doc** (from the producing session): name `/skill:writing-plans` as the entry point — never a phase past planning, since this gesture only arms through `start plan` — give the spec's path, and assert its approval status.
|
|
61
61
|
|
|
@@ -80,6 +80,26 @@ If yes, decompose into separate plans and call it out:
|
|
|
80
80
|
|
|
81
81
|
A single plan should land in one PR worth of work. Multi-PR sequences get separate plans.
|
|
82
82
|
|
|
83
|
+
## Recon (mandatory)
|
|
84
|
+
|
|
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
|
+
|
|
87
|
+
```
|
|
88
|
+
subagent({ agent: "scout", context: "fresh", cwd: "<abs worktree path>",
|
|
89
|
+
phase: "plan-recon", output: "<abs plan path — same filename as the spec, per the table above>",
|
|
90
|
+
task: <the fixed template below, with the spec path filled> })
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
> Recon for implementation planning. Read the approved spec at `<abs spec path>` - it is the single source of truth for what is being built. Also read the repo's `AGENTS.md` and, if present, `.pi/gauntlet-overrides.md` for conventions. Build an implementation map for the spec: exact file paths to create/modify/delete; existing call sites and tests with line ranges; conventions and patterns the plan must match; the project's test runner and the exact scoped-invocation form for running individual test files (derived from the repo's Makefile/bin/config and the overrides file); the style/lint and auto-format commands in both scoped per-file form and repo-wide form (same sources); separately, the full-suite verification entrypoint and whether it bundles style/format checks. Flag any spec claim that contradicts the code. Read-only recon: do not edit any file except writing your report to your output path. Start your report with the line `# CONTEXT DRAFT - NOT A PLAN - fully replaced at plan-writing` verbatim. End with an "Open questions that matter for the plan" section. Compact handoff, not a dump.
|
|
94
|
+
|
|
95
|
+
Consumption:
|
|
96
|
+
|
|
97
|
+
- `Read` the draft at the plan path before mapping files — the on-disk copy is canonical (prune-proof, restart-proof).
|
|
98
|
+
- The draft is a helper, not a fence: verify load-bearing claims against real code before planning against them.
|
|
99
|
+
- Plan-writing is a **full-replacement `write`** at the same path. Re-read the draft in the same turn immediately before the overwrite. After the write, confirm line 1 is no longer the draft marker before self-review and handoff.
|
|
100
|
+
- **Degradation:** the scout failed when its task errored or the output file is missing or empty. Proceed from your own reads with a one-line note; never block. If the file is absent, no marker check applies at the overwrite.
|
|
101
|
+
- **Re-entry:** re-dispatching recon overwrites whatever the plan path holds — including a committed prior plan (recoverable from git history) or an uncommitted one (destroyed). Re-planning is a deliberate overwrite.
|
|
102
|
+
|
|
83
103
|
## File Structure
|
|
84
104
|
|
|
85
105
|
**Before drafting tasks, map the files.**
|
|
@@ -113,6 +133,8 @@ Group tasks into **waves** so the executor can parallelize independent work (see
|
|
|
113
133
|
|
|
114
134
|
**File-ownership contract.** The per-task `**Files:**` block *is* the ownership declaration — no new syntax. Rule: **within a wave, the union of every task's declared paths must be pairwise disjoint.** Globs are allowed for `Modify` when exact paths are unknown, but must not overlap another same-wave task's paths. A task that must touch another's file belongs in a later wave.
|
|
115
135
|
|
|
136
|
+
**Test-command contract.** Every code-touching wave declares at least one scoped test command across its tasks' steps. A wave with zero test commands is legal only when every task's `Files:` block is documentation-only (the trailing doc-only wave below).
|
|
137
|
+
|
|
116
138
|
**Runtime-resource disjointness.** File-disjoint is necessary but not sufficient: two tasks with disjoint files that both mutate the same DB, bind the same port, or share a fixture are **not** parallel-safe and must land in different waves. The executor auto-selects parallel for *every* multi-task wave, so this grouping is the sole parallel-safety guarantee — there is no selection-time judgment downstream. No new mandatory per-task syntax; when a shared runtime resource is the reason two file-disjoint tasks sit in different waves, record it in an inline note on the later wave.
|
|
117
139
|
|
|
118
140
|
**Doc tasks.** Doc updates are real plan tasks, not an afterthought. Task-local docs (a doc that only describes the file(s) a task already touches) ride with that task. Cross-cutting or index docs (README, `AGENTS.md`, topic guides, taxonomy indexes) sequence into a dedicated trailing doc-only wave — last wave by convention, file-disjoint from every code task so the pairwise-disjoint wave contract holds.
|
|
@@ -143,6 +165,7 @@ Each step is **one action, 2-5 minutes**:
|
|
|
143
165
|
- "Run it, confirm it fails" — step
|
|
144
166
|
- "Implement minimal code to pass" — step
|
|
145
167
|
- "Run tests, confirm green" — step
|
|
168
|
+
- "Format & lint the task's files" — step
|
|
146
169
|
- "Commit" — step
|
|
147
170
|
|
|
148
171
|
## Plan Document Header
|
|
@@ -160,11 +183,15 @@ Each step is **one action, 2-5 minutes**:
|
|
|
160
183
|
|
|
161
184
|
**Spec:** `<project>/doc/specs/<same-filename-as-this-plan>.md`
|
|
162
185
|
|
|
186
|
+
**Verification:** `<full verification command set — tests + style + format; a single bundling entrypoint, or the listed individual commands; from the recon report / project overrides>`
|
|
187
|
+
|
|
163
188
|
**Linear:** `E-XXXX` (omit if no ticket)
|
|
164
189
|
|
|
165
190
|
---
|
|
166
191
|
```
|
|
167
192
|
|
|
193
|
+
The `**Verification:**` line is the **only** place the full verification entrypoint may appear — never in any task or wave step. The verify phase reads it from the plan instead of re-deriving it; execution runs scoped commands only.
|
|
194
|
+
|
|
168
195
|
## Task Structure
|
|
169
196
|
|
|
170
197
|
Each task uses `- [ ]` checkbox steps so execution tools (and humans) can track progress.
|
|
@@ -204,7 +231,12 @@ Each task uses `- [ ]` checkbox steps so execution tools (and humans) can track
|
|
|
204
231
|
Run: `uv run pytest tests/path/test.py::test_name -v`
|
|
205
232
|
Expected: PASS
|
|
206
233
|
|
|
207
|
-
- [ ] **Step 5:
|
|
234
|
+
- [ ] **Step 5: Format & lint the task's files**
|
|
235
|
+
|
|
236
|
+
Run: `<scoped fmt/lint command from recon> exact/path/to/file.py tests/exact/path/to/test.py`
|
|
237
|
+
Expected: no diff after re-run / 0 offenses
|
|
238
|
+
|
|
239
|
+
- [ ] **Step 6: Commit**
|
|
208
240
|
|
|
209
241
|
```bash
|
|
210
242
|
git add tests/path/test.py src/path/file.py
|
|
@@ -212,6 +244,8 @@ Each task uses `- [ ]` checkbox steps so execution tools (and humans) can track
|
|
|
212
244
|
```
|
|
213
245
|
```
|
|
214
246
|
|
|
247
|
+
Every code task carries this step (red -> green -> fmt/lint -> commit). Doc-only tasks omit it unless the project formats Markdown.
|
|
248
|
+
|
|
215
249
|
## No Placeholders
|
|
216
250
|
|
|
217
251
|
Every plan failure mode:
|
|
@@ -235,6 +269,8 @@ After drafting the plan and before announcing it complete, run three checks your
|
|
|
235
269
|
- **Placeholder scan.** Grep the doc for `TODO`, `TBD`, `xxx`, `[fill in]`, `<example>`, `etc.`, "probably", "something like". Resolve or convert each into an explicit Open Question.
|
|
236
270
|
- **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.
|
|
237
271
|
- **Wave disjointness.** For every multi-task wave, confirm the tasks' `Files:` sets are pairwise disjoint **and** that no two tasks contend on a shared mutable runtime resource (DB/schema, port, fixture, external service, shared temp path). Either kind of overlap = mis-grouped wave; split or re-order before handoff.
|
|
272
|
+
- **Scoped-test coverage.** Every code-touching wave declares at least one scoped test command; only doc-only waves may have none.
|
|
273
|
+
- **Header-only entrypoint.** The full verification entrypoint appears only in the plan header's `**Verification:**` line. Grep the task body for the header's command string — expect zero hits.
|
|
238
274
|
|
|
239
275
|
Fix what this review finds before handoff.
|
|
240
276
|
|
|
@@ -183,7 +183,7 @@ If a skill leans on pi capabilities, name them explicitly:
|
|
|
183
183
|
|---|---|
|
|
184
184
|
| Plan/phase persistence | `plan_tracker` tool (provided by the `pi-gauntlet` package's `plan-tracker` extension) |
|
|
185
185
|
| Progressive disclosure | Direct `read` of `reference/<topic>.md` paths named inline in SKILL.md |
|
|
186
|
-
| Runtime enforcement | `verify-before-ship` extension from `pi-gauntlet` (advisory warning before `git
|
|
186
|
+
| Runtime enforcement | `verify-before-ship` extension from `pi-gauntlet` (advisory warning before `git push` / `gh pr create` when no canonical verification command has succeeded since the last source edit) |
|
|
187
187
|
| Subagent dispatch | `subagent` tool from `pi-cohort`; baseline subagents from `pi-gauntlet` are `implementer`, `code-reviewer`, `spec-reviewer`, `conformance-reviewer`. Consumer repos can add project-specific subagents under `.pi/agents/`. |
|
|
188
188
|
|
|
189
189
|
Don't invent capabilities. Don't reference Claude Code's `Task` tool, OpenCode hooks, or Codex `spawn_agent` unless the skill is explicitly for that harness.
|