pi-gauntlet 6.0.0 → 6.1.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 +12 -0
- package/README.md +1 -1
- package/agents/code-reviewer.md +1 -0
- package/agents/conformance-reviewer.md +1 -1
- package/bin/gauntlet-performance.test.mjs +4 -4
- package/bin/gauntlet-spec-index.mjs +142 -100
- package/bin/gauntlet-spec-index.test.mjs +41 -1
- package/bin/gauntlet-telemetry-seal.mjs +49 -14
- package/bin/gauntlet-telemetry-seal.test.mjs +76 -5
- package/extensions/lib/gauntlet-settings.test.ts +13 -3
- package/extensions/lib/gauntlet-settings.ts +18 -2
- package/extensions/lib/phase-tracker-helpers.test.ts +14 -0
- package/extensions/lib/phase-tracker-helpers.ts +5 -4
- package/extensions/lib/telemetry-collect.test.ts +6 -0
- package/extensions/lib/telemetry-collect.ts +4 -2
- package/extensions/lib/telemetry-diff.test.ts +52 -4
- package/extensions/lib/telemetry-diff.ts +4 -4
- package/extensions/lib/telemetry-paths.test.ts +17 -6
- package/extensions/lib/telemetry-paths.ts +7 -2
- package/extensions/lib/telemetry-ship.test.ts +9 -6
- package/extensions/lib/telemetry-ship.ts +4 -3
- package/extensions/phase-tracker.test.ts +24 -0
- package/extensions/phase-tracker.ts +10 -6
- package/extensions/telemetry.test.ts +32 -7
- package/extensions/telemetry.ts +30 -26
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +5 -5
- package/skills/dispatching-parallel-agents/SKILL.md +1 -1
- package/skills/finishing-a-development-branch/SKILL.md +8 -4
- package/skills/gauntlet-resume/reference/reconstruction.md +1 -4
- package/skills/requesting-code-review/SKILL.md +4 -2
- package/skills/subagent-driven-development/SKILL.md +6 -6
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +10 -8
- package/skills/verification-before-completion/reference/conformance-check.md +2 -0
- package/skills/verification-before-completion/reference/whole-diff-minors.md +31 -0
- package/skills/writing-plans/SKILL.md +4 -3
|
@@ -290,6 +290,19 @@ test("brainstorm write guard is unchanged by the implement guard", async () => {
|
|
|
290
290
|
}
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
+
test("brainstorm write confinement: a docs/specs write under the default dirs does not warn", async () => {
|
|
294
|
+
const priorDepth = process.env.PI_SUBAGENT_DEPTH;
|
|
295
|
+
delete process.env.PI_SUBAGENT_DEPTH;
|
|
296
|
+
try {
|
|
297
|
+
const h = harness({ cwd: tempCwd(), branch: [phaseResult("start", phases({ brainstorm: "in_progress" }))] });
|
|
298
|
+
await h.emit("session_start");
|
|
299
|
+
await h.emitEvent("tool_call", writeCall("t1", "docs/specs/x.md"));
|
|
300
|
+
assert.equal((await h.emitEvent("tool_result", writeResult("t1")))[0], undefined);
|
|
301
|
+
} finally {
|
|
302
|
+
if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
293
306
|
test("resumed session: plan-implement recovery edge fires (AC 3)", async () => {
|
|
294
307
|
const h = harness({ cwd: tempCwd(), branch: [...resumedBranch({ plan: "complete" }), assistant()] });
|
|
295
308
|
await settle(h);
|
|
@@ -1244,6 +1257,17 @@ test("gauntlet_setting escalationLoop: setting absent -> ctx-derived main-loop m
|
|
|
1244
1257
|
assert.equal(res.details.implModel, "p/strong:high");
|
|
1245
1258
|
});
|
|
1246
1259
|
|
|
1260
|
+
test("gauntlet_setting flowGuards: default dirs + derived plan dirs; repo override replaces them", async () => {
|
|
1261
|
+
const h = harness({ cwd: tempCwd({ piGauntlet: {} }) });
|
|
1262
|
+
const tool = h.tools.find((t) => t.name === "gauntlet_setting")!;
|
|
1263
|
+
const def = (await tool.execute("f1", { key: "flowGuards" }, undefined, undefined, h.ctx)) as { details: unknown };
|
|
1264
|
+
assert.deepEqual(def.details, { key: "flowGuards", enforce: true, specDirs: ["doc/specs", "docs/specs"], planDirs: ["doc/plans", "docs/plans"], errors: [] });
|
|
1265
|
+
const set = harness({ cwd: tempCwd({ piGauntlet: { flowGuards: { specDirs: ["design/specs"] } } }) });
|
|
1266
|
+
const res = (await set.tools.find((t) => t.name === "gauntlet_setting")!.execute("f2", { key: "flowGuards" }, undefined, undefined, set.ctx)) as { details: { specDirs: string[]; planDirs: string[] } };
|
|
1267
|
+
assert.deepEqual(res.details.specDirs, ["design/specs"]);
|
|
1268
|
+
assert.deepEqual(res.details.planDirs, ["design/plans"]);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1247
1271
|
// --- Conformance fix-loop dispatch guard (spec 2026-09-13-conformance-dispatch-guard) ---
|
|
1248
1272
|
|
|
1249
1273
|
const verifyBranch = (extra: unknown[] = []) => [
|
|
@@ -17,6 +17,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
17
17
|
import { type Static, Type } from "@sinclair/typebox";
|
|
18
18
|
import {
|
|
19
19
|
mainLoopModel,
|
|
20
|
+
planDirsFor,
|
|
20
21
|
resolveClosureReview,
|
|
21
22
|
resolveEscalationLoop,
|
|
22
23
|
resolveFlowGuards,
|
|
@@ -734,7 +735,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
734
735
|
});
|
|
735
736
|
|
|
736
737
|
const GauntletSettingParams = Type.Object({
|
|
737
|
-
key: StringEnum(["specCouncil", "closureReview", "escalationLoop"] as const, {
|
|
738
|
+
key: StringEnum(["specCouncil", "closureReview", "escalationLoop", "flowGuards"] as const, {
|
|
738
739
|
description: "Which gauntlet setting to resolve (merged repo-over-preset).",
|
|
739
740
|
}),
|
|
740
741
|
});
|
|
@@ -747,16 +748,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
747
748
|
executionMode: "sequential",
|
|
748
749
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
749
750
|
const { gauntlet, errors } = loadGauntletSettings(ctx.cwd);
|
|
751
|
+
const fg = resolveFlowGuards(gauntlet);
|
|
750
752
|
const payload =
|
|
751
753
|
params.key === "specCouncil"
|
|
752
754
|
? { key: "specCouncil" as const, ...resolveSpecCouncil(gauntlet), errors }
|
|
753
755
|
: params.key === "closureReview"
|
|
754
756
|
? { key: "closureReview" as const, ...resolveClosureReview(gauntlet), errors }
|
|
755
|
-
:
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
757
|
+
: params.key === "flowGuards"
|
|
758
|
+
? { key: "flowGuards" as const, ...fg, planDirs: planDirsFor(fg.specDirs), errors }
|
|
759
|
+
: {
|
|
760
|
+
key: "escalationLoop" as const,
|
|
761
|
+
...resolveEscalationLoop(gauntlet, mainLoopModel(ctx.model, ctx.thinkingLevel)),
|
|
762
|
+
errors,
|
|
763
|
+
};
|
|
760
764
|
return {
|
|
761
765
|
content: [{ type: "text", text: "```json\n" + JSON.stringify(payload, null, 2) + "\n```" }],
|
|
762
766
|
details: payload,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { after, test } from "node:test";
|
|
6
6
|
import registerTelemetry, { realFs, replayBranch, type Deps, type GitResult } from "./telemetry.ts";
|
|
7
|
-
import { DEFAULT_TELEMETRY_BUCKETS } from "./lib/gauntlet-settings.ts";
|
|
7
|
+
import { DEFAULT_TELEMETRY_BUCKETS, planDirsFor, resolveFlowGuards } from "./lib/gauntlet-settings.ts";
|
|
8
8
|
import { emptyAccumulators, parseRecord, serializeRecord } from "./lib/telemetry-record.ts";
|
|
9
9
|
import { guardReason } from "./lib/telemetry-ship.ts";
|
|
10
10
|
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
const root = mkdtempSync(join(tmpdir(), "telemetry-test-"));
|
|
25
25
|
tempDirs.push(root);
|
|
26
26
|
mkdirSync(join(root, "doc/specs"), { recursive: true });
|
|
27
|
+
mkdirSync(join(root, "docs/specs"), { recursive: true });
|
|
27
28
|
mkdirSync(join(root, "doc/plans"), { recursive: true });
|
|
28
29
|
mkdirSync(join(root, ".worktrees/x/doc/specs"), { recursive: true });
|
|
29
30
|
const handlers = new Map<string, Handler[]>();
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
const gitCwds: string[] = [];
|
|
32
33
|
const jjCalls: string[][] = [];
|
|
33
34
|
const readFileCalls: string[] = [];
|
|
35
|
+
const specDirs = resolveFlowGuards({}).specDirs;
|
|
34
36
|
let clock = Date.parse("2026-09-17T10:00:00Z");
|
|
35
37
|
const deps: Deps = {
|
|
36
38
|
fs: { ...realFs, readFile: (p) => { readFileCalls.push(p); return realFs.readFile(p); } },
|
|
@@ -56,7 +58,7 @@
|
|
|
56
58
|
if (args[0] === "ls-files") return { code: 0, stdout: "", stderr: "" };
|
|
57
59
|
return { code: 0, stdout: "", stderr: "" };
|
|
58
60
|
},
|
|
59
|
-
settings: () => ({ telemetry: { enabled: o.enabled ?? true, dir: o.telemetryDir ?? ".pi/gauntlet/telemetry", buckets: DEFAULT_TELEMETRY_BUCKETS, warning: o.telemetryWarning }, errors: [], agentOverrides: { implementer: { model: "p/x" } }, versions: { pi: "0.85.1" } }),
|
|
61
|
+
settings: () => ({ specDirs, planDirs: planDirsFor(specDirs), telemetry: { enabled: o.enabled ?? true, dir: o.telemetryDir ?? ".pi/gauntlet/telemetry", buckets: DEFAULT_TELEMETRY_BUCKETS, warning: o.telemetryWarning }, errors: [], agentOverrides: { implementer: { model: "p/x" } }, versions: { pi: "0.85.1" } }),
|
|
60
62
|
// A plain jj workspace: `jj root` answers the temp root while git rev-parse fails.
|
|
61
63
|
jj: async (args, cwd) => {
|
|
62
64
|
jjCalls.push(args);
|
|
@@ -180,6 +182,19 @@
|
|
|
180
182
|
assert.equal(readFileSync(h.excludeFile(), "utf8"), ".pi/gauntlet/telemetry/\n", "exclude line is written once after rebind");
|
|
181
183
|
});
|
|
182
184
|
|
|
185
|
+
test("a docs/specs write binds under the default dirs and the record lands under telemetry.dir/docs/specs", async () => {
|
|
186
|
+
const h = harness();
|
|
187
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
188
|
+
assert.equal(existsSync(h.recordPath("docs/specs/x.md")), false);
|
|
189
|
+
await h.writeSpec("docs/specs/x.md");
|
|
190
|
+
assert.equal(h.recordPath("docs/specs/x.md"), join(h.root, ".pi/gauntlet/telemetry/docs/specs/x.yaml"));
|
|
191
|
+
assert.equal(h.readRecord("docs/specs/x.md").spec, "docs/specs/x.md");
|
|
192
|
+
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
193
|
+
const rec = h.readRecord("docs/specs/x.md");
|
|
194
|
+
assert.equal(rec.approved_at, rec.events.at(-1)!.ts);
|
|
195
|
+
assert.equal(h.gitWrites().length, 0);
|
|
196
|
+
});
|
|
197
|
+
|
|
183
198
|
test("bind derives approved_at from a buffered brainstorm completion", async () => {
|
|
184
199
|
const h = harness();
|
|
185
200
|
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
@@ -231,7 +246,7 @@
|
|
|
231
246
|
{ type: "message", message: { role: "toolResult", toolName: "write", toolCallId: "w1", isError: false } },
|
|
232
247
|
{ type: "message", message: { role: "toolResult", toolName: "write", toolCallId: "w2", isError: false } },
|
|
233
248
|
{ type: "message", message: { role: "toolResult", toolName: "write", toolCallId: "w3", isError: true } },
|
|
234
|
-
]);
|
|
249
|
+
], ["doc/specs", "docs/specs"]);
|
|
235
250
|
assert.deepEqual(replay.phases, successfulPhases);
|
|
236
251
|
assert.equal(replay.gauntletEntered, true);
|
|
237
252
|
assert.equal(replay.planCheckSpec, "/repo/doc/specs/a.md");
|
|
@@ -246,7 +261,7 @@
|
|
|
246
261
|
const unarmed = replayBranch([
|
|
247
262
|
...write("w1", "doc/specs/a.md"),
|
|
248
263
|
{ type: "message", message: { role: "toolResult", toolName: "plan_check", details: { status: "pass", specPath: "/repo/doc/specs/a.md" } } },
|
|
249
|
-
]);
|
|
264
|
+
], ["doc/specs", "docs/specs"]);
|
|
250
265
|
assert.equal(unarmed.gauntletEntered, false);
|
|
251
266
|
assert.equal(unarmed.lastSpecWrite, undefined);
|
|
252
267
|
assert.equal(unarmed.planCheckSpec, undefined);
|
|
@@ -254,21 +269,31 @@
|
|
|
254
269
|
...write("w0", "doc/specs/before.md"),
|
|
255
270
|
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
256
271
|
...write("w1", "doc/specs/a.md", "edit"),
|
|
257
|
-
]);
|
|
272
|
+
], ["doc/specs", "docs/specs"]);
|
|
258
273
|
assert.deepEqual([armed.gauntletEntered, armed.lastSpecWrite], [true, "doc/specs/a.md"]);
|
|
259
274
|
const reset = replayBranch([
|
|
260
275
|
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
261
276
|
...write("w1", "doc/specs/a.md"),
|
|
262
277
|
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "reset", phases: P() } } },
|
|
263
|
-
]);
|
|
278
|
+
], ["doc/specs", "docs/specs"]);
|
|
264
279
|
assert.deepEqual([reset.gauntletEntered, reset.lastSpecWrite], [false, undefined]);
|
|
265
280
|
const resumed = replayBranch([
|
|
266
281
|
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
267
282
|
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "skip", phases: { ...P({ brainstorm: "skipped" }), brainstorm: { status: "skipped", reason: "resume: /repo/doc/specs/a.md" } } } } },
|
|
268
|
-
]);
|
|
283
|
+
], ["doc/specs", "docs/specs"]);
|
|
269
284
|
assert.deepEqual([resumed.gauntletEntered, resumed.lastSpecWrite], [true, "/repo/doc/specs/a.md"]);
|
|
270
285
|
});
|
|
271
286
|
|
|
287
|
+
test("replayBranch honors a custom spec dir list", () => {
|
|
288
|
+
const branch = [
|
|
289
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
290
|
+
{ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "w1", name: "write", arguments: { path: "design/specs/a.md" } }] } },
|
|
291
|
+
{ type: "message", message: { role: "toolResult", toolName: "write", toolCallId: "w1", isError: false } },
|
|
292
|
+
];
|
|
293
|
+
assert.equal(replayBranch(branch, ["design/specs"]).lastSpecWrite, "design/specs/a.md");
|
|
294
|
+
assert.equal(replayBranch(branch, ["doc/specs"]).lastSpecWrite, undefined);
|
|
295
|
+
});
|
|
296
|
+
|
|
272
297
|
test("session_start replay binds from plan_check details with no spec write, extends sessions, preserves run_id", async () => {
|
|
273
298
|
const first = harness();
|
|
274
299
|
await first.phaseResult("start", P({ brainstorm: "in_progress" }));
|
package/extensions/telemetry.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import * as piRuntime from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import { DEFAULT_TEST_COMMANDS, resolveTelemetry, settingsErrorWarning, type TelemetryResolved } from "./lib/gauntlet-settings.ts";
|
|
17
|
+
import { DEFAULT_TEST_COMMANDS, planDirsFor, resolveFlowGuards, resolveTelemetry, settingsErrorWarning, type TelemetryResolved } from "./lib/gauntlet-settings.ts";
|
|
18
18
|
import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
|
|
19
19
|
import { checkoutOf } from "./lib/checkout.ts";
|
|
20
20
|
import { CONTEXT_DRAFT_MARKER, nextGauntletEntered } from "./lib/phase-tracker-helpers.ts";
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
}
|
|
44
44
|
export interface SettingsSnapshot {
|
|
45
45
|
telemetry: TelemetryResolved;
|
|
46
|
+
specDirs: string[];
|
|
47
|
+
planDirs: string[];
|
|
46
48
|
errors: string[];
|
|
47
49
|
agentOverrides?: Record<string, unknown>;
|
|
48
50
|
versions: Record<string, string>;
|
|
@@ -118,6 +120,8 @@
|
|
|
118
120
|
function realSettings(cwd: string): SettingsSnapshot {
|
|
119
121
|
const { gauntlet, errors, root } = loadGauntletSettings(cwd);
|
|
120
122
|
const telemetry = resolveTelemetry(gauntlet);
|
|
123
|
+
const { specDirs } = resolveFlowGuards(gauntlet);
|
|
124
|
+
const planDirs = planDirsFor(specDirs);
|
|
121
125
|
const sm = SettingsManager.create(root, getAgentDir());
|
|
122
126
|
const layer = (s: unknown) => ((s as { subagents?: { agentOverrides?: unknown } })?.subagents?.agentOverrides ?? undefined) as Record<string, unknown> | undefined;
|
|
123
127
|
const preset = layer(sm.getGlobalSettings());
|
|
@@ -125,7 +129,7 @@
|
|
|
125
129
|
const agentOverrides = preset || repo ? { ...(preset ?? {}), ...(repo ?? {}) } : undefined;
|
|
126
130
|
const rawCmds = gauntlet.verifyBeforeShip?.testCommands;
|
|
127
131
|
const testCommands = Array.isArray(rawCmds) && rawCmds.length > 0 && rawCmds.every((c) => typeof c === "string" && c.trim()) ? (rawCmds as string[]) : undefined;
|
|
128
|
-
return { telemetry, errors, agentOverrides, versions: realVersions(), testCommands };
|
|
132
|
+
return { telemetry, specDirs, planDirs, errors, agentOverrides, versions: realVersions(), testCommands };
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
export const realDeps: Deps = { fs: realFs, git: realGit, jj: realJj, now: isoNow, settings: realSettings };
|
|
@@ -142,16 +146,16 @@
|
|
|
142
146
|
// gauntlet-resume's reconstruction skips brainstorm with `resume: <spec path>`; the path
|
|
143
147
|
// is the bind candidate on that route because no spec write follows.
|
|
144
148
|
const RESUME_REASON_RE = /^resume: (\S+)$/;
|
|
145
|
-
export function resumeSpecOf(phases: PhaseMap | undefined): string | undefined {
|
|
149
|
+
export function resumeSpecOf(phases: PhaseMap | undefined, specDirs: readonly string[]): string | undefined {
|
|
146
150
|
const b = phases?.brainstorm;
|
|
147
151
|
const m = b?.status === "skipped" && typeof b.reason === "string" ? RESUME_REASON_RE.exec(b.reason) : null;
|
|
148
|
-
return m && isSpecPath(toPosix(m[1])) ? m[1] : undefined;
|
|
152
|
+
return m && isSpecPath(toPosix(m[1]), specDirs) ? m[1] : undefined;
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
// Walks the session branch like phase-tracker.ts: successful phase_tracker results set
|
|
152
156
|
// the phase map and the flow-entry marker; bind candidates are taken only while armed
|
|
153
157
|
// and dropped on reset. Silent: no events are emitted.
|
|
154
|
-
export function replayBranch(entries: Iterable<unknown
|
|
158
|
+
export function replayBranch(entries: Iterable<unknown>, specDirs: readonly string[]): ReplayResult {
|
|
155
159
|
const out: ReplayResult = { phases: emptyPhases(), gauntletEntered: false };
|
|
156
160
|
const calls = new Map<string, { name: string; args: unknown }>();
|
|
157
161
|
for (const raw of entries) {
|
|
@@ -173,14 +177,14 @@
|
|
|
173
177
|
out.planCheckSpec = undefined;
|
|
174
178
|
out.lastSpecWrite = undefined;
|
|
175
179
|
}
|
|
176
|
-
const resumed = d.action === "skip" ? resumeSpecOf(d.phases) : undefined;
|
|
180
|
+
const resumed = d.action === "skip" ? resumeSpecOf(d.phases, specDirs) : undefined;
|
|
177
181
|
if (out.gauntletEntered && resumed) out.lastSpecWrite = resumed;
|
|
178
182
|
} else if (msg.toolName === "plan_check") {
|
|
179
183
|
const d = msg.details as { status?: string; specPath?: string } | undefined;
|
|
180
184
|
if (out.gauntletEntered && d?.status === "pass" && typeof d.specPath === "string") out.planCheckSpec = d.specPath;
|
|
181
185
|
} else if ((msg.toolName === "write" || msg.toolName === "edit") && !msg.isError && msg.toolCallId) {
|
|
182
186
|
const p = (calls.get(msg.toolCallId)?.args as { path?: unknown } | undefined)?.path;
|
|
183
|
-
if (out.gauntletEntered && typeof p === "string" && isSpecPath(toPosix(p))) out.lastSpecWrite = p;
|
|
187
|
+
if (out.gauntletEntered && typeof p === "string" && isSpecPath(toPosix(p), specDirs)) out.lastSpecWrite = p;
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
return out;
|
|
@@ -328,7 +332,7 @@
|
|
|
328
332
|
predecessorLinks.clear();
|
|
329
333
|
record = loadOrCreate(specRel);
|
|
330
334
|
for (const link of record.supersedes ?? []) {
|
|
331
|
-
if (!isSafeSpecLink(link)) continue;
|
|
335
|
+
if (!isSafeSpecLink(link, snap.specDirs)) continue;
|
|
332
336
|
const predecessor = deps.fs.readFile(abs(link));
|
|
333
337
|
if (predecessor !== undefined && isSupersededByBanner(predecessor, record.spec)) predecessorLinks.add(link);
|
|
334
338
|
}
|
|
@@ -369,9 +373,9 @@
|
|
|
369
373
|
councilPending = undefined;
|
|
370
374
|
};
|
|
371
375
|
|
|
372
|
-
const isSafeSpecLink = (link: string): boolean => {
|
|
376
|
+
const isSafeSpecLink = (link: string, specDirs: readonly string[]): boolean => {
|
|
373
377
|
const normalized = posix.normalize(link);
|
|
374
|
-
return isSpecPath(link) && !posix.isAbsolute(link) && normalized !== ".." && !normalized.startsWith("../");
|
|
378
|
+
return isSpecPath(link, specDirs) && !posix.isAbsolute(link) && normalized !== ".." && !normalized.startsWith("../");
|
|
375
379
|
};
|
|
376
380
|
|
|
377
381
|
const refreshLinks = () => {
|
|
@@ -449,9 +453,9 @@
|
|
|
449
453
|
}
|
|
450
454
|
phases = details.phases;
|
|
451
455
|
if (action === "skip" && !boundSpec && canBind() && lastCtx) {
|
|
452
|
-
const resumed = resumeSpecOf(details.phases);
|
|
456
|
+
const resumed = resumeSpecOf(details.phases, snap.specDirs);
|
|
453
457
|
const loc = resumed ? await locate(lastCtx, resumed) : undefined;
|
|
454
|
-
if (loc && isSpecPath(loc.rel)) {
|
|
458
|
+
if (loc && isSpecPath(loc.rel, snap.specDirs)) {
|
|
455
459
|
await bind(loc.rel, snap, loc.toplevel, loc.via);
|
|
456
460
|
flush();
|
|
457
461
|
}
|
|
@@ -481,13 +485,13 @@
|
|
|
481
485
|
pendingTest.clear();
|
|
482
486
|
sessionId = ctx.sessionManager.getSessionId();
|
|
483
487
|
currentDir = snap.telemetry.dir;
|
|
484
|
-
const replay = replayBranch(ctx.sessionManager.getBranch());
|
|
488
|
+
const replay = replayBranch(ctx.sessionManager.getBranch(), snap.specDirs);
|
|
485
489
|
phases = replay.phases;
|
|
486
490
|
gauntletEntered = replay.gauntletEntered;
|
|
487
491
|
if (!canBind()) return;
|
|
488
492
|
const candidate = replay.planCheckSpec ?? replay.lastSpecWrite;
|
|
489
493
|
const loc = candidate ? await locate(ctx, candidate) : undefined;
|
|
490
|
-
if (loc && isSpecPath(loc.rel)) {
|
|
494
|
+
if (loc && isSpecPath(loc.rel, snap.specDirs)) {
|
|
491
495
|
await bind(loc.rel, snap, loc.toplevel, loc.via);
|
|
492
496
|
flush();
|
|
493
497
|
}
|
|
@@ -537,7 +541,7 @@
|
|
|
537
541
|
const rawPath = (event.input as { path?: unknown }).path;
|
|
538
542
|
const loc = typeof rawPath === "string" ? await locate(ctx, rawPath) : undefined;
|
|
539
543
|
if (!loc) {
|
|
540
|
-
if (typeof rawPath === "string" && isSpecPath(toPosix(rawPath))) warn(`spec ${rawPath} is outside a git checkout; telemetry not recorded`);
|
|
544
|
+
if (typeof rawPath === "string" && isSpecPath(toPosix(rawPath), snap.specDirs)) warn(`spec ${rawPath} is outside a git checkout; telemetry not recorded`);
|
|
541
545
|
return undefined;
|
|
542
546
|
}
|
|
543
547
|
return onSpecInteraction(event.toolName, loc, event, snap);
|
|
@@ -568,13 +572,13 @@
|
|
|
568
572
|
const onSpecInteraction = async (tool: "write" | "edit" | "read", loc: { toplevel: string; rel: string; via: "git" | "jj" }, event: { input: unknown; content: unknown[] }, snap: SettingsSnapshot): Promise<unknown> => {
|
|
569
573
|
const rel = loc.rel;
|
|
570
574
|
if (!boundSpec) {
|
|
571
|
-
if ((tool === "write" || tool === "edit") && isSpecPath(rel)) {
|
|
575
|
+
if ((tool === "write" || tool === "edit") && isSpecPath(rel, snap.specDirs)) {
|
|
572
576
|
await bind(rel, snap, loc.toplevel, loc.via);
|
|
573
577
|
if (!boundSpec) return undefined;
|
|
574
578
|
const patch = onBoundSpecWrite(tool, rel, event);
|
|
575
579
|
flush();
|
|
576
580
|
return patch;
|
|
577
|
-
} else if (isPlanPath(rel)) {
|
|
581
|
+
} else if (isPlanPath(rel, snap.planDirs)) {
|
|
578
582
|
const spec = planSpecHeader(deps.fs.readFile(join(loc.toplevel, rel)) ?? "");
|
|
579
583
|
const specRel = spec ? repoRelativeToolPath(loc.toplevel, loc.toplevel, spec) : undefined;
|
|
580
584
|
if (specRel && deps.fs.exists(join(loc.toplevel, specRel))) {
|
|
@@ -586,7 +590,7 @@
|
|
|
586
590
|
}
|
|
587
591
|
if (loc.toplevel !== toplevel) {
|
|
588
592
|
// Another checkout: a fresh run, never a rename of this record.
|
|
589
|
-
if ((tool === "write" || tool === "edit") && isSpecPath(rel) && canBind() && !sealedOnDisk(loc.toplevel, rel, snap.telemetry.dir)) {
|
|
593
|
+
if ((tool === "write" || tool === "edit") && isSpecPath(rel, snap.specDirs) && canBind() && !sealedOnDisk(loc.toplevel, rel, snap.telemetry.dir)) {
|
|
590
594
|
unbind();
|
|
591
595
|
await bind(rel, snap, loc.toplevel, loc.via);
|
|
592
596
|
if (!boundSpec) return undefined;
|
|
@@ -602,13 +606,13 @@
|
|
|
602
606
|
flush();
|
|
603
607
|
return patch;
|
|
604
608
|
}
|
|
605
|
-
if (tool === "write" && isSpecPath(rel) && phases.brainstorm.status === "in_progress" && isDraftOrMissing(boundSpec)) {
|
|
609
|
+
if (tool === "write" && isSpecPath(rel, snap.specDirs) && phases.brainstorm.status === "in_progress" && isDraftOrMissing(boundSpec)) {
|
|
606
610
|
if (await rebind(rel)) onBoundSpecWrite(tool, rel, event);
|
|
607
611
|
flush();
|
|
608
612
|
return undefined;
|
|
609
613
|
}
|
|
610
|
-
if (tool === "edit" && isSpecPath(rel)) {
|
|
611
|
-
onOtherSpecEdit(rel, event);
|
|
614
|
+
if (tool === "edit" && isSpecPath(rel, snap.specDirs)) {
|
|
615
|
+
onOtherSpecEdit(rel, event, snap.specDirs);
|
|
612
616
|
flush();
|
|
613
617
|
}
|
|
614
618
|
return undefined;
|
|
@@ -616,7 +620,7 @@
|
|
|
616
620
|
|
|
617
621
|
// Defined in block 2.
|
|
618
622
|
let onBoundSpecWrite: (tool: "write" | "edit", rel: string, event: { input: unknown; content: unknown[] }) => unknown;
|
|
619
|
-
let onOtherSpecEdit: (rel: string, event: { input: unknown }) => void;
|
|
623
|
+
let onOtherSpecEdit: (rel: string, event: { input: unknown }, specDirs: string[]) => void;
|
|
620
624
|
|
|
621
625
|
let amendmentOpen = false;
|
|
622
626
|
let lastPlanTasks: { status: string }[] | undefined;
|
|
@@ -728,7 +732,7 @@
|
|
|
728
732
|
if (!record || !toplevel || liveShipEvent(record.events)) return;
|
|
729
733
|
record.status = "shipped";
|
|
730
734
|
record.shipped_at = deps.now();
|
|
731
|
-
const out = await computeJjDiff({ jj: deps.jj ?? realJj, cwd: toplevel, spec: record.spec, dir: currentDir, buckets: snap.telemetry.buckets });
|
|
735
|
+
const out = await computeJjDiff({ jj: deps.jj ?? realJj, cwd: toplevel, spec: record.spec, dir: currentDir, buckets: snap.telemetry.buckets, planDirs: snap.planDirs });
|
|
732
736
|
if (out.warning) warn(out.warning);
|
|
733
737
|
record.derived.modified_files = out.modified_files;
|
|
734
738
|
record.derived.diff = out.diff;
|
|
@@ -743,7 +747,7 @@
|
|
|
743
747
|
const rawPath = (event.input as { path?: unknown }).path;
|
|
744
748
|
if (typeof rawPath === "string" && phases.brainstorm.status === "in_progress") {
|
|
745
749
|
const loc = await locate(ctx, rawPath);
|
|
746
|
-
if (loc && isSpecPath(loc.rel)) {
|
|
750
|
+
if (loc && isSpecPath(loc.rel, snap.specDirs)) {
|
|
747
751
|
const recRel = recordPathFor(snap.telemetry.dir, loc.rel);
|
|
748
752
|
const existing = deps.fs.readFile(join(loc.toplevel, recRel));
|
|
749
753
|
const parsed = existing ? parseRecord(existing) : undefined;
|
|
@@ -832,8 +836,8 @@
|
|
|
832
836
|
return undefined;
|
|
833
837
|
};
|
|
834
838
|
|
|
835
|
-
onOtherSpecEdit = (rel, event) => {
|
|
836
|
-
if (!record || !boundSpec || !isSafeSpecLink(rel)) return;
|
|
839
|
+
onOtherSpecEdit = (rel, event, specDirs) => {
|
|
840
|
+
if (!record || !boundSpec || !isSafeSpecLink(rel, specDirs)) return;
|
|
837
841
|
if (!isSupersededByBanner(insertedText(event.input), boundSpec)) return;
|
|
838
842
|
predecessorLinks.add(rel);
|
|
839
843
|
refreshLinks();
|
package/package.json
CHANGED
|
@@ -17,9 +17,9 @@ Identify the target project → set up an isolated worktree → understand curre
|
|
|
17
17
|
|
|
18
18
|
Do **not** implement until the design is presented and approved, regardless of simplicity. Implementation-heavy requests define spec scope; they do not lift this gate.
|
|
19
19
|
|
|
20
|
-
You **may** read code/docs, run the existing system to observe current behavior, write under `
|
|
20
|
+
You **may** read code/docs, run the existing system to observe current behavior, write under a resolved spec dir (`gauntlet_setting({ key: "flowGuards" })` -> `specDirs`), and `edit` a predecessor spec there to add a [supersession banner](reference/superseding.md).
|
|
21
21
|
|
|
22
|
-
You may **not** write outside
|
|
22
|
+
You may **not** write outside the resolved spec dirs; build, deploy, validate, or exercise the proposed change; run implementation skills; commit the spec on `main`; or start `/skill:writing-plans`.
|
|
23
23
|
|
|
24
24
|
The line: current-system observation is research; exercising the proposed change waits for approval.
|
|
25
25
|
|
|
@@ -63,7 +63,7 @@ Detection order: (1) infer from ticket labels, description, or paths; (2) use th
|
|
|
63
63
|
The spec is the first commit in a dedicated worktree, never a separate commit on `main`.
|
|
64
64
|
|
|
65
65
|
1. Invoke `/skill:using-git-worktrees`; use `.worktrees/` or the project-native location.
|
|
66
|
-
2. Carry its `Worktree ready at <full-path>` value: spec path `<full-path
|
|
66
|
+
2. Carry its `Worktree ready at <full-path>` value: spec path `<full-path>/<spec dir>/<filename>.md`; every dispatch uses that `cwd`; git uses `git -C <full-path>`. Keep the process cwd unchanged.
|
|
67
67
|
3. Write, review, and commit there.
|
|
68
68
|
|
|
69
69
|
Spec, plan, and implementation share this worktree. The spec and its telemetry record (`<telemetry.dir>/<spec path with .md -> .yaml>`, default `.pi/gauntlet/telemetry/doc/specs/<spec>.yaml`) are deliverables and ship in the squash; only the plan is stripped. `/skill:finishing-a-development-branch` strips the plan, then seals and commits the record before landing. Explicit trivial one-off edits outside this flow need no worktree.
|
|
@@ -146,7 +146,7 @@ Shared schemas, contracts, and invariants require a spec. If work is mechanical
|
|
|
146
146
|
|
|
147
147
|
## Filename Convention
|
|
148
148
|
|
|
149
|
-
Write under the routed `doc/specs
|
|
149
|
+
Write under the routed spec dir: pick the routing prose's dir (overrides file, `AGENTS.md`) when it is one of the resolved dirs, else the one resolved dir from `gauntlet_setting({ key: "flowGuards" })` that already exists in the repo, else the first resolved dir (`doc/specs` under the default). Name ticketed files `YYYY-MM-DD-<ticket-id>-<topic>.md`, otherwise `YYYY-MM-DD-<topic>.md`. Use a filename-safe tracker slug (`E-12345`, `gh-123`), but native references in plan headers and commits. `<topic>` is 3-6 kebab-case words without `-design` or another suffix.
|
|
150
150
|
|
|
151
151
|
Mint the slug once during gather and reuse it. If questionary invalidates it, write to the new path and delete the uncommitted draft.
|
|
152
152
|
|
|
@@ -293,7 +293,7 @@ One question at a time, YAGNI, 2-3 approaches, two design rounds, clarify freely
|
|
|
293
293
|
|
|
294
294
|
## Red Flags — STOP
|
|
295
295
|
|
|
296
|
-
- Writes outside
|
|
296
|
+
- Writes outside the resolved spec dirs ([owner](#hard-constraint)).
|
|
297
297
|
- Draft overwrite without the same-turn full read, or overwrite via `edit` ([owner](#spec-self-review-before-user-review-gate)).
|
|
298
298
|
- Dispatch while line 1 is the context-draft marker ([owner](#spec-self-review-before-user-review-gate)).
|
|
299
299
|
- Inline scope or ambiguity checks ([owner](#spec-council-optional)).
|
|
@@ -111,7 +111,7 @@ When agents return:
|
|
|
111
111
|
|
|
112
112
|
Fix rounds in review loops reuse the fan-out mechanics above, keyed off the reviewer's partition certificate — the orchestrator never partitions findings itself.
|
|
113
113
|
|
|
114
|
-
Reviewers certify fix concurrency with a `Parallel-safe:` line (see the reviewer's report contract). The fan-out trigger is a `disjoint` group naming **≥ 2 finding IDs**: that group IS the parallel wave — dispatch **one `implementer` per finding ID in the group** (`context: "fresh"`, `worktree: true`, `cwd` = the current worktree; task = that finding's block **verbatim**, including its `touched-files` line as the ownership boundary).
|
|
114
|
+
Reviewers certify fix concurrency with a `Parallel-safe:` line (see the reviewer's report contract). The fan-out trigger is a `disjoint` group naming **≥ 2 actionable finding IDs**: that group IS the parallel wave — dispatch **one `implementer` per actionable finding ID in the group (the probe's actionable set; the consuming loop places non-actionable findings)** (`context: "fresh"`, `worktree: true`, `cwd` = the current worktree; task = that finding's block **verbatim**, including its `touched-files` line as the ownership boundary). An actionable finding named in any `conflicts` pair runs sequentially after every finding it names has integrated (chained `conflicts` define a partial order; remaining serial findings run in the line's order). Actionable findings outside any ≥ 2-ID `disjoint` group run sequentially. Fan out per review line only — never merge or co-schedule groups from different `Parallel-safe:` lines; run those fan-outs serially.
|
|
115
115
|
|
|
116
116
|
**Precondition:** a clean committed HEAD containing the code under review. When the reviewed change is an unintegrated patch (a wave-mode per-patch spec review), each fix task branches from the wave's base HEAD and carries the prior patch verbatim in its task text — the consuming loop's existing re-dispatch protocol. When the tree is dirty (e.g. post-integration, before the wave commit), the fan-out is unavailable: fix sequentially in place.
|
|
117
117
|
|
|
@@ -82,7 +82,9 @@ This is an **enforced disposition gate**, not a surface-only notice. The user is
|
|
|
82
82
|
|
|
83
83
|
**Freshness precondition - before any verdict branch, including `CONFORMS`.** The durable block opens with a sentinel: `status: CONFORMS (0 open)` or `status: GAPS (N open)`, then `audited-base: <full HEAD SHA at audit time>`, then an optional `happy-path: <outcome>` line (present exactly when a happy-path run happened; informational, never a freshness input). Read the sentinel, then apply the reference's freshness rule (its `## Closure / conformance` block is the single source): compare `audited-base` to the current working tree; any change, doubt, missing/mismatched sentinel, legacy terse row, or malformed structured reviewer block triggers a fresh audit and replacement of the closure block. Never infer `CONFORMS` from the absence of bullets. Only a clean, valid `status: CONFORMS (0 open)` handoff enters the zero-gap fast path.
|
|
84
84
|
|
|
85
|
-
**
|
|
85
|
+
**Whole-diff minors (both paths).** Read the block's `whole-diff minors:` section (`conformance-check.md` "Closure / conformance") and render it as one informational, non-blocking line in the `happy-path:` position - directly under the `happy-path:` line when present, otherwise directly under `Closure / conformance: CONFORMS` or the `Conformance: N decisions needed before shipping.` header: `whole-diff minors (3, not blocking): F3, F7, F9`, `whole-diff minors: none`, or `whole-diff minors: not recorded` (section absent). Never a re-audit trigger. The reply point is the Step 4 menu.
|
|
86
|
+
|
|
87
|
+
**Zero-gap fast path:** print (plus the informational lines below)
|
|
86
88
|
|
|
87
89
|
```
|
|
88
90
|
Closure / conformance: CONFORMS
|
|
@@ -177,7 +179,9 @@ Which option?
|
|
|
177
179
|
|
|
178
180
|
Rows 1-2 run the Option 1/2 blocks; row 3 runs the Keep block and row 4 the Discard block.
|
|
179
181
|
|
|
180
|
-
**Don't add explanation** - keep options concise.
|
|
182
|
+
**Don't add explanation** - keep options concise. When the minors line lists findings and the block has no `whole-diff minors fix:` line, add only this line under the menu: `Or reply "fix minors" (or "fix minors F3, F9") for one Minor fix round.` This line is the only addition to the exact numbered options.
|
|
183
|
+
|
|
184
|
+
**Standalone extra reply - `fix minors`.** When the rendered `whole-diff minors` line lists findings, `fix minors` (all) or `fix minors F3, F9` (a subset by `Fn`) is a valid reply beside the numbered options. It dispatches one `implementer` round (`cwd` = the worktree; task = the selected finding lines verbatim; `SCOPED_TEST_COMMANDS` = the plan-header verification set), commits `whole-diff minors fix`, and returns to Step 3.5, where the freshness rule re-audits conformance as for any working-tree change and the regenerated block records the spent round (`whole-diff minors fix: <full SHA>`); Step 4 then re-renders. No code review follows this round: the whole-diff review is an unscoped first review, so its Minors are graded Minors. A block that already carries `whole-diff minors fix:` refuses the reply with "minors round already spent". Any numbered choice ships without them.
|
|
181
185
|
|
|
182
186
|
### Step 5: Execute Choice
|
|
183
187
|
|
|
@@ -185,11 +189,11 @@ Rows 1-2 run the Option 1/2 blocks; row 3 runs the Keep block and row 4 the Disc
|
|
|
185
189
|
|
|
186
190
|
Run this on the feature branch before any landing path. The spec and its telemetry record (`<telemetry.dir>/<spec path with .md -> .yaml>`, default `.pi/gauntlet/telemetry/doc/specs/<spec>.yaml`) are deliverables and ship in the squash; only the plan is stripped. The record is an untracked, git-excluded file until this step commits it. `<bin>` is `<directory of this skill's SKILL.md>/../../bin`, resolved from the skill's `<location>` in the system prompt.
|
|
187
191
|
|
|
188
|
-
Read the plan's `**Spec:**` header (repo-relative, backticks stripped) before the strip; that value
|
|
192
|
+
Read the plan's `**Spec:**` header (repo-relative, backticks stripped) before the strip; pass that value as `--spec`. Resolve the spec dirs with `gauntlet_setting({ key: "flowGuards" })` and use their sibling `plans` dirs for plan paths.
|
|
189
193
|
|
|
190
194
|
```bash
|
|
191
195
|
# Plans are ephemeral - if one was committed on this branch, remove it before landing.
|
|
192
|
-
PLAN_PATH
|
|
196
|
+
PLAN_PATH=<sibling-plans-dir>/<plan-file>.md # the sibling plans dir of the spec's dir
|
|
193
197
|
if git -C "$WORKTREE" ls-files --error-unmatch "$PLAN_PATH" >/dev/null 2>&1; then
|
|
194
198
|
git -C "$WORKTREE" rm "$PLAN_PATH" && git -C "$WORKTREE" commit -m "Remove ephemeral plan doc"
|
|
195
199
|
fi
|
|
@@ -25,10 +25,7 @@ for a base ref before reading any artifact.
|
|
|
25
25
|
|
|
26
26
|
A pinned spec (seed route, `seed.md`) skips this section.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
else the active pi profile's `settings.json`, else the default `["doc/specs"]` (sibling
|
|
30
|
-
`doc/plans`). An empty array is the default. `<dirs>` below is that resolved list,
|
|
31
|
-
space-separated - never the literal defaults when a setting is present.
|
|
28
|
+
Resolve with `gauntlet_setting({ key: "flowGuards" })`; `<dirs>` below is its `specDirs` plus `planDirs` (each spec dir's sibling `plans` dir), space-separated.
|
|
32
29
|
|
|
33
30
|
```bash
|
|
34
31
|
git -C <worktree> diff --diff-filter=A --name-only <base>..HEAD -- <dirs>
|
|
@@ -51,9 +51,11 @@ subagent({ agent: "code-reviewer", async: false, task: "... filled template ..."
|
|
|
51
51
|
- `{DESCRIPTION}` - Brief summary
|
|
52
52
|
- `{SCOPED_TEST_COMMANDS}` - the scoped verification commands the reviewer may run, or `none`
|
|
53
53
|
|
|
54
|
-
**Fix rounds.**
|
|
54
|
+
**Fix rounds.** A `FIX_FIRST` report triggers a fix round. A `SHIP` report dispatches nothing and ends the loop, whatever Minors it lists. `REJECT` is neither: stop and report (in SDD, per its `stop-note.md`); no 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 Critical/Moderate 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). Sequential round (one task): the task carries the whole report; every Minor rides. Fan-out: the certificate partitions the Critical and Moderate findings; each Minor attaches once to the first task, in `Fn` order, whose ownership boundary - the union of its findings' `touched-files:` - already contains the Minor's `touched-files:`; a Minor no boundary contains is not dispatched this round, and a boundary never grows for a Minor. The implementer fixes its Minors or declines one with a stated reason. 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. A `FIX_FIRST` re-review buys one more fix round and one more foreground re-review; still `FIX_FIRST` -> escalate to the user. A `SHIP` report's Minors are not carried, with one exception: the whole-diff review's Minor finding lines go verbatim into the closure block's `whole-diff minors:` section (`verification-before-completion/reference/conformance-check.md` "Closure / conformance").
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
Worked example: a `FIX_FIRST` report with 1 Moderate and 3 Minors -> one implementer task holding all four findings (one actionable ID, no fan-out). A `SHIP` report with 4 Minors -> no dispatch; the loop ends.
|
|
57
|
+
|
|
58
|
+
Take the recipient stance from `receiving-code-review`: verify each finding, answer a wrong finding with evidence.
|
|
57
59
|
|
|
58
60
|
## Integration with Workflows
|
|
59
61
|
|
|
@@ -62,7 +62,7 @@ For each task in `plan_tracker`:
|
|
|
62
62
|
3. **Dispatch spec reviewer.** Pass the task text, the patch diff, the absolute spec path, and the task's `**Spec:**` anchors, plus the task's `**Tests:**` block and `- Test:` paths as contract (never as commands - SR never executes) — 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.
|
|
63
63
|
4. If spec reviewer finds gaps → re-dispatch implementer to fix → re-review. Loop until ✅, within [Fix-Loop Rounds](#fix-loop-rounds).
|
|
64
64
|
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 the task's `SCOPED_TEST_COMMANDS`.
|
|
65
|
-
6. If quality
|
|
65
|
+
6. If the quality review is issue-bearing → re-dispatch the implementer per `/skill:requesting-code-review` Fix rounds → re-review. Loop until ✅, within [Fix-Loop Rounds](#fix-loop-rounds).
|
|
66
66
|
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.
|
|
67
67
|
|
|
68
68
|
The orchestrator is the spec's only writer during execution. Amendment trigger: implementer `BLOCKED` citing a spec defect, or a review finding showing the spec (not the code) is wrong -> pause the fix loop, execute brainstorming's [amendment path](../brainstorming/SKILL.md#amending-an-approved-spec) in place (it reviews first, stops only on escalation or redraw), resume. Code-vs-spec mismatch stays in the SR loop. 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.
|
|
@@ -73,15 +73,15 @@ After all tasks: proceed to [After All Tasks](#after-all-tasks-complete) - it ow
|
|
|
73
73
|
|
|
74
74
|
One rule governs both review loops - spec-compliance and code-quality - in sequential and parallel-wave modes alike.
|
|
75
75
|
|
|
76
|
-
**Re-review dispatch rule:** every re-review task includes the complete prior review report verbatim under the marker `## Previous review report (re-review trigger)`, plus the trajectory block from the reviewer's prompt template. The marker's presence is what obligates the reviewer to emit the `TRAJECTORY:` line. You never select, summarize, or diff findings yourself - pattern-match the sentinel line only.
|
|
76
|
+
**Re-review dispatch rule:** every re-review task includes the complete prior review report verbatim under the marker `## Previous review report (re-review trigger)`, plus the trajectory block from the reviewer's prompt template. The marker's presence is what obligates the reviewer to emit the `TRAJECTORY:` line. You never select, summarize, or diff findings yourself - pattern-match the sentinel line only. Code re-reviews also carry a `## Fix delta` block holding the fix round just integrated: sequential mode, the output of `git diff <HEAD the prior review was dispatched against> HEAD`; wave mode (HEAD unmoved until step 7), the applied fix patch(es) verbatim, never a SHA range. Whole-diff re-reviews never carry the block. Omit it otherwise only when the input is unavailable (a fresh-session resume); the reviewer then treats the task's whole diff as the delta.
|
|
77
77
|
|
|
78
|
-
**Fix fan-out.** When
|
|
78
|
+
**Fix fan-out.** When a fix round is fanned out per `/skill:requesting-code-review` Fix rounds, dispatch per `dispatching-parallel-agents` "Fix fan-out"; the fan-out counts as **one** fix against this budget, its scoped test gate is the consuming task/wave's `SCOPED_TEST_COMMANDS`, and one re-review of the integrated delta follows.
|
|
79
79
|
|
|
80
80
|
**Behaviour-change reroute.** When the triggering CR report carries `Behaviour-change: yes`, the fix round's re-review is SR first, then CR. The SR dispatch reviews the fix diff against the task's spec anchors as a first review (no `## Previous review report` marker, so no `TRAJECTORY:` line; SR carries no test commands but does carry the task's `**Tests:**` block and `- Test:` paths), but its ordinal continues the task's SR-loop count - a task whose SR loop ended at review 2 gets review 3 here, and an issue-bearing rerouted SR after review 4 escalates. Issues follow the normal sequence: fix, then SR re-review pasting this SR's report. CR round numbering is unchanged. `Behaviour-change: no` re-reviews with CR only. A missing or malformed `Behaviour-change:` line is re-asked once like `Parallel-safe:` (see `dispatching-parallel-agents` "Structural probe"); still missing -> route through SR, never default to `no`. In wave mode the SR re-review targets the task(s) whose files the fix touched.
|
|
81
81
|
|
|
82
82
|
Every fix re-dispatch (implementer) and code-review re-review carries the consuming task/wave's `SCOPED_TEST_COMMANDS`; spec-reviewer re-reviews carry the `**Tests:**` block and `- Test:` paths, no commands - SR never executes.
|
|
83
83
|
|
|
84
|
-
**The sequence.** Each review that finds issues is a decision point: read the `TRAJECTORY:` line before dispatching anything (review 1 has no line - on issues, dispatch fix 1). Any clean review ends the loop.
|
|
84
|
+
**The sequence.** Each review that finds issues is a decision point: read the `TRAJECTORY:` line before dispatching anything (review 1 has no line - on issues, dispatch fix 1). Any clean review ends the loop. For code review, *issue-bearing* means verdict `FIX_FIRST` and *clean* means verdict `SHIP`, whatever Minors the report lists; spec review keeps its gaps/acceptance meaning. `REJECT` is neither: stop and report per `stop-note.md`; the loop never enters.
|
|
85
85
|
|
|
86
86
|
1. **Review 1** (first review - no sentinel). Issues -> dispatch fix 1.
|
|
87
87
|
2. **Review 2** (re-review). Issues -> if `STAGNANT`, escalate now - no fix 2. Otherwise dispatch fix 2.
|
|
@@ -170,7 +170,7 @@ Auto-selected at handoff by `writing-plans` (any wave with ≥2 tasks) when the
|
|
|
170
170
|
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, plus its `**Tests:**` block and `- Test:` paths as contract — 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 `SCOPED_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.
|
|
171
171
|
4. **Integrate.** `git -C <worktree> 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.
|
|
172
172
|
5. **Test gate.** Run the wave's `SCOPED_TEST_COMMANDS` (union of its tasks' `Tests:` commands) on the integrated tree as `(cd "<worktree>" && <command>)` for each command — 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).
|
|
173
|
-
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 the wave's `SCOPED_TEST_COMMANDS`. Code-quality review on the integrated wave diff;
|
|
173
|
+
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 the wave's `SCOPED_TEST_COMMANDS`. Code-quality review on the integrated wave diff; when issue-bearing, re-dispatch the implementer per `/skill:requesting-code-review` Fix rounds and re-review, to ✅ within [Fix-Loop Rounds](#fix-loop-rounds), same as sequential. Skip for doc-only waves (SR-only per the commit precondition below).
|
|
174
174
|
7. **Commit and complete the wave.** After the gate passes, commit with `git -C <worktree> add ... && git -C <worktree> commit ...`, then 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.
|
|
175
175
|
|
|
176
176
|
**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.
|
|
@@ -221,7 +221,7 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
|
|
|
221
221
|
|
|
222
222
|
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.)
|
|
223
223
|
1. **Parent full verification.** Run the complete plan-header `**Verification:**` command set foreground in the worktree via `(cd "<worktree>" && <command>)`: 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`.
|
|
224
|
-
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.
|
|
224
|
+
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. Act on the verdict per `/skill:requesting-code-review` Fix rounds; carry the final `SHIP` report's Minor finding lines verbatim into the closure block's `whole-diff minors:` section (`verification-before-completion/reference/conformance-check.md` "Closure / conformance"), or `whole-diff minors: none`. 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.
|
|
225
225
|
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.
|
|
226
226
|
|
|
227
227
|
**Happy-path run - first action of this step, only when the plan header carries a `**Happy path:**` line or the diff selects a row.** The overrides file's `## Happy path` table (schema in the README overrides contract) names path-prefixed rows; the plan header's line is the plan-time default.
|