pi-gauntlet 4.3.1 → 4.4.1
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 +66 -0
- package/README.md +71 -211
- package/agents/conformance-reviewer.md +4 -4
- package/extensions/lib/phase-tracker-helpers.test.ts +120 -0
- package/extensions/lib/phase-tracker-helpers.ts +97 -0
- package/extensions/phase-tracker.ts +103 -16
- package/extensions/plan-tracker.ts +1 -1
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +72 -25
- package/skills/brainstorming/gatherer.md +117 -0
- package/skills/finishing-a-development-branch/SKILL.md +46 -9
- package/skills/roasting-the-spec/SKILL.md +25 -16
- package/skills/subagent-driven-development/SKILL.md +4 -4
- package/skills/verification-before-completion/SKILL.md +1 -1
- package/skills/verification-before-completion/reference/conformance-check.md +131 -118
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic for phase-tracker's substep action and marker commit guard.
|
|
3
|
+
* No fs/git access here — file access is injected so node --test covers it
|
|
4
|
+
* (registered in scripts/ci.mjs alongside the settings resolvers).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
export const CONTEXT_DRAFT_MARKER = "# CONTEXT DRAFT - NOT A SPEC - fully replaced at spec-writing";
|
|
10
|
+
|
|
11
|
+
export type SubstepCheck = { ok: true } | { ok: false; error: string };
|
|
12
|
+
|
|
13
|
+
export function checkSubstep(phaseStatus: string): SubstepCheck {
|
|
14
|
+
if (phaseStatus === "in_progress") return { ok: true };
|
|
15
|
+
return { ok: false, error: `substep requires an in_progress phase (status is ${phaseStatus})` };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function phaseLabel(name: string, substep?: string): string {
|
|
19
|
+
return substep ? `${name}(${substep})` : name;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Same statement-start anchor as the branch guards in phase-tracker.ts. Exported
|
|
23
|
+
// so Wave 2 wiring in phase-tracker.ts can drop its duplicate copy.
|
|
24
|
+
export const STMT_START = "(?:^|[\\n;&|(])\\s*";
|
|
25
|
+
// `git commit`, tolerating any global flags between `git` and `commit` (e.g.
|
|
26
|
+
// `-c user.email=x`, common in CI per-commit identity). Group 1 captures the
|
|
27
|
+
// whole flags span as ONE opaque block rather than -C directly: nesting a
|
|
28
|
+
// capture inside a repeated alternation resets it to undefined on iterations
|
|
29
|
+
// that take the other branch (JS regex semantics), so -C after a later flag
|
|
30
|
+
// (e.g. `-C /wt -c user.email=x commit`) would silently lose its capture.
|
|
31
|
+
// DASH_C below re-extracts -C from that span once, outside any repetition.
|
|
32
|
+
// `commit` must be followed by whitespace or end-of-string, not just a word
|
|
33
|
+
// boundary, so `commit-graph` / `commit-tree` don't false-positive.
|
|
34
|
+
const GIT_COMMIT = new RegExp(
|
|
35
|
+
STMT_START + "git\\s+((?:-\\S+(?:\\s+\\S+)?\\s+)*)commit(?=\\s|$)",
|
|
36
|
+
);
|
|
37
|
+
const DASH_C = /(?:^|\s)-C\s+(\S+)/;
|
|
38
|
+
// Global, lookaround-delimited (not consuming) so adjacent `cd a && cd b &&`
|
|
39
|
+
// statements don't eat each other's anchor/`&&` and hide the second match.
|
|
40
|
+
// parseGitCommit picks the LAST cd before the matched git-commit position, so
|
|
41
|
+
// `cd /a && cd /b && git commit` resolves against /b, not the first cd found.
|
|
42
|
+
const LEADING_CD = /(?<=^|[\n;&|(])\s*cd\s+([^\s)]+)\s*(?=&&|\|\||[;)\n]|$)/g;
|
|
43
|
+
|
|
44
|
+
export interface CommitForm {
|
|
45
|
+
cPath: string | undefined;
|
|
46
|
+
cdPath: string | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Textual match anchored at statement starts (^ ; & | ( or newline). Quoted text
|
|
50
|
+
// can still match when preceded by such a char (e.g. sh -c 'x; git commit') —
|
|
51
|
+
// accepted heuristic, same tolerance as the existing Guard 3 mutation checks.
|
|
52
|
+
export function parseGitCommit(command: string): CommitForm | undefined {
|
|
53
|
+
const m = GIT_COMMIT.exec(command);
|
|
54
|
+
if (!m) return undefined;
|
|
55
|
+
let cdPath: string | undefined;
|
|
56
|
+
for (const cd of command.matchAll(LEADING_CD)) {
|
|
57
|
+
if (cd.index! < m.index!) cdPath = cd[1];
|
|
58
|
+
}
|
|
59
|
+
return { cPath: DASH_C.exec(m[1])?.[1], cdPath };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveRepoDir(form: CommitForm, sessionCwd: string): string {
|
|
63
|
+
const base = form.cdPath ? resolve(sessionCwd, form.cdPath) : sessionCwd;
|
|
64
|
+
return form.cPath ? resolve(base, form.cPath) : base;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Line-1 anchoring prevents false positives on specs that QUOTE the marker.
|
|
68
|
+
// Working-tree read (not index) is an accepted false-negative window for a
|
|
69
|
+
// backstop whose primary check lives in the brainstorming skill.
|
|
70
|
+
export function findMarkerFile(
|
|
71
|
+
repoDir: string,
|
|
72
|
+
specDirs: string[],
|
|
73
|
+
listFiles: (dir: string) => string[],
|
|
74
|
+
readFirstLine: (file: string) => string | undefined,
|
|
75
|
+
): string | undefined {
|
|
76
|
+
for (const dir of specDirs) {
|
|
77
|
+
for (const file of listFiles(resolve(repoDir, dir))) {
|
|
78
|
+
if (readFirstLine(file) === CONTEXT_DRAFT_MARKER) return file;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Transitions always build fresh state: a substep never survives start/complete/skip/reset.
|
|
85
|
+
export function transitionPhaseState(status: string, reason?: string): { status: string; reason?: string } {
|
|
86
|
+
return reason === undefined ? { status } : { status, reason };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function markerGuardApplies(flowGuardsEnforced: boolean, brainstormStatus: string): boolean {
|
|
90
|
+
return flowGuardsEnforced && brainstormStatus === "in_progress";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const markerBlockReason = (file: string): string =>
|
|
94
|
+
`Blocked: ${file} still begins with the context-draft marker - the spec-writing ` +
|
|
95
|
+
`overwrite has not happened. Overwrite the draft with the real spec (write tool, ` +
|
|
96
|
+
`full replacement) before committing. ` +
|
|
97
|
+
`To override, set piGauntlet.flowGuards.enforce: false.`;
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { execSync } from "node:child_process";
|
|
13
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
13
15
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
14
16
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
15
17
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -21,6 +23,16 @@ import {
|
|
|
21
23
|
settingsErrorWarning,
|
|
22
24
|
} from "./lib/gauntlet-settings.ts";
|
|
23
25
|
import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
|
|
26
|
+
import {
|
|
27
|
+
checkSubstep,
|
|
28
|
+
findMarkerFile,
|
|
29
|
+
markerBlockReason,
|
|
30
|
+
parseGitCommit,
|
|
31
|
+
phaseLabel,
|
|
32
|
+
resolveRepoDir,
|
|
33
|
+
STMT_START,
|
|
34
|
+
transitionPhaseState,
|
|
35
|
+
} from "./lib/phase-tracker-helpers.ts";
|
|
24
36
|
|
|
25
37
|
const PHASES = ["brainstorm", "plan", "implement", "verify", "ship"] as const;
|
|
26
38
|
type Phase = (typeof PHASES)[number];
|
|
@@ -29,12 +41,13 @@ type PhaseStatus = "pending" | "in_progress" | "complete" | "skipped";
|
|
|
29
41
|
interface PhaseState {
|
|
30
42
|
status: PhaseStatus;
|
|
31
43
|
reason?: string;
|
|
44
|
+
substep?: string;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
type PhaseMap = Record<Phase, PhaseState>;
|
|
35
48
|
|
|
36
49
|
interface PhaseTrackerDetails {
|
|
37
|
-
action: "start" | "complete" | "skip" | "status" | "reset";
|
|
50
|
+
action: "start" | "complete" | "skip" | "status" | "reset" | "substep";
|
|
38
51
|
phases: PhaseMap;
|
|
39
52
|
error?: string;
|
|
40
53
|
}
|
|
@@ -58,12 +71,12 @@ const CLOSURE_GATE_ERROR =
|
|
|
58
71
|
' phase_tracker({ action: "skip", phase: "verify", reason: "<user waiver>" })';
|
|
59
72
|
|
|
60
73
|
const SHIP_ADVISORY =
|
|
61
|
-
"Verify is complete and ship is pending.
|
|
62
|
-
"
|
|
63
|
-
"/skill:finishing-a-development-branch
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
74
|
+
"Verify is complete and ship is pending. Verify may complete when the conformance\n" +
|
|
75
|
+
"verdict is CONFORMS, or every gap is either fixed or carried open as a deferred\n" +
|
|
76
|
+
"accept/rescope/unauthorized decision. Invoke /skill:finishing-a-development-branch\n" +
|
|
77
|
+
"now - do not add a 'ready to finish?' prompt; its disposition + squash/PR/keep/discard\n" +
|
|
78
|
+
"menu is the human gate that resolves any carried-open decision. Only reopen verify if\n" +
|
|
79
|
+
"a `fix` gap was left unresolved (neither fixed nor deferred).";
|
|
67
80
|
|
|
68
81
|
// --- Flow guards (spec 2026-06-17-gauntlet-flow-guards) ---
|
|
69
82
|
|
|
@@ -72,7 +85,6 @@ const GUARD_PHASES: Phase[] = ["brainstorm", "plan", "implement"];
|
|
|
72
85
|
// Guard 2 — branch ops in place. `git switch` never targets a file path;
|
|
73
86
|
// `git checkout -b/-B` is explicit branch creation. Bare `git checkout <x>`
|
|
74
87
|
// is excluded (ambiguous with file checkout). `git worktree ...` is exempt.
|
|
75
|
-
const STMT_START = "(?:^|[\\n;&|(])\\s*";
|
|
76
88
|
const BRANCH_SWITCH = new RegExp(STMT_START + "git\\s+switch\\b");
|
|
77
89
|
const BRANCH_CHECKOUT = new RegExp(STMT_START + "git\\s+checkout\\s+-[bB]\\b");
|
|
78
90
|
const GIT_WORKTREE = /\bgit\s+worktree\b/;
|
|
@@ -155,7 +167,7 @@ const pathInSpecDirs = (rawPath: string, specDirs: string[]): boolean => {
|
|
|
155
167
|
};
|
|
156
168
|
|
|
157
169
|
const PhaseTrackerParams = Type.Object({
|
|
158
|
-
action: StringEnum(["start", "complete", "skip", "status", "reset"] as const, {
|
|
170
|
+
action: StringEnum(["start", "complete", "skip", "status", "reset", "substep"] as const, {
|
|
159
171
|
description: "Action to perform",
|
|
160
172
|
}),
|
|
161
173
|
phase: Type.Optional(
|
|
@@ -173,6 +185,11 @@ const PhaseTrackerParams = Type.Object({
|
|
|
173
185
|
description: "Reset and re-start a phase that is already complete or skipped (rare; default false)",
|
|
174
186
|
}),
|
|
175
187
|
),
|
|
188
|
+
substep: Type.Optional(
|
|
189
|
+
Type.Union([Type.String(), Type.Null()], {
|
|
190
|
+
description: "Substep label for action=substep on an in_progress phase; null or omitted clears it",
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
176
193
|
});
|
|
177
194
|
|
|
178
195
|
export type PhaseTrackerInput = Static<typeof PhaseTrackerParams>;
|
|
@@ -201,7 +218,8 @@ function hasActivity(phases: PhaseMap): boolean {
|
|
|
201
218
|
function formatWidget(phases: PhaseMap, theme: Theme): string {
|
|
202
219
|
const parts = PHASES.map((p) => {
|
|
203
220
|
const icon = phaseIcon(phases[p].status, theme);
|
|
204
|
-
const
|
|
221
|
+
const labeled = phaseLabel(p, phases[p].status === "in_progress" ? phases[p].substep : undefined);
|
|
222
|
+
const name = phases[p].status === "skipped" ? theme.fg("dim", labeled) : labeled;
|
|
205
223
|
return `${icon} ${name}`;
|
|
206
224
|
});
|
|
207
225
|
return `${theme.fg("muted", "Phases:")} ${parts.join(theme.fg("dim", " → "))}`;
|
|
@@ -213,7 +231,7 @@ function formatStatus(phases: PhaseMap): string {
|
|
|
213
231
|
const s = phases[p];
|
|
214
232
|
const icon = s.status === "complete" ? "✓" : s.status === "in_progress" ? "→" : s.status === "skipped" ? "⊘" : "○";
|
|
215
233
|
const suffix = s.reason ? ` (${s.reason})` : "";
|
|
216
|
-
lines.push(` ${icon} ${p}${suffix}`);
|
|
234
|
+
lines.push(` ${icon} ${phaseLabel(p, s.status === "in_progress" ? s.substep : undefined)}${suffix}`);
|
|
217
235
|
}
|
|
218
236
|
return lines.join("\n");
|
|
219
237
|
}
|
|
@@ -386,6 +404,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
386
404
|
return { block: true, reason: branchBlockReason(gphase) };
|
|
387
405
|
}
|
|
388
406
|
|
|
407
|
+
// Marker commit guard — the context draft (brainstorming gather step) must be
|
|
408
|
+
// overwritten by the real spec before any commit lands. Backstop to the skill's
|
|
409
|
+
// own post-write check. Blocked, like Guard 2; skipped entirely when
|
|
410
|
+
// flowGuards.enforce is false (checked above).
|
|
411
|
+
// gating contract: markerGuardApplies (see helpers) - enforce checked once above
|
|
412
|
+
// (flowGuardsEnforced()); re-checking it here would be belt-and-suspenders.
|
|
413
|
+
if (phases.brainstorm.status === "in_progress") {
|
|
414
|
+
const commit = parseGitCommit(command);
|
|
415
|
+
if (commit) {
|
|
416
|
+
const repoDir = resolveRepoDir(commit, ctx.cwd);
|
|
417
|
+
const listFiles = (dir: string): string[] => {
|
|
418
|
+
const walk = (d: string): string[] => {
|
|
419
|
+
return readdirSync(d, { withFileTypes: true }).flatMap((e) => {
|
|
420
|
+
const p = join(d, e.name);
|
|
421
|
+
return e.isDirectory() ? walk(p) : e.isFile() ? [p] : [];
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
try {
|
|
425
|
+
return walk(dir);
|
|
426
|
+
} catch {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const readFirstLine = (file: string): string | undefined => {
|
|
431
|
+
try {
|
|
432
|
+
return readFileSync(file, "utf8").split("\n", 1)[0];
|
|
433
|
+
} catch {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
const hit = findMarkerFile(repoDir, specDirs(), listFiles, readFirstLine);
|
|
438
|
+
if (hit) return { block: true, reason: markerBlockReason(hit) };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
389
442
|
// Guard 3 — bash mutation outside the spec dir during brainstorm.
|
|
390
443
|
if (phases.brainstorm.status === "in_progress" && !firedGuards.get("brainstorm-write")) {
|
|
391
444
|
// Redirect target is cleanly extractable: judge it directly against the spec dirs,
|
|
@@ -474,7 +527,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
474
527
|
description:
|
|
475
528
|
"Track workflow phase progress (brainstorm → plan → implement → verify → ship). " +
|
|
476
529
|
"Actions: start (mark phase in_progress), complete (mark phase complete), " +
|
|
477
|
-
"skip (mark phase skipped with reason), status (show all phases), reset (clear all phases).",
|
|
530
|
+
"skip (mark phase skipped with reason), status (show all phases), reset (clear all phases), substep (set/clear a substep label on an in_progress phase).",
|
|
478
531
|
parameters: PhaseTrackerParams,
|
|
479
532
|
|
|
480
533
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -518,7 +571,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
518
571
|
} as PhaseTrackerDetails,
|
|
519
572
|
};
|
|
520
573
|
}
|
|
521
|
-
phases = { ...phases, [params.phase]:
|
|
574
|
+
phases = { ...phases, [params.phase]: transitionPhaseState("in_progress") as PhaseState };
|
|
522
575
|
firedGuards.clear();
|
|
523
576
|
updateWidget(ctx);
|
|
524
577
|
return {
|
|
@@ -559,7 +612,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
559
612
|
} as PhaseTrackerDetails,
|
|
560
613
|
};
|
|
561
614
|
}
|
|
562
|
-
phases = { ...phases, [params.phase]:
|
|
615
|
+
phases = { ...phases, [params.phase]: transitionPhaseState("complete") as PhaseState };
|
|
563
616
|
firedGuards.clear();
|
|
564
617
|
updateWidget(ctx);
|
|
565
618
|
const advisory =
|
|
@@ -591,7 +644,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
591
644
|
};
|
|
592
645
|
}
|
|
593
646
|
const reason = params.reason;
|
|
594
|
-
phases = { ...phases, [params.phase]:
|
|
647
|
+
phases = { ...phases, [params.phase]: transitionPhaseState("skipped", reason) as PhaseState };
|
|
595
648
|
firedGuards.clear();
|
|
596
649
|
updateWidget(ctx);
|
|
597
650
|
return {
|
|
@@ -602,6 +655,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
602
655
|
};
|
|
603
656
|
}
|
|
604
657
|
|
|
658
|
+
case "substep": {
|
|
659
|
+
if (!params.phase) {
|
|
660
|
+
return {
|
|
661
|
+
content: [{ type: "text", text: "Error: phase required for substep" }],
|
|
662
|
+
details: { action: "substep", phases: { ...phases }, error: "phase required" } as PhaseTrackerDetails,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const check = checkSubstep(phases[params.phase].status);
|
|
666
|
+
if (!check.ok) {
|
|
667
|
+
return {
|
|
668
|
+
content: [{ type: "text", text: `Error: phase "${params.phase}": ${check.error}` }],
|
|
669
|
+
details: { action: "substep", phases: { ...phases }, error: check.error } as PhaseTrackerDetails,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
const entry: PhaseState = { status: "in_progress" };
|
|
673
|
+
if (typeof params.substep === "string" && params.substep.trim()) entry.substep = params.substep.trim();
|
|
674
|
+
phases = { ...phases, [params.phase]: entry };
|
|
675
|
+
updateWidget(ctx);
|
|
676
|
+
const label = entry.substep
|
|
677
|
+
? `Phase "${params.phase}" substep → ${entry.substep}`
|
|
678
|
+
: `Phase "${params.phase}" substep cleared`;
|
|
679
|
+
return {
|
|
680
|
+
content: [{ type: "text", text: `${label}\n${formatStatus(phases)}` }],
|
|
681
|
+
details: { action: "substep", phases: { ...phases } } as PhaseTrackerDetails,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
605
685
|
case "status": {
|
|
606
686
|
return {
|
|
607
687
|
content: [{ type: "text", text: formatStatus(phases) }],
|
|
@@ -610,7 +690,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
610
690
|
}
|
|
611
691
|
|
|
612
692
|
case "reset": {
|
|
613
|
-
phases =
|
|
693
|
+
phases = Object.fromEntries(
|
|
694
|
+
PHASES.map((p) => [p, transitionPhaseState("pending")]),
|
|
695
|
+
) as PhaseMap;
|
|
614
696
|
conformanceDispatched = false;
|
|
615
697
|
firedGuards.clear();
|
|
616
698
|
updateWidget(ctx);
|
|
@@ -667,6 +749,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
667
749
|
);
|
|
668
750
|
case "skip":
|
|
669
751
|
return new Text(theme.fg("dim", "⊘ ") + theme.fg("muted", "phase skipped"), 0, 0);
|
|
752
|
+
case "substep": {
|
|
753
|
+
const active = PHASES.find((ph) => p[ph].status === "in_progress");
|
|
754
|
+
const label = active ? phaseLabel(active, p[active].substep) : "";
|
|
755
|
+
return new Text(theme.fg("warning", "→ ") + theme.fg("muted", label), 0, 0);
|
|
756
|
+
}
|
|
670
757
|
case "status": {
|
|
671
758
|
let text = theme.fg("muted", "Phases:");
|
|
672
759
|
for (const ph of PHASES) {
|
|
@@ -129,7 +129,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
129
129
|
name: "plan_tracker",
|
|
130
130
|
label: "Plan Tracker",
|
|
131
131
|
description:
|
|
132
|
-
"Track progress while EXECUTING an implementation plan
|
|
132
|
+
"Track progress while EXECUTING an implementation plan (the implement phase) or a verify-phase conformance fix wave. Actions: init (set task list), update (change task status), status (show current state), clear (remove plan). Do NOT use for brainstorming, research, or planning checklists: those phases are open-ended and a bounded task list misrepresents them as a fixed N-step process.",
|
|
133
133
|
parameters: PlanTrackerParams,
|
|
134
134
|
|
|
135
135
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
package/package.json
CHANGED
|
@@ -49,16 +49,20 @@ Work through the items below **in order**. This is your own checklist to follow,
|
|
|
49
49
|
|
|
50
50
|
Re-entering while a brainstorm is already in progress is safe: mid-brainstorm there are no tasks or later phases to lose, so the reset just re-establishes the same clean slate.
|
|
51
51
|
2. **Set up the worktree** — see [Worktree First](#worktree-first)
|
|
52
|
-
3. **
|
|
53
|
-
|
|
52
|
+
3. **Gather context** — follow `gatherer.md` (same directory): set substep `gather`,
|
|
53
|
+
dispatch the builders, assemble the context draft at the spec path, clear the
|
|
54
|
+
substep. Unconditional, foreground, no user interaction — the next thing the user
|
|
55
|
+
sees is a questionary question. This produces the draft; the step below consumes it.
|
|
56
|
+
4. **Understand the idea against the draft** — `Read` the draft, verify load-bearing
|
|
57
|
+
claims against real code, ask questions one at a time, append citable findings
|
|
54
58
|
5. **Propose 2-3 approaches** — with trade-offs and a recommendation
|
|
55
59
|
6. **Present the design** — in sections, get approval after each
|
|
56
60
|
7. **Write the spec** — to `doc/specs/` (see [Filename Convention](#filename-convention))
|
|
57
61
|
8. **Spec self-review (lint)** — placeholder scan + internal consistency + documentation named, run inline
|
|
58
|
-
9. **Critique pass (auto-dispatched)** — scope + ambiguity; the spec council via `/skill:roasting-the-spec` when `gauntlet_setting` returns verdict `council
|
|
59
|
-
10. **Re-run placeholder scan** — after the critique pass returns,
|
|
60
|
-
11. **Generate spec summary** — dispatch a fresh, spec-only `spec-summarizer` writing to an absolute temp-dir path via `outputMode: "file-only"`, then `Read` that file back as the **last content-producing** tool call before composing the gate and render its contents **verbatim** at the top of the gate message — do not paraphrase, condense, re-section, or rewrite it (see [User Review Gate](#user-review-gate)); this is part of the existing gate, not a new one
|
|
61
|
-
12. **User review gate** — user reviews the
|
|
62
|
+
9. **Critique pass (auto-dispatched)** — scope + ambiguity; the spec council via `/skill:roasting-the-spec` when `gauntlet_setting` returns verdict `council` (it applies its apply-set, including any external-ref inlining, to the spec before returning — see [Spec Council](#spec-council-optional)), else a fresh `worker` that applies its own fixes in place
|
|
63
|
+
10. **Re-run placeholder scan** — after the critique pass returns, re-scan the **applied** spec for placeholders its edits may have introduced; surface any ambiguity the critique could not safely resolve at the user gate
|
|
64
|
+
11. **Generate spec summary** — dispatch a fresh, spec-only `spec-summarizer` over the **final (post-apply)** spec, writing to an absolute temp-dir path via `outputMode: "file-only"`, then `Read` that file back as the **last content-producing** tool call before composing the gate and render its contents **verbatim** at the top of the gate message — do not paraphrase, condense, re-section, or rewrite it (see [User Review Gate](#user-review-gate)); this is part of the existing gate, not a new one
|
|
65
|
+
12. **User review gate** — user reviews the applied spec's verbatim summary plus the council audit (Applied/Deferred/Rejected), with a revert valve for any applied council edit
|
|
62
66
|
13. **Transition** — only after approval, invoke `/skill:writing-plans`
|
|
63
67
|
|
|
64
68
|
## Project Routing
|
|
@@ -112,12 +116,26 @@ Don't try to design a multi-subsystem monolith in one spec doc.
|
|
|
112
116
|
|
|
113
117
|
### 3. Understand the idea
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
The gather step (see `gatherer.md`) has already assembled a context draft at the spec
|
|
120
|
+
path.
|
|
121
|
+
|
|
122
|
+
- `Read` the draft **unconditionally before composing question one**. The on-disk
|
|
123
|
+
copy is canonical — this one rule defeats both a turn-boundary prune after assembly
|
|
124
|
+
and a session restart.
|
|
125
|
+
- The draft is a **helper, not a fence**: judgment still drives exploration. Verify
|
|
126
|
+
load-bearing claims (schemas, contracts, the code being changed) against real code
|
|
127
|
+
via targeted reads (`read_symbol`-grade, not scout's paraphrase) before designing
|
|
128
|
+
against them.
|
|
129
|
+
- **Check if the codebase or ecosystem already solves this** — the draft's recon
|
|
130
|
+
section starts that answer; confirm it before designing from scratch.
|
|
131
|
+
- Ask questions **one at a time** to refine the idea. Prefer multiple-choice; one
|
|
132
|
+
question per message. Focus on: purpose, constraints, success criteria, who/what
|
|
133
|
+
it touches.
|
|
134
|
+
- **Append bar:** append to the draft's `## Appended during questionary` only
|
|
135
|
+
findings the spec will cite — schema shapes, hard constraints, ticket-vs-code
|
|
136
|
+
contradictions, user answers that changed scope. Not a log of every grep.
|
|
137
|
+
(Appending uses `edit`; the `edit` prohibition in the spec-writing step applies
|
|
138
|
+
only there.)
|
|
121
139
|
|
|
122
140
|
### 4. Explore approaches
|
|
123
141
|
|
|
@@ -197,8 +215,25 @@ Spec lives in the project's `doc/specs/` (see [Project Routing](#project-routing
|
|
|
197
215
|
|
|
198
216
|
`<topic>` is a short kebab-case slug (3–6 words). Do **not** append `-design` or any other suffix.
|
|
199
217
|
|
|
218
|
+
The slug is minted **once, at gather time**, from the initial prompt; the spec-writing
|
|
219
|
+
overwrite reuses the path. If the questionary invalidated the slug, rename at
|
|
220
|
+
spec-writing: write the spec at the new path **and delete the old draft file**
|
|
221
|
+
(nothing was committed, so this is free).
|
|
222
|
+
|
|
200
223
|
## Spec Self-Review (Before User Review Gate)
|
|
201
224
|
|
|
225
|
+
Spec-writing replaces the context draft, in this exact order:
|
|
226
|
+
|
|
227
|
+
1. `Read` the draft in full — **immediately before** the overwrite. Without this, a
|
|
228
|
+
pruned questionary plus a full-replacement `write` destroys the only copy of the
|
|
229
|
+
gathered context at the moment it feeds the spec.
|
|
230
|
+
2. Write the spec with the `write` tool (**full replacement**) at the spec path.
|
|
231
|
+
Using `edit` at this step is a red flag.
|
|
232
|
+
3. **Immediately after the write**, confirm line 1 of the file is no longer
|
|
233
|
+
`# CONTEXT DRAFT - NOT A SPEC - fully replaced at spec-writing` — before
|
|
234
|
+
dispatching lint, critique, council, or summarizer. The phase-tracker commit
|
|
235
|
+
guard is a backstop, not the primary check.
|
|
236
|
+
|
|
202
237
|
After writing the spec to `<project>/doc/specs/<filename>.md` (per [Filename Convention](#filename-convention)) and before showing it to the user, run a self-review pass. **Read all five bullets first, then act:** only the **first three** run here at the main loop (the inline lint); the **last two** (scope + ambiguity) do **not** run inline — they are the dispatched critique pass (checklist item 9). Do not apply scope/ambiguity edits yourself.
|
|
203
238
|
|
|
204
239
|
- **Placeholder scan.** Any `TODO`, `TBD`, `<fill in>`, `[example]`, `xxx`? Either resolve them or convert to explicit "Open Questions" with names.
|
|
@@ -209,7 +244,7 @@ After writing the spec to `<project>/doc/specs/<filename>.md` (per [Filename Con
|
|
|
209
244
|
|
|
210
245
|
The first three checks — **placeholder scan**, **internal consistency**, and **documentation named** — are the inline **lint**: run them here at the main loop and fix what they surface. The last two — **scope** and **ambiguity** — are **not** run inline; they are the **critique pass**, auto-dispatched (per [Spec Council](#spec-council-optional)):
|
|
211
246
|
|
|
212
|
-
- **Council** (`gauntlet_setting({ key: "specCouncil" })` returns verdict `council`) → invoke `/skill:roasting-the-spec`; it runs the critique and
|
|
247
|
+
- **Council** (`gauntlet_setting({ key: "specCouncil" })` returns verdict `council`) → invoke `/skill:roasting-the-spec`; it runs the critique, derives dispositions, and **applies the apply-set to the spec** (including inlining any `external-ref:` cluster it has context for) **before returning** — see that skill for apply mechanics. It returns a structured audit: `Applied:` / `Deferred:` / `Rejected:`.
|
|
213
248
|
- **Otherwise** → dispatch one fresh `worker` that applies the scope + ambiguity checks and fixes them in place:
|
|
214
249
|
|
|
215
250
|
```
|
|
@@ -218,23 +253,24 @@ The first three checks — **placeholder scan**, **internal consistency**, and *
|
|
|
218
253
|
"Read the spec at <abs path to doc/specs/...>. Edit ONLY that file. Apply two checks and\n" +
|
|
219
254
|
"fix what you find in place: (1) Scope — does every paragraph serve the goal? Cut filler;\n" +
|
|
220
255
|
"state out-of-scope explicitly. (2) Ambiguity — is every 'we should' a concrete decision?\n" +
|
|
221
|
-
"Replace 'we could probably' with 'we will'/'we won't'. Also
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
"ambiguity you could NOT
|
|
256
|
+
"Replace 'we could probably' with 'we will'/'we won't'. Also inline any load-bearing\n" +
|
|
257
|
+
"external reference (ticket AC, commit SHA, doc) already given to you in the problem\n" +
|
|
258
|
+
"statement above; if the spec relies on one not provided here, flag it (do NOT fetch) in\n" +
|
|
259
|
+
"your summary. Return a summary of what you changed, and flag any ambiguity you could NOT\n" +
|
|
260
|
+
"safely resolve." })
|
|
225
261
|
```
|
|
226
262
|
|
|
227
263
|
`worker`'s model resolves from `subagents.agentOverrides.worker.model` in `settings.json` (unset → inherits the main loop); the dispatch passes no `model:`.
|
|
228
264
|
|
|
229
|
-
|
|
265
|
+
Both paths apply their fixes **before returning** — the council auto-applies inside `/skill:roasting-the-spec`, the worker edits the spec file directly. After the critique pass returns, re-run the placeholder scan over the **applied** spec to catch anything the edits introduced. If the worker flagged ambiguities it could not safely resolve, surface them in the [User Review Gate](#user-review-gate) message so the user decides - the worker auto-applies fixes but never silently swallows an open question.
|
|
230
266
|
|
|
231
267
|
## Spec Council (Optional)
|
|
232
268
|
|
|
233
|
-
After the inline lint and before the user review gate, **brainstorming owns the critique-pass gate
|
|
269
|
+
After the inline lint and before the user review gate, **brainstorming owns the critique-pass gate**; council **apply mechanics** live in `/skill:roasting-the-spec` (single source of truth - link, don't restate). Resolve the council with `gauntlet_setting({ key: "specCouncil" })` - the tool returns the merged (repo-over-preset) value as `{ verdict, members, chair, malformed, warning, errors }`. **Do not** hand-roll a settings read. When `verdict` is `"council"`, the council *is* the critique pass - invoke `/skill:roasting-the-spec` automatically (no offer, no prompt), passing `members`/`chair`; it applies its apply-set and returns the audit (Applied/Deferred/Rejected). When `verdict` is `"worker"`, run the fresh-`worker` critique instead (see [Spec Self-Review](#spec-self-review-before-user-review-gate)). If `malformed` is true or `errors` is non-empty, emit the `warning`/error as one line, then branch strictly on `verdict` - `malformed` can accompany *either* verdict (e.g. a bad `chair` with valid `members` still returns `council`), so never infer the worker path from `malformed` alone. If `gauntlet_setting` is unavailable, stop and report - never fall back to a manual bash/JSON settings merge. The already-applied council edits (or the worker's in-place fixes) ride in the same worktree commit. The conceptual precedence rule lives in `verification-before-completion/reference/settings-precedence.md`.
|
|
234
270
|
|
|
235
271
|
## User Review Gate
|
|
236
272
|
|
|
237
|
-
After self-review
|
|
273
|
+
After self-review and the critique pass (council or worker, both already applied to the spec - see [Spec Council](#spec-council-optional)), dispatch the spec-only summarizer over the **applied** spec, then commit the spec on the worktree branch and stop. This is the **same** single human gate - the summary is folded into it, not a new gate.
|
|
238
274
|
|
|
239
275
|
Mint an absolute temp path outside the worktree (so it is never committed), then dispatch the summarizer on a fresh context, reading only the spec, writing to that path via file-only output (no `model:` - it inherits the main loop unless a preset sets `subagents.agentOverrides.spec-summarizer.model`):
|
|
240
276
|
|
|
@@ -250,7 +286,7 @@ subagent({ agent: "spec-summarizer", context: "fresh", cwd: "<abs worktree path,
|
|
|
250
286
|
|
|
251
287
|
`<SUMMARY_PATH>` above is a placeholder in the dispatch object; it means substitute the value of the shell variable `$SUMMARY_PATH` set above. The steps below use `$SUMMARY_PATH` (the shell form) once the value is in hand.
|
|
252
288
|
|
|
253
|
-
Then commit the spec — this commit is **unconditional**: the summary is only a gate aid, so a degraded or missing summary never blocks it. Evaluate the summary in two stages (the **Degrade path** referenced in each is defined just below):
|
|
289
|
+
Then commit the spec — this commit is **unconditional**: the summary is only a gate aid, so a degraded or missing summary never blocks it. If the council path ran, include its audit (`Applied:` / `Deferred:` / `Rejected:`, verbatim from `/skill:roasting-the-spec`'s return) in the **commit message body** - this is the durable, non-contractual record a finish-time revert reads back; the audit is never a committed spec section. Evaluate the summary in two stages (the **Degrade path** referenced in each is defined just below):
|
|
254
290
|
|
|
255
291
|
1. **From the dispatch tool result, before the `Read`.** If the result is **not** an `"Output saved to: <path> (<N> KB, <M> lines)"` reference (e.g. an exit-0 save error returns the full inline output plus an "Output file error" line — the prunable shape, no file to read), or the reference reports under ~500 bytes, or a size grossly disproportionate to the spec (under ~2% of its byte size), or over ~45 KB (the `Read` truncates at 50KB / 2000 lines, so a larger file cannot render whole) — skip the `Read` and take the degrade path. Use the reference's reported figures; do not re-derive them.
|
|
256
292
|
2. **The `Read` itself, as the last content-producing tool call before composing the gate.** `Read` `$SUMMARY_PATH` and paste its contents verbatim at the top of the gate. If the `Read` fails, returns 0 bytes, or reports truncation — take the degrade path. The `Read` must be last: pi-condense does not protect a `/tmp` read, so any turn boundary between the `Read` and the render lets the ~9KB read result be pruned, reproducing the bug.
|
|
@@ -259,21 +295,28 @@ Then commit the spec — this commit is **unconditional**: the summary is only a
|
|
|
259
295
|
|
|
260
296
|
Either way — summary rendered or degraded — then `rm "$SUMMARY_PATH"` (unconditional cleanup; harmless if the file was never created, since it lives outside the worktree under the OS temp dir).
|
|
261
297
|
|
|
262
|
-
Render the temp file's contents **verbatim** first — paste it as-is, do **not** paraphrase, condense, re-section, drop sections, or merge it with council
|
|
298
|
+
Render the temp file's contents **verbatim** first — paste it as-is, do **not** paraphrase, condense, re-section, drop sections, or merge it with the council audit. "Fold into the gate" means *place it inside the gate message*, not *rewrite it*. This summary is of the **final (post-apply)** spec, since both critique paths already applied before this dispatch. After the verbatim block, append the commit confirmation, then — as their **own** adjacent lines, not edits to the summary — the council audit (if the council path ran: `Applied:` / `Deferred:` / `Rejected:`, one line each), critique-pass-unresolved ambiguities, and every entry from the summarizer's gap/external-context footer (surface **all** of them, not just the top risk):
|
|
263
299
|
|
|
264
300
|
```
|
|
265
301
|
<spec-only summary read back from the temp file — pasted verbatim, unedited>
|
|
266
302
|
|
|
267
303
|
Spec written and committed to <project>/doc/specs/<filename>.md (worktree: <path>).
|
|
268
304
|
|
|
269
|
-
<
|
|
305
|
+
Applied: <cluster -> edit>, ...
|
|
306
|
+
Deferred: <cluster -> where it belongs>, ...
|
|
307
|
+
Rejected: <cluster -> one-line reason>, ...
|
|
308
|
+
(omit the three lines above when the worker path ran, not the council)
|
|
270
309
|
|
|
271
|
-
|
|
310
|
+
<unresolved ambiguities; every gap-footer entry from the summary>
|
|
311
|
+
|
|
312
|
+
Please review. Approve to proceed, tell me what to change in the spec, or say "revert applied council edit <X>" to undo a specific applied edit.
|
|
272
313
|
```
|
|
273
314
|
|
|
274
315
|
If you believe the summary needs correcting, do **not** silently rewrite it — re-dispatch the summarizer or note the discrepancy as an adjacent line beneath the verbatim block.
|
|
275
316
|
|
|
276
|
-
|
|
317
|
+
**Revert valve.** "Revert applied council edit X" is a normal change request: revise the spec to undo edit X, re-dispatch the summarizer with a **fresh** temp path (per the re-dispatch rule below), and re-present the gate. This is cheap here - the spec is not yet plan- or code-bearing.
|
|
318
|
+
|
|
319
|
+
Wait for the user. On a change request (including a revert), revise the spec and re-present — mint a **fresh** temp path for the re-dispatched summarizer (never reuse a prior round's path, so stale content can never be mistaken for the new summary). On approval, proceed immediately to `/skill:writing-plans` with no further prompt — the plan and execution mode are mechanical derivatives, so the only human gate here is spec approval itself. Don't land the spec on `main`; it stays in the worktree and ships in the same squash commit as the implementation.
|
|
277
320
|
|
|
278
321
|
After approval, mark the brainstorm phase complete:
|
|
279
322
|
|
|
@@ -294,6 +337,10 @@ phase_tracker({ action: "complete", phase: "brainstorm" })
|
|
|
294
337
|
## Red Flags — STOP
|
|
295
338
|
|
|
296
339
|
- About to write code or start a non-spec edit while this skill is active
|
|
340
|
+
- About to dispatch lint, critique, council, or summarizer while the spec file's line 1 is still the context-draft marker
|
|
341
|
+
- About to run the spec-writing overwrite without re-reading the draft in the same turn
|
|
342
|
+
- About to use `edit` instead of `write` for the spec-writing overwrite
|
|
343
|
+
- About to insert a human gate, announcement, or question between gather dispatch and questionary question one
|
|
297
344
|
- About to run, deploy, or validate the **proposed change** (vs. observing current behaviour) before the user approved the design
|
|
298
345
|
- About to skip the critique pass (council if configured, else fresh worker)
|
|
299
346
|
- Critique dispatch (council or worker) failed to complete and you proceeded to the gate anyway
|