pi-gauntlet 5.8.1 → 5.9.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 +8 -0
- package/README.md +6 -0
- package/bin/gauntlet-spec-index.mjs +223 -0
- package/bin/gauntlet-spec-index.test.mjs +173 -0
- package/extensions/lib/checkout.test.ts +59 -0
- package/extensions/lib/checkout.ts +57 -0
- package/extensions/lib/gauntlet-settings-loader.test.ts +32 -0
- package/extensions/lib/gauntlet-settings-loader.ts +11 -7
- package/extensions/lib/phase-tracker-helpers.test.ts +17 -0
- package/extensions/lib/phase-tracker-helpers.ts +16 -17
- package/extensions/lib/telemetry-paths.test.ts +10 -0
- package/extensions/lib/telemetry-paths.ts +9 -5
- package/extensions/phase-tracker.test.ts +75 -23
- package/extensions/phase-tracker.ts +20 -29
- package/extensions/telemetry.test.ts +51 -10
- package/extensions/telemetry.ts +57 -41
- package/extensions/verify-before-ship.test.ts +16 -0
- package/extensions/verify-before-ship.ts +4 -4
- package/package.json +7 -1
- package/skills/brainstorming/SKILL.md +11 -28
- package/skills/brainstorming/gatherer.md +19 -6
- package/skills/brainstorming/reference/superseding.md +18 -0
- package/skills/finishing-a-development-branch/SKILL.md +51 -88
- package/skills/gauntlet-resume/SKILL.md +1 -3
- package/skills/gauntlet-resume/reference/brief-contract.md +1 -1
- package/skills/roasting-the-spec/SKILL.md +1 -1
- package/skills/subagent-driven-development/SKILL.md +18 -15
- package/skills/using-git-worktrees/SKILL.md +46 -39
- package/skills/verification-before-completion/reference/conformance-check.md +6 -6
- package/skills/verification-before-completion/reference/settings-precedence.md +8 -6
- package/skills/writing-plans/SKILL.md +5 -3
- package/skills/writing-plans/reference/plan-contract.md +1 -1
package/extensions/telemetry.ts
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
11
|
import * as nodeFs from "node:fs";
|
|
12
|
-
import { dirname, join, posix } from "node:path";
|
|
12
|
+
import { dirname, isAbsolute, join, posix, resolve } from "node:path";
|
|
13
13
|
import * as piRuntime from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { DEFAULT_TEST_COMMANDS, resolveTelemetry, settingsErrorWarning, type TelemetryResolved } from "./lib/gauntlet-settings.ts";
|
|
17
17
|
import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
|
|
18
|
+
import { checkoutOf } from "./lib/checkout.ts";
|
|
18
19
|
import { CONTEXT_DRAFT_MARKER } from "./lib/phase-tracker-helpers.ts";
|
|
19
20
|
import { sha256 } from "./lib/plan-check.ts";
|
|
20
21
|
import { SUPERSEDED_BY_RE, aggregateNumstat, isPlanPath, isSpecPath, isSupersededByBanner, matchDiscardStatement, matchShipStatement, matchTestStatement, parseSpecLinks, planSpecHeader, recordPathFor, repoRelativeToolPath, toPosix, truncateCommand } from "./lib/telemetry-paths.ts";
|
|
@@ -100,9 +101,9 @@
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
function realSettings(cwd: string): SettingsSnapshot {
|
|
103
|
-
const { gauntlet, errors } = loadGauntletSettings(cwd);
|
|
104
|
+
const { gauntlet, errors, root } = loadGauntletSettings(cwd);
|
|
104
105
|
const telemetry = resolveTelemetry(gauntlet);
|
|
105
|
-
const sm = SettingsManager.create(
|
|
106
|
+
const sm = SettingsManager.create(root, getAgentDir());
|
|
106
107
|
const layer = (s: unknown) => ((s as { subagents?: { agentOverrides?: unknown } })?.subagents?.agentOverrides ?? undefined) as Record<string, unknown> | undefined;
|
|
107
108
|
const preset = layer(sm.getGlobalSettings());
|
|
108
109
|
const repo = layer(sm.getProjectSettings());
|
|
@@ -158,8 +159,7 @@
|
|
|
158
159
|
|
|
159
160
|
export default function (pi: ExtensionAPI, deps: Deps = realDeps) {
|
|
160
161
|
let phases: PhaseMap = emptyPhases();
|
|
161
|
-
let toplevel: string | undefined;
|
|
162
|
-
let inGit = false;
|
|
162
|
+
let toplevel: string | undefined; // toplevel of the bound record's checkout; undefined until bind
|
|
163
163
|
let boundSpec: string | undefined; // repo-relative
|
|
164
164
|
let record: TelemetryRecord | undefined;
|
|
165
165
|
const predecessorLinks = new Set<string>();
|
|
@@ -180,12 +180,13 @@
|
|
|
180
180
|
return s;
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
// Resolves a tool path (relative to ctx.cwd or absolute) to its owning checkout and
|
|
184
|
+
// checkout-relative key. undefined outside any checkout.
|
|
185
|
+
const locate = async (ctx: ExtensionContext, p: string): Promise<{ toplevel: string; rel: string } | undefined> => {
|
|
186
|
+
const candidateAbs = isAbsolute(p) ? p : resolve(ctx.cwd, p);
|
|
187
|
+
const co = await checkoutOf(candidateAbs, deps.git);
|
|
188
|
+
const rel = co ? repoRelativeToolPath(co.toplevel, ctx.cwd, p) : undefined;
|
|
189
|
+
return co && rel ? { toplevel: co.toplevel, rel } : undefined;
|
|
189
190
|
};
|
|
190
191
|
|
|
191
192
|
const live = (): Accumulators => record
|
|
@@ -228,7 +229,7 @@
|
|
|
228
229
|
};
|
|
229
230
|
|
|
230
231
|
const commitRecord = async () => {
|
|
231
|
-
if (!record || !recordRel || frozen || !
|
|
232
|
+
if (!record || !recordRel || frozen || !toplevel) return;
|
|
232
233
|
const text = deps.fs.readFile(abs(recordRel)) ?? "";
|
|
233
234
|
if (text === lastCommitted) return;
|
|
234
235
|
const paths = [recordRel, ...(oldRecordRel ? [oldRecordRel] : [])];
|
|
@@ -273,7 +274,8 @@
|
|
|
273
274
|
return newRecord({ spec: specRel, session: sessionId, now: buffer[0]?.ts ?? deps.now(), runId: randomUUID() });
|
|
274
275
|
};
|
|
275
276
|
|
|
276
|
-
const bind = async (specRel: string, snap: SettingsSnapshot) => {
|
|
277
|
+
const bind = async (specRel: string, snap: SettingsSnapshot, specToplevel: string) => {
|
|
278
|
+
toplevel = specToplevel;
|
|
277
279
|
currentDir = snap.telemetry.dir;
|
|
278
280
|
boundSpec = specRel;
|
|
279
281
|
recordRel = recordPathFor(currentDir, specRel);
|
|
@@ -302,7 +304,6 @@
|
|
|
302
304
|
const approval = record.events.find((e) => e.kind === "phase" && e.name === "brainstorm" && (e.action === "complete" || e.action === "skip"));
|
|
303
305
|
if (approval) record.approved_at ??= approval.ts;
|
|
304
306
|
buffer = [];
|
|
305
|
-
if (!inGit) warn("not a git repository: record is written but never committed");
|
|
306
307
|
if (!settingsWarned && (snap.errors.length || snap.telemetry.warning)) {
|
|
307
308
|
settingsWarned = true;
|
|
308
309
|
if (snap.errors.length) warn(settingsErrorWarning(snap.errors));
|
|
@@ -312,6 +313,7 @@
|
|
|
312
313
|
};
|
|
313
314
|
|
|
314
315
|
const unbind = () => {
|
|
316
|
+
toplevel = undefined;
|
|
315
317
|
boundSpec = undefined;
|
|
316
318
|
record = undefined;
|
|
317
319
|
recordRel = undefined;
|
|
@@ -347,8 +349,8 @@
|
|
|
347
349
|
warn(`record move skipped: ${to} already exists`);
|
|
348
350
|
return false;
|
|
349
351
|
}
|
|
350
|
-
const trackedResult =
|
|
351
|
-
const tracked = trackedResult
|
|
352
|
+
const trackedResult = await deps.git(["ls-files", "--error-unmatch", "--", from], toplevel!);
|
|
353
|
+
const tracked = trackedResult.code === 0 && trackedResult.stdout.trim() !== "";
|
|
352
354
|
try {
|
|
353
355
|
deps.fs.mkdirp(dirname(abs(to)));
|
|
354
356
|
if (tracked) {
|
|
@@ -408,14 +410,13 @@
|
|
|
408
410
|
const snap = enabledSettings(ctx);
|
|
409
411
|
if (!snap) return;
|
|
410
412
|
sessionId = ctx.sessionManager.getSessionId();
|
|
411
|
-
await ensureToplevel(ctx);
|
|
412
413
|
currentDir = snap.telemetry.dir;
|
|
413
414
|
const replay = replayBranch(ctx.sessionManager.getBranch());
|
|
414
415
|
phases = replay.phases;
|
|
415
416
|
const candidate = replay.planCheckSpec ?? replay.lastSpecWrite;
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
418
|
-
await bind(rel, snap);
|
|
417
|
+
const loc = candidate ? await locate(ctx, candidate) : undefined;
|
|
418
|
+
if (loc && isSpecPath(loc.rel)) {
|
|
419
|
+
await bind(loc.rel, snap, loc.toplevel);
|
|
419
420
|
await flush(false);
|
|
420
421
|
}
|
|
421
422
|
});
|
|
@@ -436,19 +437,20 @@
|
|
|
436
437
|
const snap = enabledSettings(ctx);
|
|
437
438
|
if (!snap) return undefined;
|
|
438
439
|
if (!sessionId) sessionId = ctx.sessionManager.getSessionId();
|
|
439
|
-
await ensureToplevel(ctx);
|
|
440
440
|
if (event.toolName === "phase_tracker" && !event.isError) {
|
|
441
441
|
await applyPhaseDetails(event.details as never, snap);
|
|
442
442
|
return undefined;
|
|
443
443
|
}
|
|
444
444
|
if (event.toolName === "plan_check" && !event.isError) {
|
|
445
445
|
const d = event.details as { status?: string; specPath?: string; planPath?: string } | undefined;
|
|
446
|
-
const
|
|
447
|
-
const
|
|
446
|
+
const specLoc = d?.specPath ? await locate(ctx, d.specPath) : undefined;
|
|
447
|
+
const planLoc = d?.planPath ? await locate(ctx, d.planPath) : undefined;
|
|
448
|
+
const spec = specLoc?.rel;
|
|
449
|
+
const plan = planLoc?.rel;
|
|
448
450
|
const pass = d?.status === "pass";
|
|
449
|
-
if (pass &&
|
|
451
|
+
if (pass && specLoc && (specLoc.rel !== boundSpec || specLoc.toplevel !== toplevel)) {
|
|
450
452
|
if (record) unbind();
|
|
451
|
-
await bind(
|
|
453
|
+
await bind(specLoc.rel, snap, specLoc.toplevel);
|
|
452
454
|
}
|
|
453
455
|
live().gates.plan_rounds += 1;
|
|
454
456
|
pushEvent({ kind: "plan_check", pass, spec, plan });
|
|
@@ -457,9 +459,12 @@
|
|
|
457
459
|
}
|
|
458
460
|
if ((event.toolName === "write" || event.toolName === "edit" || event.toolName === "read") && !event.isError) {
|
|
459
461
|
const rawPath = (event.input as { path?: unknown }).path;
|
|
460
|
-
const
|
|
461
|
-
if (!
|
|
462
|
-
|
|
462
|
+
const loc = typeof rawPath === "string" ? await locate(ctx, rawPath) : undefined;
|
|
463
|
+
if (!loc) {
|
|
464
|
+
if (typeof rawPath === "string" && isSpecPath(toPosix(rawPath))) warn(`spec ${rawPath} is outside a git checkout; telemetry not recorded`);
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
return onSpecInteraction(event.toolName, loc, event, snap);
|
|
463
468
|
}
|
|
464
469
|
if (event.toolName === "subagent" && !event.isError) {
|
|
465
470
|
await onSubagentResult(event.details, event.content);
|
|
@@ -493,23 +498,35 @@
|
|
|
493
498
|
});
|
|
494
499
|
|
|
495
500
|
// Binding + spec-write bookkeeping; the bound-spec hooks are defined in block 2.
|
|
496
|
-
const onSpecInteraction = async (tool: "write" | "edit" | "read", rel: string, event: { input: unknown; content: unknown[] }, snap: SettingsSnapshot): Promise<unknown> => {
|
|
501
|
+
const onSpecInteraction = async (tool: "write" | "edit" | "read", loc: { toplevel: string; rel: string }, event: { input: unknown; content: unknown[] }, snap: SettingsSnapshot): Promise<unknown> => {
|
|
502
|
+
const rel = loc.rel;
|
|
497
503
|
if (!boundSpec) {
|
|
498
504
|
if ((tool === "write" || tool === "edit") && isSpecPath(rel)) {
|
|
499
|
-
await bind(rel, snap);
|
|
505
|
+
await bind(rel, snap, loc.toplevel);
|
|
500
506
|
const patch = onBoundSpecWrite(tool, rel, event);
|
|
501
507
|
await flush(true);
|
|
502
508
|
return patch;
|
|
503
509
|
} else if (isPlanPath(rel)) {
|
|
504
|
-
const spec = planSpecHeader(deps.fs.readFile(
|
|
505
|
-
const specRel = spec ? repoRelativeToolPath(toplevel
|
|
506
|
-
if (specRel && deps.fs.exists(
|
|
507
|
-
await bind(specRel, snap);
|
|
510
|
+
const spec = planSpecHeader(deps.fs.readFile(join(loc.toplevel, rel)) ?? "");
|
|
511
|
+
const specRel = spec ? repoRelativeToolPath(loc.toplevel, loc.toplevel, spec) : undefined;
|
|
512
|
+
if (specRel && deps.fs.exists(join(loc.toplevel, specRel))) {
|
|
513
|
+
await bind(specRel, snap, loc.toplevel);
|
|
508
514
|
await flush(true);
|
|
509
515
|
}
|
|
510
516
|
}
|
|
511
517
|
return undefined;
|
|
512
518
|
}
|
|
519
|
+
if (loc.toplevel !== toplevel) {
|
|
520
|
+
// Another checkout: a fresh run, never a rename of this record.
|
|
521
|
+
if ((tool === "write" || tool === "edit") && isSpecPath(rel)) {
|
|
522
|
+
unbind();
|
|
523
|
+
await bind(rel, snap, loc.toplevel);
|
|
524
|
+
const patch = onBoundSpecWrite(tool, rel, event);
|
|
525
|
+
await flush(true);
|
|
526
|
+
return patch;
|
|
527
|
+
}
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
513
530
|
if (tool === "read") return undefined;
|
|
514
531
|
if (rel === boundSpec) {
|
|
515
532
|
const patch = onBoundSpecWrite(tool, rel, event);
|
|
@@ -603,7 +620,7 @@
|
|
|
603
620
|
let pendingShip: { id: string; option: "squash" | "pr" | "discard" } | undefined;
|
|
604
621
|
|
|
605
622
|
const computeDiff = async (snap: SettingsSnapshot) => {
|
|
606
|
-
if (!record || !
|
|
623
|
+
if (!record || !toplevel) return;
|
|
607
624
|
let baseRef: string | undefined;
|
|
608
625
|
for (const ref of BASE_REFS) {
|
|
609
626
|
if ((await deps.git(["rev-parse", "--verify", "--quiet", ref], toplevel!)).code === 0) {
|
|
@@ -665,13 +682,12 @@
|
|
|
665
682
|
if (event.toolName === "write") {
|
|
666
683
|
const rawPath = (event.input as { path?: unknown }).path;
|
|
667
684
|
if (typeof rawPath === "string" && phases.brainstorm.status === "in_progress") {
|
|
668
|
-
await
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
const existing = deps.fs.readFile(join(toplevel!, recRel));
|
|
685
|
+
const loc = await locate(ctx, rawPath);
|
|
686
|
+
if (loc && isSpecPath(loc.rel)) {
|
|
687
|
+
const recRel = recordPathFor(snap.telemetry.dir, loc.rel);
|
|
688
|
+
const existing = deps.fs.readFile(join(loc.toplevel, recRel));
|
|
673
689
|
const parsed = existing ? parseRecord(existing) : undefined;
|
|
674
|
-
if (parsed?.shipped_at) return { block: true, reason: guardReason(rel, parsed.shipped_at, recRel) };
|
|
690
|
+
if (parsed?.shipped_at) return { block: true, reason: guardReason(loc.rel, parsed.shipped_at, recRel) };
|
|
675
691
|
}
|
|
676
692
|
}
|
|
677
693
|
return undefined;
|
|
@@ -72,6 +72,22 @@ test("gh pr create warns when unverified", async () => {
|
|
|
72
72
|
assert.match(warning ?? "", /ran without verification/);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
test("git -C <worktree> push and merge --squash are ship events (AC 5)", async () => {
|
|
76
|
+
for (const cmd of ["git -C /p push -u origin HEAD", "git -C /p merge --squash f"]) {
|
|
77
|
+
const h = harness();
|
|
78
|
+
await editSource(h);
|
|
79
|
+
await h.emitEvent("tool_call", bashCall("s1", cmd));
|
|
80
|
+
assert.match(warningOf(await h.emitEvent("tool_result", bashResult("s1"))) ?? "", /ran without verification/, cmd);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a quoted mention of git push is not a ship event", async () => {
|
|
85
|
+
const h = harness();
|
|
86
|
+
await editSource(h);
|
|
87
|
+
await h.emitEvent("tool_call", bashCall("q1", 'rg "git push" skills/'));
|
|
88
|
+
assert.equal((await h.emitEvent("tool_result", bashResult("q1")))[0], undefined);
|
|
89
|
+
});
|
|
90
|
+
|
|
75
91
|
test("a passing recognised run clears the warning for push", async () => {
|
|
76
92
|
const h = harness();
|
|
77
93
|
await editSource(h);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Verify-before-ship extension
|
|
3
3
|
*
|
|
4
|
-
* Single-session verification gate for shipping commands
|
|
4
|
+
* Single-session verification gate for statement-start shipping commands: git merge
|
|
5
|
+
* --squash, git push, and gh pr create (with any git global flags).
|
|
5
6
|
* Local commits are not ship events; commit-time review-cadence advisories live in
|
|
6
7
|
* phase-tracker. Tracks whether a recognised verification command has succeeded
|
|
7
8
|
* since the last source-file write; injects an advisory warning into the tool
|
|
@@ -32,8 +33,7 @@ import {
|
|
|
32
33
|
settingsErrorWarning,
|
|
33
34
|
} from "./lib/gauntlet-settings.ts";
|
|
34
35
|
import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
|
|
35
|
-
|
|
36
|
-
const SHIP_CMD = /\b(git\s+push|gh\s+pr\s+create)\b/;
|
|
36
|
+
import { matchShipStatement } from "./lib/telemetry-paths.ts";
|
|
37
37
|
|
|
38
38
|
const SOURCE_EXT = /\.(ts|tsx|js|jsx|py|rb|go|rs|java|swift|kt)$/;
|
|
39
39
|
const TEST_PATH = /(^|\/)(tests?|__tests__)\/|\.(test|spec)\.|_test\.(py|go|rb)$/;
|
|
@@ -101,7 +101,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
101
101
|
return undefined;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
if (
|
|
104
|
+
if (matchShipStatement(command) && !verified) {
|
|
105
105
|
if (settingsWarned && !settingsErrShown) {
|
|
106
106
|
addShipWarning(event.toolCallId, settingsWarned);
|
|
107
107
|
settingsErrShown = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-gauntlet",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.1",
|
|
4
4
|
"description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
"bin",
|
|
32
32
|
"CHANGELOG.md"
|
|
33
33
|
],
|
|
34
|
+
"bin": {
|
|
35
|
+
"gauntlet-spec-index": "bin/gauntlet-spec-index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=24.15.0"
|
|
39
|
+
},
|
|
34
40
|
"pi": {
|
|
35
41
|
"skills": [
|
|
36
42
|
"./skills"
|
|
@@ -22,7 +22,7 @@ You **may**:
|
|
|
22
22
|
- Read code and docs
|
|
23
23
|
- Run the existing system to observe its **current** behaviour — this is research and feeds the spec (boot a local service, replay a sample request, capture a baseline classification, etc.)
|
|
24
24
|
- Write to the project's `doc/specs/` directory
|
|
25
|
-
- `edit` a predecessor spec in the project's spec directory to add a supersession banner (see [Marking superseded specs](
|
|
25
|
+
- `edit` a predecessor spec in the project's spec directory to add a supersession banner (see [Marking superseded specs](reference/superseding.md)) — the `edit` prohibition at spec-writing binds the spec being written, not a predecessor file
|
|
26
26
|
|
|
27
27
|
You may **not**:
|
|
28
28
|
|
|
@@ -65,7 +65,7 @@ Work through the items below **in order**. This is your own checklist to follow,
|
|
|
65
65
|
then state the chat premise note (section 3) before approaches
|
|
66
66
|
5. **Propose 2-3 approaches** — with trade-offs and a recommendation
|
|
67
67
|
6. **Present the design** — in two rounds, one approval each
|
|
68
|
-
7. **Write the spec** — to `doc/specs/` (see [Filename Convention](#filename-convention)); then mark any known superseded predecessor(s) per [Marking superseded specs](
|
|
68
|
+
7. **Write the spec** — to `doc/specs/` (see [Filename Convention](#filename-convention)); then mark any known superseded predecessor(s) per [Marking superseded specs](reference/superseding.md), at the exact-order position defined in [Spec Self-Review](#spec-self-review-before-user-review-gate)
|
|
69
69
|
8. **Spec self-review (lint)** — placeholder scan + internal consistency + documentation named, run inline
|
|
70
70
|
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
|
|
71
71
|
10. **Re-run placeholder scan** — after the critique pass returns, re-scan the **applied** spec for placeholders its edits may have introduced; if a predecessor banner exists, confirm its `<scope>` still matches the applied spec (critique edits can change what is superseded); surface any ambiguity the critique could not safely resolve at the user gate
|
|
@@ -92,8 +92,8 @@ If the project's `AGENTS.md` calls out additional reading for a specific area (e
|
|
|
92
92
|
The spec is the **first commit in a dedicated worktree**, not a separate commit on `main`. Before drafting any spec content:
|
|
93
93
|
|
|
94
94
|
1. Invoke `/skill:using-git-worktrees`. Worktrees live under `.worktrees/` at the repo root, or wherever a project-native worktree script places them.
|
|
95
|
-
2.
|
|
96
|
-
3.
|
|
95
|
+
2. Carry the worktree path from the `using-git-worktrees` Step 4 report (`Worktree ready at <full-path>`) as a value: the spec path is `<full-path>/doc/specs/<filename>.md`, every dispatch below sets `cwd: "<full-path>"`, and git runs as `git -C <full-path> ...`. The process cwd stays where pi was launched.
|
|
96
|
+
3. Write the spec at that path, run self-review, commit with `git -C <full-path>`, hand off.
|
|
97
97
|
|
|
98
98
|
Spec doc, plan doc, and implementation are all developed in the same worktree; the squash ships the spec and the implementation, with the ephemeral plan stripped before landing (see `/skill:finishing-a-development-branch`).
|
|
99
99
|
|
|
@@ -103,6 +103,8 @@ Spec doc, plan doc, and implementation are all developed in the same worktree; t
|
|
|
103
103
|
|
|
104
104
|
### 1. Check git state
|
|
105
105
|
|
|
106
|
+
Check git state in the primary checkout (the process cwd; no worktree exists yet):
|
|
107
|
+
|
|
106
108
|
```bash
|
|
107
109
|
git status
|
|
108
110
|
git --no-pager log --oneline -5
|
|
@@ -239,25 +241,6 @@ overwrite reuses the path. If the questionary invalidated the slug, rename at
|
|
|
239
241
|
spec-writing: write the spec at the new path **and delete the old draft file**
|
|
240
242
|
(nothing was committed, so this is free).
|
|
241
243
|
|
|
242
|
-
## Marking superseded specs
|
|
243
|
-
|
|
244
|
-
When the new spec replaces a prior spec — fully or in part — (from the draft's scout recon or the request), mark the predecessor. No mechanical sweep: grep or path-overlap hits never decide supersession.
|
|
245
|
-
|
|
246
|
-
- `edit` the predecessor spec (in the project's spec directory, per [Project Routing](#project-routing)) to insert, after its title line and a blank line, one banner line per successor:
|
|
247
|
-
|
|
248
|
-
```markdown
|
|
249
|
-
> **Superseded by:** [<repo-relative path to successor>](<href relative to THIS file>) - <scope>
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
- The visible label is the successor's repo-relative path; the href is computed relative to the predecessor's own directory (Markdown resolves links from the containing file). Same directory: `[doc/specs/B.md](./B.md)`.
|
|
253
|
-
- `<scope>` is the value after the ` - ` separator: `fully`, or the named superseded section(s), e.g. `"Settings resolution" section only`. The scope value itself carries no leading dash — the template above already supplies the separator.
|
|
254
|
-
- Banners are **append-only**: add below any existing supersession lines, formatted or free-form prose. One old spec may accumulate banners from multiple successors. No migration, no dedup.
|
|
255
|
-
- **No transitive rewrite**: if A points at B and B is later superseded by C, A keeps pointing at B; the reader hops.
|
|
256
|
-
- **Mark, never delete.** Delete/archive policy is consumer territory via overrides.
|
|
257
|
-
- **Coverage limits**: unmarked does NOT mean current (code drift, abandoned designs, and partial ships produce no successor spec); marked does NOT mean dead (partial supersession leaves live sections).
|
|
258
|
-
- Predecessor in a **different service's spec directory**: out of scope — record it in the new spec's Open Questions instead of editing outside the write grant.
|
|
259
|
-
- **Override contract**: the gauntlet overrides file (see Project overrides) may replace the banner *syntax*; placement, append-only, no-transitive-rewrite, and mark-never-delete stay fixed. A syntax override entry must itself state the scout-citation guidance for its format (the shipped `gatherer.md` guidance names only the default banner).
|
|
260
|
-
|
|
261
244
|
## Spec Self-Review (Before User Review Gate)
|
|
262
245
|
|
|
263
246
|
Spec-writing replaces the context draft, in this exact order:
|
|
@@ -273,7 +256,7 @@ Spec-writing replaces the context draft, in this exact order:
|
|
|
273
256
|
guard is a backstop, not the primary check.
|
|
274
257
|
4. **After the line-1 check and before the inline lint**, `edit` any known
|
|
275
258
|
predecessor spec to insert its supersession banner (see
|
|
276
|
-
[Marking superseded specs](
|
|
259
|
+
[Marking superseded specs](reference/superseding.md)). This position is fixed:
|
|
277
260
|
the banner is written after any slug rename, so it always cites the final path.
|
|
278
261
|
|
|
279
262
|
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.
|
|
@@ -290,7 +273,7 @@ The first three checks — **placeholder scan**, **internal consistency**, and *
|
|
|
290
273
|
- **Otherwise** → dispatch one fresh `worker` that applies the scope + ambiguity checks and fixes them in place:
|
|
291
274
|
|
|
292
275
|
```
|
|
293
|
-
subagent({ agent: "worker", context: "fresh", async: false, cwd: "<abs worktree path, from
|
|
276
|
+
subagent({ agent: "worker", context: "fresh", async: false, cwd: "<abs worktree path, from the using-git-worktrees Step 4 report>", task:
|
|
294
277
|
"Problem statement: <the problem the spec addresses + the user's stated intent>.\n" +
|
|
295
278
|
"Read the spec at <abs path to doc/specs/...>. Edit ONLY that file. Apply two checks and\n" +
|
|
296
279
|
"fix what you find in place: (1) Scope — does every paragraph serve the goal? Cut filler;\n" +
|
|
@@ -321,14 +304,14 @@ SUMMARY_PATH=$(mktemp "${TMPDIR:-/tmp}/gauntlet-spec-summary.XXXXXX") # absolu
|
|
|
321
304
|
```
|
|
322
305
|
|
|
323
306
|
```
|
|
324
|
-
subagent({ agent: "spec-summarizer", context: "fresh", async: false, cwd: "<abs worktree path, from
|
|
307
|
+
subagent({ agent: "spec-summarizer", context: "fresh", async: false, cwd: "<abs worktree path, from the using-git-worktrees Step 4 report>",
|
|
325
308
|
output: "<SUMMARY_PATH>", outputMode: "file-only", task:
|
|
326
309
|
"Summarize the spec at <abs path to doc/specs/...> for the user review gate. Read ONLY that file." })
|
|
327
310
|
```
|
|
328
311
|
|
|
329
312
|
`<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.
|
|
330
313
|
|
|
331
|
-
Then commit the spec — staging any predecessor spec edited per [Marking superseded specs](
|
|
314
|
+
Then commit the spec — staging any predecessor spec edited per [Marking superseded specs](reference/superseding.md) alongside it; a change request at the gate that renames, materially revises, or drops the spec also reconciles the predecessor's banner before recommitting. 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 (`Coverage:` when present, then `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):
|
|
332
315
|
|
|
333
316
|
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.
|
|
334
317
|
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.
|
|
@@ -373,7 +356,7 @@ phase_tracker({ action: "complete", phase: "brainstorm" })
|
|
|
373
356
|
|
|
374
357
|
Execute this section in place from any later phase. Do not invoke `/skill:brainstorming` (its entry resets both trackers). Worktree, spec commits, and plan survive.
|
|
375
358
|
|
|
376
|
-
1. Edit the spec. Show `git --no-pager diff -- <spec path>` and one line of impact (affected plan tasks / waves, or "no plan yet").
|
|
359
|
+
1. Edit the spec. Show `git -C <abs worktree path> --no-pager diff -- <spec path>` and one line of impact (affected plan tasks / waves, or "no plan yet").
|
|
377
360
|
2. Wait for approval. Change request -> revise, re-show.
|
|
378
361
|
3. No plan yet -> commit the spec; continue. Plan exists -> update affected anchors and tasks: `plan_tracker` `add` for new tasks; anchor-changed completed tasks are reopened as `in_progress` and re-run the task loop (`update` never sets `pending`). A removed task is deleted from the plan; then re-`init` the tracker with `{ name, status }` elements: preserved tasks keep their order and statuses, reopened tasks are `in_progress` in place, every still-`pending` task (including newly added ones, whatever wave label they carry) trails the non-pending ones, removed tasks are the only deletions (the only permitted `init` after handoff; never `clear`). Re-run `plan_check` until it passes, commit spec + plan together; continue. A task reopened while `verify` or `ship` is in progress: `phase_tracker({ action: "skip", phase: "<current>", reason: "amendment reopened Task N" })`, then `phase_tracker({ action: "start", phase: "implement", force: true })`; later phases re-enter with `force: true` and rerun in full.
|
|
379
362
|
|
|
@@ -35,6 +35,10 @@ subagent({
|
|
|
35
35
|
Absolute `output:` paths are mandatory: relative paths in parallel mode resolve
|
|
36
36
|
against the worktree and would get committed.
|
|
37
37
|
|
|
38
|
+
`<SPEC_INDEX>` is `<directory of this skill's SKILL.md>/../../bin/gauntlet-spec-index.mjs`,
|
|
39
|
+
resolved to an absolute path by the main loop from the skill's `<location>` in the system prompt
|
|
40
|
+
before pasting the task.
|
|
41
|
+
|
|
38
42
|
## Task templates
|
|
39
43
|
|
|
40
44
|
Scout (always dispatched):
|
|
@@ -46,12 +50,21 @@ Scout (always dispatched):
|
|
|
46
50
|
> exact paths and line ranges. If a spec you cite carries a supersession marker
|
|
47
51
|
> (default: a `> **Superseded by:**` banner; the project's overrides may define
|
|
48
52
|
> another format), follow the successor for the superseded scope and cite it
|
|
49
|
-
> instead; cite the old spec only for its unsuperseded sections
|
|
50
|
-
> check:
|
|
51
|
-
>
|
|
52
|
-
>
|
|
53
|
-
>
|
|
54
|
-
>
|
|
53
|
+
> instead; cite the old spec only for its unsuperseded sections (banner contract:
|
|
54
|
+
> `reference/superseding.md`). Predecessor check: compose a 5-15 term keyword query
|
|
55
|
+
> from the request (topic nouns, component names, file names - not stop words; if
|
|
56
|
+
> the request is only a ticket reference, take the terms from the ticket title via
|
|
57
|
+
> the tracker CLI when one is available, otherwise use the fallback below). Run
|
|
58
|
+
> `node <SPEC_INDEX> --query '<keywords>' --limit 10` from the worktree root,
|
|
59
|
+
> keeping the keywords inside single quotes, and treat its rows as the candidate
|
|
60
|
+
> list. If the command fails, fall back to listing the project's spec directory
|
|
61
|
+
> and reading titles and `**Goal:**` lines, and write
|
|
62
|
+
> `Spec index unavailable - predecessor check used directory listing.` in your
|
|
63
|
+
> handoff. Either way open at
|
|
64
|
+
> most five candidates whose topic matches this request, and name any whose design
|
|
65
|
+
> this request replaces or amends with the section(s) affected - `Predecessor:
|
|
66
|
+
> <path>, <scope>` - or `Predecessor: none`. Judge by topic; shared file paths never
|
|
67
|
+
> decide. End with an
|
|
55
68
|
> "Open questions that matter for the spec"
|
|
56
69
|
> section. Compact handoff, not a dump.
|
|
57
70
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Marking superseded specs
|
|
2
|
+
|
|
3
|
+
When the new spec replaces a prior spec — fully or in part — (from the draft's scout recon or the request), mark the predecessor. No mechanical sweep: grep or path-overlap hits never decide supersession.
|
|
4
|
+
|
|
5
|
+
- `edit` the predecessor spec (in the project's spec directory, per [Project Routing](../SKILL.md#project-routing)) to insert, after its title line and a blank line, one banner line per successor:
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
> **Superseded by:** [<repo-relative path to successor>](<href relative to THIS file>) - <scope>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- The visible label is the successor's repo-relative path; the href is computed relative to the predecessor's own directory (Markdown resolves links from the containing file). Same directory: `[doc/specs/B.md](./B.md)`.
|
|
12
|
+
- `<scope>` is the value after the ` - ` separator: `fully`, or the named superseded section(s), e.g. `"Settings resolution" section only`. The scope value itself carries no leading dash — the template above already supplies the separator.
|
|
13
|
+
- Banners are **append-only**: add below any existing supersession lines, formatted or free-form prose. One old spec may accumulate banners from multiple successors. No migration, no dedup.
|
|
14
|
+
- **No transitive rewrite**: if A points at B and B is later superseded by C, A keeps pointing at B; the reader hops.
|
|
15
|
+
- **Mark, never delete.** Delete/archive policy is consumer territory via overrides.
|
|
16
|
+
- **Coverage limits**: unmarked does NOT mean current (code drift, abandoned designs, and partial ships produce no successor spec); marked does NOT mean dead (partial supersession leaves live sections).
|
|
17
|
+
- Predecessor in a **different service's spec directory**: out of scope — record it in the new spec's Open Questions instead of editing outside the write grant.
|
|
18
|
+
- **Override contract**: the gauntlet overrides file (see `../SKILL.md#project-overrides`) may replace the banner *syntax*; placement, append-only, no-transitive-rewrite, and mark-never-delete stay fixed. A syntax override entry must itself state the scout-citation guidance for its format (the shipped `gatherer.md` guidance names only the default banner).
|