pi-gauntlet 8.0.2 → 8.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 +5 -0
- package/README.md +6 -4
- package/agents/loop-implementer.md +7 -5
- package/bin/gauntlet-performance.mjs +10 -1
- package/bin/gauntlet-performance.test.mjs +85 -5
- package/bin/gauntlet-telemetry-seal.mjs +1 -1
- package/extensions/lib/plan-check.test.ts +4 -2
- package/extensions/lib/plan-check.ts +1 -1
- package/extensions/lib/telemetry-collect.test.ts +13 -6
- package/extensions/lib/telemetry-collect.ts +4 -3
- package/extensions/lib/telemetry-record.test.ts +14 -0
- package/extensions/lib/telemetry-record.ts +2 -2
- package/extensions/phase-tracker.test.ts +45 -0
- package/extensions/plan-tracker.test.ts +100 -1
- package/extensions/plan-tracker.ts +99 -3
- package/extensions/telemetry.test.ts +103 -0
- package/extensions/telemetry.ts +19 -6
- package/package.json +2 -2
- package/skills/brainstorming/reference/amendment-surface.md +1 -1
- package/skills/gauntlet-handoff/SKILL.md +1 -1
- package/skills/gauntlet-resume/SKILL.md +4 -5
- package/skills/gauntlet-resume/reference/brief-contract.md +6 -6
- package/skills/gauntlet-resume/reference/reconstruction.md +28 -6
- package/skills/loop-implementation/SKILL.md +22 -27
- package/skills/loop-implementation/reference/coordinated-legs.md +4 -14
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
3
6
|
import registerPlanTracker, { validateSnapshot } from "./plan-tracker.ts";
|
|
4
7
|
|
|
5
8
|
type ToolResult = {
|
|
@@ -7,7 +10,7 @@ type ToolResult = {
|
|
|
7
10
|
details: { action: string; tasks: { name: string; status: string }[]; error?: string };
|
|
8
11
|
};
|
|
9
12
|
|
|
10
|
-
function harness(branch: unknown[] = []) {
|
|
13
|
+
function harness(branch: unknown[] = [], cwd = process.cwd()) {
|
|
11
14
|
const tools: {
|
|
12
15
|
name: string;
|
|
13
16
|
description: string;
|
|
@@ -29,6 +32,7 @@ function harness(branch: unknown[] = []) {
|
|
|
29
32
|
let widgetText: string | undefined;
|
|
30
33
|
const theme = { fg: (_c: string, s: string) => s, bold: (s: string) => s };
|
|
31
34
|
const ctx = {
|
|
35
|
+
cwd,
|
|
32
36
|
hasUI: true,
|
|
33
37
|
ui: {
|
|
34
38
|
setWidget(_id: string, cb?: (tui: unknown, theme: unknown) => { text: string }) {
|
|
@@ -380,6 +384,101 @@ test("legacy-invalid snapshot: first update is rejected listing legacy offenders
|
|
|
380
384
|
assert.deepEqual(repaired.details.tasks.map((t) => t.status), ["complete", "skipped", "complete"]);
|
|
381
385
|
});
|
|
382
386
|
|
|
387
|
+
const ledger = (rows: string[], tail = "", pending = "") => [
|
|
388
|
+
"# Ledger - loop engine", pending,
|
|
389
|
+
"| row | status | attempts | evidence |", "|---|---|---|---|",
|
|
390
|
+
...rows.map(row => `| ${row} |`), "", "## Decisions", tail,
|
|
391
|
+
].join("\n");
|
|
392
|
+
|
|
393
|
+
test("refresh projects ledger states, numeric pending targets and live leg mappings", async () => {
|
|
394
|
+
const dir = mkdtempSync(join(tmpdir(), "plan-refresh-"));
|
|
395
|
+
const path = join(dir, "ledger.md");
|
|
396
|
+
const cases = [
|
|
397
|
+
{
|
|
398
|
+
name: "cut, baseline, dispatched, failed, in flight and pending without cap",
|
|
399
|
+
text: ledger(["1 | cut | 0 | deferred: #1", "2 | done | 0 | baseline", "3 | done | 1 | check \\| passed", "4 | failed | 1 | check failed", "5 | open | 0 |", "6 | open | 0 |"], "row 3 done | passed | row 4\n## Notes\nrow 2 done | not a decision | none", "pending: 5 | home | abc1234"),
|
|
400
|
+
expected: [["row 1", "skipped"], ["row 2", "skipped"], ["row 3", "complete"], ["row 4", "failed"], ["row 5", "in_progress"], ["row 6", "pending"]],
|
|
401
|
+
},
|
|
402
|
+
{ name: "retry overrides failed", text: ledger(["1 | failed | 1 | failed"], "", "pending: 1 | home | abc1234"), expected: [["row 1", "in_progress"]] },
|
|
403
|
+
{ name: "leading-zero target names its numeric row", text: ledger(["1 | open | 0 |"], "", "pending: 01 | home | abc1234"), expected: [["row 1", "in_progress"]] },
|
|
404
|
+
{ name: "done beats stale pending", text: ledger(["1 | done | 1 | passed"], "row 1 done | passed | none", "pending: 1 | home | abc1234"), expected: [["row 1", "complete"]] },
|
|
405
|
+
{ name: "cut beats stale pending and all cut are skipped", text: ledger(["1 | cut | 0 | deferred: #1"], "", "pending: 1 | home | abc1234"), expected: [["row 1", "skipped"]] },
|
|
406
|
+
{ name: "seed names no row", text: ledger(["1 | failed | 1 | failed", "2 | open | 0 |"], "", "pending: seed | home | abc1234"), expected: [["row 1", "failed"], ["row 2", "pending"]] },
|
|
407
|
+
{
|
|
408
|
+
name: "mapped home and companion tasks sort non-pending first",
|
|
409
|
+
text: ledger(["1 | open | 0 |", "2 | done | 1 | passed", "3 | failed | 1 | failed"], "row 2 done | passed | row 3\n\n## Leg mapping\n| row | ac | leg |\n|---|---|---|\n| 1 | 1 | repo-a |\n| 2 | 1 | repo-b |\n| 3 | 2 | repo-a |", "pending: 3 | repo-a | abc1234"),
|
|
410
|
+
expected: [["row 2 (repo-b)", "complete"], ["row 3 (repo-a)", "in_progress"], ["row 1 (repo-a)", "pending"]],
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
for (const row of cases) {
|
|
414
|
+
writeFileSync(path, row.text);
|
|
415
|
+
const h = harness([], dir);
|
|
416
|
+
const result = await h.call({ action: "refresh", path: "ledger.md" });
|
|
417
|
+
assert.equal(result.details.error, undefined, row.name);
|
|
418
|
+
assert.equal(result.details.action, "refresh", row.name);
|
|
419
|
+
assert.deepEqual(result.details.tasks.map(t => [t.name, t.status]), row.expected, row.name);
|
|
420
|
+
assert.deepEqual(validateSnapshot(result.details.tasks as any), [], row.name);
|
|
421
|
+
assert.deepEqual((await h.call({ action: "status" })).details.tasks, result.details.tasks, row.name);
|
|
422
|
+
const done = result.details.tasks.filter(t => ["complete", "skipped"].includes(t.status)).length;
|
|
423
|
+
assert.ok(h.widget()!.includes(`(${done}/${row.expected.length})`), row.name);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("refresh replaces only whole ledger names, preserves Gn statuses and replays its snapshot", async () => {
|
|
428
|
+
const dir = mkdtempSync(join(tmpdir(), "plan-refresh-kept-"));
|
|
429
|
+
const path = join(dir, "ledger.md");
|
|
430
|
+
writeFileSync(path, ledger(["1 | open | 0 |", "2 | open | 0 |", "3 | done | 1 | passed"], "row 3 done | passed | row 1"));
|
|
431
|
+
const h = harness();
|
|
432
|
+
await h.call({ action: "init", tasks: [
|
|
433
|
+
{ name: "row 99", status: "complete" }, { name: "row 8 (old-repo)", status: "failed" },
|
|
434
|
+
{ name: "G1: closed", status: "complete" }, { name: "G2: working", status: "in_progress" },
|
|
435
|
+
"row 4 notes", "G3: pending",
|
|
436
|
+
] });
|
|
437
|
+
const result = await h.call({ action: "refresh", path });
|
|
438
|
+
assert.equal(result.details.error, undefined);
|
|
439
|
+
assert.deepEqual(result.details.tasks, [
|
|
440
|
+
{ name: "row 3", status: "complete" },
|
|
441
|
+
{ name: "G1: closed", status: "complete" }, { name: "G2: working", status: "in_progress" },
|
|
442
|
+
{ name: "row 1", status: "pending" }, { name: "row 2", status: "pending" },
|
|
443
|
+
{ name: "row 4 notes", status: "pending" }, { name: "G3: pending", status: "pending" },
|
|
444
|
+
]);
|
|
445
|
+
assert.deepEqual(validateSnapshot(result.details.tasks as any), []);
|
|
446
|
+
const replay = harness([{ type: "message", message: { role: "toolResult", toolName: "plan_tracker", details: result.details } }]);
|
|
447
|
+
await replay.fire("session_start");
|
|
448
|
+
assert.deepEqual((await replay.call({ action: "status" })).details.tasks, result.details.tasks);
|
|
449
|
+
assert.equal(replay.widget(), h.widget());
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("refresh rejects unreadable or malformed ledgers atomically, including absent numeric pending rows", async () => {
|
|
453
|
+
const dir = mkdtempSync(join(tmpdir(), "plan-refresh-error-"));
|
|
454
|
+
const path = join(dir, "ledger.md");
|
|
455
|
+
const valid = ledger(["1 | open | 0 |"]);
|
|
456
|
+
const h = harness();
|
|
457
|
+
const initial = await h.call({ action: "init", tasks: [{ name: "G1: closed", status: "complete" }, "G2: pending"] });
|
|
458
|
+
const widget = h.widget();
|
|
459
|
+
const cases = [
|
|
460
|
+
{ name: "missing path", path: undefined, text: valid, error: /refresh: cannot read .*path required/ },
|
|
461
|
+
{ name: "unreadable path", path: join(dir, "absent.md"), text: valid, error: /refresh: cannot read .*absent\.md:/ },
|
|
462
|
+
{ name: "wrong discriminator", path, text: "# Plan", error: /line 1 is "# Plan", not a loop ledger/ },
|
|
463
|
+
{ name: "missing header", path, text: valid.replace("| row | status | attempts | evidence |", "| row | state | attempts | evidence |"), error: /row table malformed at line \d+/ },
|
|
464
|
+
{ name: "nonnumeric row", path, text: valid.replace("| 1 | open", "| x | open"), error: /row table malformed at line 5/ },
|
|
465
|
+
{ name: "invalid status", path, text: valid.replace("| 1 | open", "| 1 | unknown"), error: /row table malformed at line 5/ },
|
|
466
|
+
{ name: "absent pending row", path, text: "pending: 2 | home | abc1234", error: /pending: names row 2 which is absent/ },
|
|
467
|
+
{ name: "zero pending row", path, text: "pending: 0 | home | abc1234", error: /pending: names row 0 which is absent/ },
|
|
468
|
+
{ name: "leading-zero absent row", path, text: "pending: 02 | home | abc1234", error: /pending: names row 2 which is absent/ },
|
|
469
|
+
{ name: "absent mapping row", path, text: valid + "\n## Leg mapping\n| row | ac | leg |\n|---|---|---|\n| 2 | 1 | repo-a |", error: /leg mapping names row 2 which is absent/ },
|
|
470
|
+
];
|
|
471
|
+
for (const row of cases) {
|
|
472
|
+
writeFileSync(path, row.text.startsWith("pending:") ? ledger(["1 | open | 0 |"], "", row.text) : row.text);
|
|
473
|
+
const result = await h.call({ action: "refresh", path: row.path });
|
|
474
|
+
assert.match(result.details.error!, row.error, row.name);
|
|
475
|
+
assert.equal(result.details.tasks, undefined, row.name);
|
|
476
|
+
assert.equal(h.widget(), widget, row.name);
|
|
477
|
+
assert.deepEqual((await h.call({ action: "status" })).details.tasks, initial.details.tasks, row.name);
|
|
478
|
+
if (row.path) assert.ok(result.content[0].text.includes(row.path), row.name);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
383
482
|
test("registration: sequential, tasks schema is Array<Union<String, Object>>, description carries the contract", () => {
|
|
384
483
|
const { executionMode, parameters, description } = harness();
|
|
385
484
|
assert.equal(executionMode(), "sequential");
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Shows a persistent TUI widget above the editor.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
9
11
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
10
12
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
11
13
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -15,10 +17,11 @@ const TASK_STATUSES = ["pending", "in_progress", "complete", "failed", "skipped"
|
|
|
15
17
|
type TaskStatus = (typeof TASK_STATUSES)[number];
|
|
16
18
|
|
|
17
19
|
interface Task { name: string; status: TaskStatus; }
|
|
18
|
-
interface PlanTrackerDetails { action: "init" | "add" | "update" | "status" | "clear"; tasks: Task[]; error?: string; }
|
|
20
|
+
interface PlanTrackerDetails { action: "init" | "add" | "update" | "status" | "clear" | "refresh"; tasks: Task[]; error?: string; }
|
|
19
21
|
|
|
20
22
|
const PlanTrackerParams = Type.Object({
|
|
21
|
-
action: StringEnum(["init", "add", "update", "status", "clear"] as const, { description: "Action to perform" }),
|
|
23
|
+
action: StringEnum(["init", "add", "update", "status", "clear", "refresh"] as const, { description: "Action to perform" }),
|
|
24
|
+
path: Type.Optional(Type.String({ description: "Artifact path for refresh; relative paths resolve against the session cwd" })),
|
|
22
25
|
tasks: Type.Optional(Type.Array(Type.Union([
|
|
23
26
|
Type.String(),
|
|
24
27
|
Type.Object({ name: Type.String(), status: StringEnum(TASK_STATUSES, { description: "Status to recreate the task with (init only)" }) }),
|
|
@@ -40,6 +43,62 @@ export function validateSnapshot(tasks: Task[]): number[] {
|
|
|
40
43
|
return offenders;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
function ledgerTasks(text: string, path: string): Task[] {
|
|
47
|
+
const lines = text.split("\n");
|
|
48
|
+
if (lines[0] !== "# Ledger - loop engine") {
|
|
49
|
+
throw new Error(`refresh: ${path} line 1 is ${JSON.stringify(lines[0])}, not a loop ledger`);
|
|
50
|
+
}
|
|
51
|
+
const malformed = (index: number): never => { throw new Error(`refresh: ${path} row table malformed at line ${index + 1}`); };
|
|
52
|
+
const header = lines.findIndex(line => /^\|\s*row\s*\|\s*status\s*\|\s*attempts\s*\|\s*evidence\s*\|$/.test(line));
|
|
53
|
+
if (header < 0) malformed(0);
|
|
54
|
+
if (!/^\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|$/.test(lines[header + 1] ?? "")) malformed(header + 1);
|
|
55
|
+
const rows: { row: number; status: string }[] = [];
|
|
56
|
+
for (let i = header + 2; i < lines.length && lines[i].trim() && !/^#{1,2}\s/.test(lines[i]); i++) {
|
|
57
|
+
const cells = lines[i].split(/(?<!\\)\|/).slice(1, -1).map(cell => cell.trim());
|
|
58
|
+
if (!lines[i].startsWith("|") || !lines[i].endsWith("|") || cells.length !== 4 || !/^\d+$/.test(cells[0]) || !/^(open|done|failed|cut)$/.test(cells[1])) malformed(i);
|
|
59
|
+
rows.push({ row: Number(cells[0]), status: cells[1] });
|
|
60
|
+
}
|
|
61
|
+
if (!rows.length) malformed(header + 2);
|
|
62
|
+
const byRow = new Map(rows.map(row => [row.row, row]));
|
|
63
|
+
const pendingMatch = lines.map(line => /^pending: (seed|\d+) \| (\S+) \| ([a-f0-9]{7,40})$/.exec(line)).find(Boolean);
|
|
64
|
+
const pending = pendingMatch && pendingMatch[1] !== "seed" ? Number(pendingMatch[1]) : undefined;
|
|
65
|
+
if (pending !== undefined && !byRow.has(pending)) {
|
|
66
|
+
throw new Error(`refresh: ${path} pending: names row ${pending} which is absent`);
|
|
67
|
+
}
|
|
68
|
+
const dispatched = new Set<number>();
|
|
69
|
+
let mapping: { row: number; repo: string }[] | undefined;
|
|
70
|
+
let section = "";
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
if (/^#{1,2}\s/.test(line)) {
|
|
73
|
+
section = line;
|
|
74
|
+
if (section === "## Leg mapping") mapping = [];
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (section === "## Decisions") {
|
|
78
|
+
const decision = /^row (\d+) done \| .+ \| .+$/.exec(line);
|
|
79
|
+
if (decision) dispatched.add(Number(decision[1]));
|
|
80
|
+
} else if (section === "## Leg mapping") {
|
|
81
|
+
const entry = /^\|\s*(\d+)\s*\|\s*\d+\s*\|\s*(\S+)\s*\|$/.exec(line);
|
|
82
|
+
if (!entry) continue;
|
|
83
|
+
const row = Number(entry[1]);
|
|
84
|
+
if (!byRow.has(row)) throw new Error(`refresh: ${path} leg mapping names row ${row} which is absent`);
|
|
85
|
+
mapping!.push({ row, repo: entry[2] });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const status = (row: number): TaskStatus => {
|
|
89
|
+
const value = byRow.get(row)!.status;
|
|
90
|
+
if (value === "cut") return "skipped";
|
|
91
|
+
if (value === "done") return dispatched.has(row) ? "complete" : "skipped";
|
|
92
|
+
if (row === pending) return "in_progress";
|
|
93
|
+
return value === "failed" ? "failed" : "pending";
|
|
94
|
+
};
|
|
95
|
+
const entries = mapping ?? rows.map(({ row }) => ({ row, repo: undefined }));
|
|
96
|
+
return entries.sort((a, b) => a.row - b.row).map(({ row, repo }) => ({
|
|
97
|
+
name: `row ${row}${repo === undefined ? "" : ` (${repo})`}`,
|
|
98
|
+
status: status(row),
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
43
102
|
const RULE_LINE = "Rule: pending tasks must trail every started or finished task.";
|
|
44
103
|
const snapshotLine = (tasks: Task[]): string => tasks.map((t, i) => `${i} ${t.name}=${t.status}`).join(", ");
|
|
45
104
|
const offenderList = (tasks: Task[], indices: number[]): string => indices.map((i) => `${i} "${tasks[i].name}"`).join(", ");
|
|
@@ -115,12 +174,47 @@ export default function (pi: ExtensionAPI) {
|
|
|
115
174
|
name: "plan_tracker",
|
|
116
175
|
label: "Plan Tracker",
|
|
117
176
|
description:
|
|
118
|
-
"Track progress while EXECUTING an implementation plan (the implement phase), a verify-phase conformance fix wave, or another bounded gate checklist (e.g. pre-merge PR verification). Statuses: pending (not yet touched), in_progress (started; several at once is fine), complete, failed (terminal-negative: ran and did not pass; never counted done), skipped (terminal: not applicable in this run; counted done). Rule: pending tasks must trail every started or finished task - an update that would leave a pending task ahead of a non-pending one is rejected with the fix. update never sets pending; init and add do. Actions: init (set task list; elements are task-name strings or { name, status } to recreate a list with known statuses), add (append tasks as pending; existing statuses preserved), 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.",
|
|
177
|
+
"Track progress while EXECUTING an implementation plan (the implement phase), a verify-phase conformance fix wave, or another bounded gate checklist (e.g. pre-merge PR verification). Statuses: pending (not yet touched), in_progress (started; several at once is fine), complete, failed (terminal-negative: ran and did not pass; never counted done), skipped (terminal: not applicable in this run; counted done). Rule: pending tasks must trail every started or finished task - an update that would leave a pending task ahead of a non-pending one is rejected with the fix. update never sets pending; init and add do. Actions: init (set task list; elements are task-name strings or { name, status } to recreate a list with known statuses), add (append tasks as pending; existing statuses preserved), update (change task status), status (show current state), clear (remove plan), refresh (derive tasks from an artifact path while retaining other tasks). 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.",
|
|
119
178
|
parameters: PlanTrackerParams,
|
|
120
179
|
executionMode: "sequential",
|
|
121
180
|
|
|
122
181
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
123
182
|
switch (params.action) {
|
|
183
|
+
case "refresh": {
|
|
184
|
+
let proposed: Task[];
|
|
185
|
+
try {
|
|
186
|
+
if (!params.path) throw new Error("refresh: cannot read <missing>: path required");
|
|
187
|
+
const path = resolve(ctx.cwd, params.path);
|
|
188
|
+
let text: string;
|
|
189
|
+
try {
|
|
190
|
+
text = readFileSync(path, "utf8");
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new Error(`refresh: cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
193
|
+
}
|
|
194
|
+
const projected = ledgerTasks(text, path);
|
|
195
|
+
const kept = tasks.filter(task => !/^row \d+(?: \(\S+\))?$/.test(task.name));
|
|
196
|
+
proposed = [
|
|
197
|
+
...projected.filter(task => task.status !== "pending"),
|
|
198
|
+
...kept.filter(task => task.status !== "pending"),
|
|
199
|
+
...projected.filter(task => task.status === "pending"),
|
|
200
|
+
...kept.filter(task => task.status === "pending"),
|
|
201
|
+
];
|
|
202
|
+
if (validateSnapshot(proposed).length) throw new Error(`refresh: ${path}: ${RULE_LINE}`);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
207
|
+
details: { action: "refresh", error: message } as PlanTrackerDetails,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
tasks = proposed;
|
|
211
|
+
updateWidget(ctx);
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: `Plan refreshed with ${tasks.length} tasks.\n${formatStatus(tasks)}` }],
|
|
214
|
+
details: { action: "refresh", tasks: tasks.map(t => ({ ...t })) } as PlanTrackerDetails,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
124
218
|
case "init": {
|
|
125
219
|
if (!params.tasks || params.tasks.length === 0) {
|
|
126
220
|
return {
|
|
@@ -316,6 +410,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
316
410
|
|
|
317
411
|
const taskList = details.tasks;
|
|
318
412
|
switch (details.action) {
|
|
413
|
+
case "refresh":
|
|
414
|
+
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", `Plan refreshed with ${taskList.length} tasks`), 0, 0);
|
|
319
415
|
case "init":
|
|
320
416
|
return new Text(
|
|
321
417
|
theme.fg("success", "✓ ") + theme.fg("muted", `Plan initialized with ${taskList.length} tasks`),
|
|
@@ -284,6 +284,27 @@
|
|
|
284
284
|
assert.deepEqual([resumed.gauntletEntered, resumed.lastSpecWrite], [true, "/repo/doc/specs/a.md"]);
|
|
285
285
|
});
|
|
286
286
|
|
|
287
|
+
test("session resume restores the last successful plan snapshot for reopen counting", async () => {
|
|
288
|
+
const first = await boundInPlan();
|
|
289
|
+
const closed = [{ name: "a", status: "complete" }];
|
|
290
|
+
const planResult = (tasks: { name: string; status: string }[], error?: string) => ({
|
|
291
|
+
type: "message", message: { role: "toolResult", toolName: "plan_tracker", details: { action: "update", tasks, error } },
|
|
292
|
+
});
|
|
293
|
+
const branch = [
|
|
294
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
295
|
+
{ type: "message", message: { role: "toolResult", toolName: "plan_check", details: { status: "pass", specPath: join(first.root, "doc/specs/a.md") } } },
|
|
296
|
+
planResult([{ name: "a", status: "pending" }]),
|
|
297
|
+
planResult(closed),
|
|
298
|
+
planResult([{ name: "a", status: "failed" }], "failed"),
|
|
299
|
+
];
|
|
300
|
+
const second = harness({ sessionId: "s2", branch });
|
|
301
|
+
second.ctx.cwd = first.root;
|
|
302
|
+
await second.emit("session_start", { type: "session_start", reason: "resume" });
|
|
303
|
+
await second.emit("tool_result", { toolName: "plan_tracker", toolCallId: "pt", input: { action: "update" }, content: [], isError: false, details: { action: "update", tasks: [{ name: "a", status: "in_progress" }] } });
|
|
304
|
+
assert.equal(second.readRecord().derived.gates.task_reopens, 1);
|
|
305
|
+
assert.deepEqual(replayBranch(branch, ["doc/specs"]).lastPlanTasks, closed);
|
|
306
|
+
});
|
|
307
|
+
|
|
287
308
|
test("replayBranch honors a custom spec dir list", () => {
|
|
288
309
|
const branch = [
|
|
289
310
|
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
@@ -1340,3 +1361,85 @@ test("resume skip reads engine from bound spec live and on branch replay", async
|
|
|
1340
1361
|
}
|
|
1341
1362
|
}
|
|
1342
1363
|
});
|
|
1364
|
+
|
|
1365
|
+
test("any successful tracker action counts named transitions once and ignores retries and amendments", async () => {
|
|
1366
|
+
const h = await boundInPlan();
|
|
1367
|
+
const pt = (action: string, tasks: { name: string; status: string }[], isError = false) => h.emit("tool_result", { toolName: "plan_tracker", input: { action }, details: { action, tasks }, content: [], isError });
|
|
1368
|
+
const closed = [{ name: "a", status: "complete" }, { name: "b", status: "complete" }];
|
|
1369
|
+
const reopened = [{ name: "b", status: "failed" }, { name: "a", status: "in_progress" }];
|
|
1370
|
+
await pt("init", closed);
|
|
1371
|
+
await pt("refresh", reopened, true);
|
|
1372
|
+
assert.equal(h.readRecord().derived.gates.task_reopens, 0);
|
|
1373
|
+
await pt("refresh", reopened);
|
|
1374
|
+
await pt("refresh", reopened);
|
|
1375
|
+
await pt("status", [{ name: "a", status: "in_progress" }, { name: "b", status: "in_progress" }]);
|
|
1376
|
+
assert.equal(h.readRecord().derived.gates.task_reopens, 2);
|
|
1377
|
+
assert.equal(h.readRecord().events.filter((e) => e.kind === "gate" && e.gate === "task_reopen").length, 1);
|
|
1378
|
+
await pt("add", closed);
|
|
1379
|
+
await pt("refresh", [{ name: "a", status: "skipped" }, { name: "b", status: "pending" }]);
|
|
1380
|
+
await pt("clear", []);
|
|
1381
|
+
await pt("init", reopened);
|
|
1382
|
+
assert.equal(h.readRecord().derived.gates.task_reopens, 2);
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
test("verification calls count from implement start through seal, reload and flush", async () => {
|
|
1386
|
+
const h = await boundInPlan();
|
|
1387
|
+
await h.bash("before", "npm test");
|
|
1388
|
+
assert.equal(h.readRecord().derived.verification_runs, undefined);
|
|
1389
|
+
await h.phaseResult("start", P({ brainstorm: "complete", plan: "complete", implement: "in_progress" }));
|
|
1390
|
+
assert.equal(h.readRecord().derived.verification_runs, 0);
|
|
1391
|
+
await h.bash("implement", "npm test", true);
|
|
1392
|
+
assert.equal(h.readRecord().derived.verification_runs, 1);
|
|
1393
|
+
assert.equal(h.readRecord().derived.tests, undefined);
|
|
1394
|
+
await h.phaseResult("start", P({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "in_progress" }));
|
|
1395
|
+
await h.bash("verify1", "npm test");
|
|
1396
|
+
await h.bash("verify2", "cd x && npm test", true);
|
|
1397
|
+
await h.bash("unmatched", "echo verification: custom-check");
|
|
1398
|
+
assert.equal(h.readRecord().derived.verification_runs, 3);
|
|
1399
|
+
assert.deepEqual(h.readRecord().derived.tests, { command: "npm test", result: "fail" });
|
|
1400
|
+
const branch = [{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", isError: false, details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
1401
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", isError: false, details: { action: "skip", phases: { ...P({ brainstorm: "skipped", plan: "complete", implement: "complete", verify: "in_progress" }), brainstorm: { status: "skipped", reason: "resume: doc/specs/a.md" } } } } }];
|
|
1402
|
+
const resumed = harness({ branch, sessionId: "s2" });
|
|
1403
|
+
resumed.ctx.cwd = h.root;
|
|
1404
|
+
await resumed.emit("session_start", {});
|
|
1405
|
+
await resumed.bash("resumed", "npm test");
|
|
1406
|
+
assert.equal(resumed.readRecord().derived.verification_runs, 4);
|
|
1407
|
+
await resumed.phaseResult("start", P({ brainstorm: "skipped", plan: "complete", implement: "complete", verify: "complete", ship: "in_progress" }));
|
|
1408
|
+
await resumed.bash("ship", "npm test");
|
|
1409
|
+
process.env.PI_SUBAGENT_DEPTH = "1";
|
|
1410
|
+
try { await resumed.bash("child", "npm test"); } finally { process.env.PI_SUBAGENT_DEPTH = "0"; }
|
|
1411
|
+
await resumed.emit("session_shutdown", {});
|
|
1412
|
+
assert.equal(resumed.readRecord().derived.verification_runs, 5);
|
|
1413
|
+
const sealed = { ...resumed.readRecord(), status: "shipped" as const, shipped_at: "2026-09-17T18:00:00Z" };
|
|
1414
|
+
writeFileSync(resumed.recordPath(), serializeRecord(sealed));
|
|
1415
|
+
const before = readFileSync(resumed.recordPath(), "utf8");
|
|
1416
|
+
await resumed.bash("sealed", "npm test");
|
|
1417
|
+
assert.equal(readFileSync(resumed.recordPath(), "utf8"), before);
|
|
1418
|
+
const unbound = harness();
|
|
1419
|
+
await unbound.bash("unbound", "npm test");
|
|
1420
|
+
assert.equal(existsSync(unbound.recordPath()), false);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
test("sealed loop fixture keeps dispatch iterations, skipped plan rows and verification runs", async () => {
|
|
1424
|
+
const h = harness({ jjWorkspace: true, gitFail: (args) => args[0] === "rev-parse" ? { code: 1, stdout: "", stderr: "not git" } : undefined });
|
|
1425
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
1426
|
+
await h.writeSpec("doc/specs/a.md", "# Spec\n\n**Engine:** loop\n\n## Scope\nSynthetic run.\n");
|
|
1427
|
+
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
1428
|
+
await h.phaseResult("start", P({ brainstorm: "complete", plan: "skipped", implement: "in_progress" }));
|
|
1429
|
+
const pt = (tasks: { name: string; status: string }[]) => h.emit("tool_result", { toolName: "plan_tracker", input: { action: "refresh" }, details: { action: "refresh", tasks }, isError: false, content: [] });
|
|
1430
|
+
await pt([{ name: "row 1", status: "skipped" }, { name: "row 2", status: "complete" }, { name: "row 3", status: "in_progress" }]);
|
|
1431
|
+
await h.emit("tool_result", { toolName: "subagent", input: {}, details: { results: [{ agent: "loop-implementer", exitCode: 0 }] }, content: [], isError: false });
|
|
1432
|
+
await pt([{ name: "row 1", status: "skipped" }, { name: "row 2", status: "complete" }, { name: "row 3", status: "complete" }]);
|
|
1433
|
+
await h.phaseResult("start", P({ brainstorm: "complete", plan: "skipped", implement: "complete", verify: "in_progress" }));
|
|
1434
|
+
await h.bash("verify", "npm test");
|
|
1435
|
+
await h.phaseResult("start", P({ brainstorm: "complete", plan: "skipped", implement: "complete", verify: "complete", ship: "in_progress" }));
|
|
1436
|
+
await h.phaseResult("complete", P({ brainstorm: "complete", plan: "skipped", implement: "complete", verify: "complete", ship: "complete" }));
|
|
1437
|
+
const rec = h.readRecord();
|
|
1438
|
+
assert.equal(rec.status, "shipped");
|
|
1439
|
+
assert.equal(rec.derived.engine, "loop");
|
|
1440
|
+
assert.equal(rec.derived.iterations, 1);
|
|
1441
|
+
assert.equal(rec.events.filter((e) => e.kind === "dispatch" && e.agent === "loop-implementer").length, 1);
|
|
1442
|
+
assert.deepEqual(rec.derived.plan, { tasks: 3, complete: 2, failed: 0, skipped: 1 });
|
|
1443
|
+
assert.equal(rec.derived.verification_runs, 1);
|
|
1444
|
+
assert.equal(rec.derived.events_dropped, 0);
|
|
1445
|
+
});
|
package/extensions/telemetry.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { sha256 } from "./lib/plan-check.ts";
|
|
22
22
|
import { SUPERSEDED_BY_RE, isPlanPath, isSpecPath, isSupersededByBanner, matchTestStatement, parseSpecLinks, planSpecHeader, recordPathFor, repoRelativeToolPath, toPosix, truncateCommand } from "./lib/telemetry-paths.ts";
|
|
23
23
|
import { computeJjDiff } from "./lib/telemetry-diff.ts";
|
|
24
|
-
import { COUNTED_USER_PHASES, REVIEWER_AGENTS, countFindings, countStructuredFindings, countOpenGaps,
|
|
24
|
+
import { COUNTED_USER_PHASES, REVIEWER_AGENTS, countFindings, countStructuredFindings, countOpenGaps, countReopens, insertedText, planTotals, textOf, usageToTokens } from "./lib/telemetry-collect.ts";
|
|
25
25
|
import { addTokens, capEvents, compact, currentPhase, derive, diffPhases, emptyAccumulators, emptyPhases, foldAccumulators, implementAutoCompletes, liveShipEvent, newRecord, parseRecord, serializeRecord, type Accumulators, type BaseEvent, type PhaseAcc, type PhaseKey, type PhaseMap, type TelemetryEvent, type TelemetryRecord, type Tokens } from "./lib/telemetry-record.ts";
|
|
26
26
|
import { guardReason } from "./lib/telemetry-ship.ts";
|
|
27
27
|
import { buildCouncil, parseAudit, parseChairReport, resolveMembers, type Cluster, type MemberResult } from "./lib/telemetry-council.ts";
|
|
@@ -141,6 +141,7 @@
|
|
|
141
141
|
gauntletEntered: boolean;
|
|
142
142
|
planCheckSpec?: string;
|
|
143
143
|
lastSpecWrite?: string;
|
|
144
|
+
lastPlanTasks?: { name?: string; status: string }[];
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
// gauntlet-resume's reconstruction skips brainstorm with `resume: <spec path>`; the path
|
|
@@ -179,6 +180,9 @@
|
|
|
179
180
|
}
|
|
180
181
|
const resumed = d.action === "skip" ? resumeSpecOf(d.phases, specDirs) : undefined;
|
|
181
182
|
if (out.gauntletEntered && resumed) out.lastSpecWrite = resumed;
|
|
183
|
+
} else if (msg.toolName === "plan_tracker") {
|
|
184
|
+
const d = msg.details as { tasks?: { name?: string; status: string }[]; error?: string } | undefined;
|
|
185
|
+
if (d && !d.error && Array.isArray(d.tasks)) out.lastPlanTasks = d.tasks;
|
|
182
186
|
} else if (msg.toolName === "plan_check") {
|
|
183
187
|
const d = msg.details as { status?: string; specPath?: string } | undefined;
|
|
184
188
|
if (out.gauntletEntered && d?.status === "pass" && typeof d.specPath === "string") out.planCheckSpec = d.specPath;
|
|
@@ -445,6 +449,7 @@
|
|
|
445
449
|
: { kind: "phase", action: t.action, name: t.name },
|
|
446
450
|
);
|
|
447
451
|
if (t.action === "start" && t.name === "brainstorm") councilPending = undefined;
|
|
452
|
+
if (record && t.action === "start" && t.name === "implement") record.derived.verification_runs ??= 0;
|
|
448
453
|
phases = details.phases;
|
|
449
454
|
if (record && boundSpec && t.name === "brainstorm" && (t.action === "complete" || t.action === "skip")) {
|
|
450
455
|
if (e) record.approved_at ??= e.ts;
|
|
@@ -491,6 +496,7 @@
|
|
|
491
496
|
sessionId = ctx.sessionManager.getSessionId();
|
|
492
497
|
currentDir = snap.telemetry.dir;
|
|
493
498
|
const replay = replayBranch(ctx.sessionManager.getBranch(), snap.specDirs);
|
|
499
|
+
lastPlanTasks = replay.lastPlanTasks;
|
|
494
500
|
phases = replay.phases;
|
|
495
501
|
gauntletEntered = replay.gauntletEntered;
|
|
496
502
|
if (!canBind()) return;
|
|
@@ -632,7 +638,7 @@
|
|
|
632
638
|
let onOtherSpecEdit: (rel: string, event: { input: unknown }, specDirs: string[]) => void;
|
|
633
639
|
|
|
634
640
|
let amendmentOpen = false;
|
|
635
|
-
let lastPlanTasks: { status: string }[] | undefined;
|
|
641
|
+
let lastPlanTasks: { name?: string; status: string }[] | undefined;
|
|
636
642
|
const pendingTest = new Map<string, string>(); // toolCallId -> matched statement
|
|
637
643
|
|
|
638
644
|
const phaseAcc = (k: PhaseKey = phaseNow()): PhaseAcc => (live().phases[k] ??= {});
|
|
@@ -724,8 +730,9 @@
|
|
|
724
730
|
const onPlanTracker = async (details: unknown, snap: SettingsSnapshot) => {
|
|
725
731
|
const d = details as { tasks?: { name?: string; status: string }[]; error?: string; action?: string } | undefined;
|
|
726
732
|
if (!d || d.error || !Array.isArray(d.tasks)) return;
|
|
727
|
-
|
|
728
|
-
|
|
733
|
+
const reopens = countReopens(lastPlanTasks, d.tasks);
|
|
734
|
+
if (reopens) {
|
|
735
|
+
live().gates.task_reopens += reopens;
|
|
729
736
|
pushEvent({ kind: "gate", gate: "task_reopen" });
|
|
730
737
|
}
|
|
731
738
|
lastPlanTasks = d.tasks;
|
|
@@ -771,9 +778,15 @@
|
|
|
771
778
|
if (event.toolName === "bash") {
|
|
772
779
|
const command = String((event.input as { command?: unknown }).command ?? "");
|
|
773
780
|
if (phaseNow() === "brainstorm" && /\bgit\b[^\n]*\bcommit\b/.test(command)) onCouncilAudit(command);
|
|
774
|
-
if (record &&
|
|
781
|
+
if (record && !frozen) {
|
|
775
782
|
const statement = matchTestStatement(command, snap.testCommands ?? DEFAULT_TEST_COMMANDS);
|
|
776
|
-
if (statement)
|
|
783
|
+
if (statement) {
|
|
784
|
+
if (record.events.some((e) => e.kind === "phase" && e.action === "start" && e.name === "implement")) {
|
|
785
|
+
record.derived.verification_runs = (record.derived.verification_runs ?? 0) + 1;
|
|
786
|
+
flush();
|
|
787
|
+
}
|
|
788
|
+
if (phases.verify.status === "in_progress" || phases.ship.status === "in_progress") pendingTest.set(event.toolCallId, statement);
|
|
789
|
+
}
|
|
777
790
|
}
|
|
778
791
|
}
|
|
779
792
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-gauntlet",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.1.0",
|
|
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",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"test": "node scripts/ci.mjs"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"pi-cohort": ">=7.6.
|
|
65
|
+
"pi-cohort": ">=7.6.6"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"yaml": "^2.9.0"
|
|
@@ -169,7 +169,7 @@ reread rule; amend the block before using a changed contract. Legacy specs
|
|
|
169
169
|
without that section retain finish-time rereading. A one-member run has no
|
|
170
170
|
companion artifact or companion aftermath.
|
|
171
171
|
|
|
172
|
-
Aftermath, once per batch, only when at least one item applied (nothing applied -> skip to the commit below). Ledger exists (line 1 `# Ledger - loop engine` in the resolved plan dir) -> skip `plan-writer
|
|
172
|
+
Aftermath, once per batch, only when at least one item applied (nothing applied -> skip to the commit below). Ledger exists (line 1 `# Ledger - loop engine` in the resolved plan dir) -> skip `plan-writer` and `plan_check`. Reconcile the ledger against the approved spec dispositions: `deviates:`, `deferred:`, or `elsewhere:` -> `cut`, evidence = the disposition; factual or requirement-preserving amendment -> preserve rows; re-bounded `done` row -> `open`, evidence = `reopened by amendment`. Preserve every row's attempts and the ledger's iterations and cap. Apply evidenced drafted-row text corrections as amend items while preserving row identity; retain the cut/reopen rules above. Ticket text and row add/remove changes remain redraws. Commit spec + ledger together with subject `loop: cut row <n>` for a batch containing only one cut, otherwise `loop: amend` (including mixed batches), carrying the amendment audit body below (including each `granted` record and granting sentence); after the commit, use the driver's next ledger-path refresh to project the new statuses and continue without task-plan aftermath. Keep task-plan `add`/`update`/`init` out of this route. Project `done -> cut` as `complete -> skipped` and a re-bound `done -> open` as `complete -> pending`; count neither as a reopen. During verify, stop on a re-bound row with `Stopped: row <n> reopened by amendment during verify`; keep verify active and offer authorization to return that row to ledger implementation or stop. On authorization, invoke `loop-implementer` through the driver's `## Dispatch` and `## Validate the return` (tuple, refresh, admission, refresh), with no phase reset; close the row on disk, then rerun the step 1 gate before verify continues. Never close it with `plan_tracker update`. No plan yet -> commit the spec; continue. Plan exists -> read `git -C "<abs worktree path>" status --porcelain` (plain jj: `jj -R "<abs worktree path>" diff --summary`); collect the parent's in-flight workflow edits as worktree-relative paths, including the amended spec and staged in-place wave-repair deliverables. Dispatch `plan-writer` per `writing-plans` `## Author the plan` step 3 with an `Amend:` block naming the amended spec sections, an `In-flight:` list of those paths (the parent supplies the set; the child never infers it), and the rule "preserve every task title and wave position the amendment does not touch; edit only affected anchors and tasks", then run that section's steps 5-6 (the parent `plan_check` and its branches).
|
|
173
173
|
|
|
174
174
|
At that return point, classify human-facing outcomes with this table; let writing-plans' first `Findings:` repair run as written.
|
|
175
175
|
|
|
@@ -80,7 +80,7 @@ Apply the first-line grammar in [menu-layout](../brainstorming/reference/menu-la
|
|
|
80
80
|
append nothing; report a plain hotfix handoff and go to step 7. Checked before the
|
|
81
81
|
trackers: chase-bug never touches tracker state, and a stale armed flow appended here
|
|
82
82
|
would steer resume away from its hotfix route.
|
|
83
|
-
6. **Append.** `phase_tracker({ action: "status" })`, then `plan_tracker({ action: "status" })`.
|
|
83
|
+
6. **Append.** `phase_tracker({ action: "status" })`, then `plan_tracker({ action: "status" })`. Never refresh or mutate either tracker; copy its observed task rows, including ledger and conformance tasks, through the shared contract.
|
|
84
84
|
- No phase `in_progress` -> append nothing; say the brief is a plain handoff.
|
|
85
85
|
- A phase `in_progress` (verify or ship included - resume stops at verify on its own)
|
|
86
86
|
-> append the process-state section exactly as `## Producers` in
|
|
@@ -24,8 +24,7 @@ before any tracker mutation; every stop names the offending path, field, or line
|
|
|
24
24
|
- Reads: anything.
|
|
25
25
|
- Writes: `phase_tracker` / `plan_tracker` state after every check passes and every
|
|
26
26
|
human question is answered; the plan file, only through `reference/brief-contract.md`
|
|
27
|
-
`## Plan repair`; on the seed route, what `/skill:using-git-worktrees`
|
|
28
|
-
writes. Never a spec copy, rename, or marker commit.
|
|
27
|
+
`## Plan repair`; the ledger through `reference/reconstruction.md` pending recovery and the loop driver's admission/repair contract; on the seed route, what `/skill:using-git-worktrees` writes. Never a spec copy, rename, or marker commit.
|
|
29
28
|
- Never: `git worktree add` directly; worktree creation by resume itself only on
|
|
30
29
|
the seed route, only through `/skill:using-git-worktrees`; infer approval from
|
|
31
30
|
artifact presence; restore
|
|
@@ -146,11 +145,11 @@ owning skill **without** its reset-bearing entry:
|
|
|
146
145
|
| Active after restore | Continue in | Entry point |
|
|
147
146
|
|---|---|---|
|
|
148
147
|
| brainstorm | [brainstorming checklist](../brainstorming/SKILL.md#checklist) | on-disk state decides the step: draft marker on line 1 of the spec file -> step 4; spec title -> step 8. Never `/skill:brainstorming` entry (it resets both trackers) |
|
|
149
|
-
| approved spec with `**Engine:** loop`, no ledger | loop-implementation | after check 6, arm with `start brainstorm`, then `skip brainstorm resume: <spec path>` before driver admission; carry absolute worktree/spec paths and plan directory through Entry steps 1-5 per `reference/reconstruction.md` "Spec without plan": label Entry steps 1-5 explicitly in the response as Entry step 1: resolved settings; Entry step 2: verification; Entry step 3: driver's plan skip; Entry step 4: clean named-branch check and ledger creation; Entry step 5: `plan_check`, commit and implement start; no writing-plans body or task tracker |
|
|
148
|
+
| approved spec with `**Engine:** loop`, no ledger | loop-implementation | after check 6, arm with `start brainstorm`, then `skip brainstorm resume: <spec path>` before driver admission; carry absolute worktree/spec paths and plan directory through Entry steps 1-5 per `reference/reconstruction.md` "Spec without plan": label Entry steps 1-5 explicitly in the response as Entry step 1: resolved settings; Entry step 2: verification; Entry step 3: driver's plan skip; Entry step 4: clean named-branch check and ledger creation; Entry step 5: `plan_check`, commit and implement start; no writing-plans body or task tracker initialization; refresh by ledger path after implement starts |
|
|
150
149
|
| plan with task engine | writing-plans body | skip its `start plan` call (already in_progress) |
|
|
151
|
-
| implement with line-1 ledger | loop-implementation | complete `reference/reconstruction.md` "Spec with ledger" ordered restore, cited by `reference/brief-contract.md`'s call table: entry checks 1-6, brainstorm start/skip, plan start/check/skip with reason `resume: <spec> (loop engine)`, implement start, no
|
|
150
|
+
| implement with line-1 ledger | loop-implementation | complete `reference/reconstruction.md` "Spec with ledger" ordered restore, cited by `reference/brief-contract.md`'s call table: entry checks 1-6, brainstorm start/skip, plan start/check/skip with reason `resume: <spec> (loop engine)`, implement start, pending recovery, then one ledger-path refresh, no `init`; state the entire ordered sequence explicitly even when entry checks are already reported as passed; then continue at step 5 (`Before each dispatch`), applying the shared cap boundary in reconstruction step 4 before continuation and preserving ledger cap, iterations/retry data; recover home or companion returns before refresh and any unresolved dirty-tree stop; reuse the existing ledger |
|
|
152
151
|
| implement with task plan | subagent-driven-development | re-validate `<task>` first (re-run its `Tests:`), then the task loop from the first non-complete task |
|
|
153
|
-
| verify with `Ledger:` (including ship-stage ledger briefs restored at verify) | /skill:loop-implementation |
|
|
152
|
+
| verify with `Ledger:` (including ship-stage ledger briefs restored at verify) | /skill:loop-implementation | after `start verify`, restore the saved mixed task list with `init` as `reference/brief-contract.md` specifies, then pending recovery and one ledger-path refresh through the shared cap boundary in `reference/reconstruction.md` "Spec with ledger" step 4; legacy briefs without tasks go directly to refresh; render cap and observed task/ledger state; read `After the loop` and its references; continue from step 1, the ledger gate once per leg; retain whole-diff code review, happy path when triggered, fresh home conformance review and finishing; never start implement or synthesize Gn completion |
|
|
154
153
|
| verify without `Ledger:` | verification-before-completion | full conformance gate; nothing carried over |
|
|
155
154
|
|
|
156
155
|
## Red flags — STOP
|
|
@@ -36,7 +36,7 @@ Ledger: <path>
|
|
|
36
36
|
Last decision: <line>
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Use the ledger path relative to the worktree and the last physical decision line, or `none` for an empty Decisions section. Read the ledger on disk; the brief is a pointer, not a second state carrier.
|
|
39
|
+
Use the ledger path relative to the worktree and the last physical decision line, or `none` for an empty Decisions section. Read the ledger on disk; the brief is a pointer, not a second ledger state carrier. Permit task rows beside a ledger: copy `plan_tracker status` verbatim, including the dispatched row's `in_progress` status and any conformance tasks with their observed statuses. Never infer task state from the brief's prose. Reject a block mixing these fields with `Active task` or missing either loop field.
|
|
40
40
|
|
|
41
41
|
Consumer rules: process state absent -> plain handoff (the consumer starts its
|
|
42
42
|
own process from `## Intent`). Give valid ledger fields precedence over `No plan active.` and run ledger admission. Restore phase-only state for `No plan active.` only with no `Ledger` field. Load named skills as the consumer.
|
|
@@ -106,7 +106,7 @@ Keep fields in this order. Use the portable repo identifier from `../../brainsto
|
|
|
106
106
|
|
|
107
107
|
Run resume entry check 3 against the run home unchanged. Resolve the home spec through Candidates, retaining human approval/confirmation requirements; artifact presence supplies no approval. Before any tracker write, compare ordered brief membership with the home spec's `### Leg:` blocks exactly; name the missing, extra or reordered leg and both lists on mismatch. Use `../../brainstorming/reference/legs.md` for membership, companion-file and scope grammar; keep no duplicate grammar here.
|
|
108
108
|
|
|
109
|
-
For each companion, parse the home overrides' selected sibling clone URL with shape-ticket's shared parser and use `<home PRIMARY parent>/<NAME>` as its declared primary. Canonicalize that primary's git common directory. Resolve an absent location from the leg's recorded `home:` contract and shared NAME (shared branch with `/` replaced by `-`), not a newly inferred wrapper. Require the companion worktree's canonical git common directory to equal the declared primary's, its live branch to equal the shared branch, and its leg file's full `**Home:**` tuple to equal the run home's repo, shared branch and repo-relative spec path. Stop naming the offending leg and expected/observed path, branch or full tuple. A live tree's branch or tuple mismatch is not informational drift.
|
|
109
|
+
For each companion, parse the home overrides' selected sibling clone URL with shape-ticket's shared parser and use `<home PRIMARY parent>/<NAME>` as its declared primary. Canonicalize that primary's git common directory. Resolve an absent location from the leg's recorded `home:` contract and shared NAME (shared branch with `/` replaced by `-`), not a newly inferred wrapper. Render that derivation in the stop: name the recorded `home:` pattern and the shared NAME it was resolved from, and state that the pattern locates the tree without authorizing its recreation, a synthesized receipt or inferred approval. Require the companion worktree's canonical git common directory to equal the declared primary's, its live branch to equal the shared branch, and its leg file's full `**Home:**` tuple to equal the run home's repo, shared branch and repo-relative spec path. Stop naming the offending leg and expected/observed path, branch or full tuple. A live tree's branch or tuple mismatch is not informational drift.
|
|
110
110
|
|
|
111
111
|
Admit a missing companion tree only after live receipt validation under `../../finishing-a-development-branch/reference/legs.md`. A copied receipt alone proves nothing. Announce per-leg HEAD and dirty drift against live trees without stopping; identify each leg. Keep approval, standing-grant validation, arming order and task snapshots unchanged. Dispatch task `Leg:` work in that leg's checkout, not the home by default.
|
|
112
112
|
|
|
@@ -192,14 +192,14 @@ Honor SKILL.md entry check 6's deferred stop before any process-state arming cal
|
|
|
192
192
|
|---|---|
|
|
193
193
|
| brainstorm | `start brainstorm`; `substep` if the brief shows one |
|
|
194
194
|
| plan | `start brainstorm`; `skip brainstorm S`; `start plan`; `substep` if shown |
|
|
195
|
-
| implement with ledger | Read ledger line 1; resolve its path-only `**Spec:**` header; run entry checks 1-6, naming check 6 (Grant field); `start brainstorm`; `skip brainstorm` reason `resume: <spec path>`; `start plan`; `plan_check` on the absolute ledger path; on PASS `skip plan` reason `resume: <spec> (loop engine)` using the resolved absolute spec path as `<spec>`; `start implement`; no `
|
|
195
|
+
| implement with ledger | Read ledger line 1; resolve its path-only `**Spec:**` header; run entry checks 1-6, naming check 6 (Grant field); `start brainstorm`; `skip brainstorm` reason `resume: <spec path>`; `start plan`; `plan_check` on the absolute ledger path; on PASS `skip plan` reason `resume: <spec> (loop engine)` using the resolved absolute spec path as `<spec>`; `start implement`; pending recovery, then one ledger-path refresh, no `init`; apply the shared cap boundary in `reconstruction.md` "Spec with ledger" step 4 before continuation; render cap, rows and decisions; continue at loop step 5. Follow `reconstruction.md` "Spec with ledger" for this self-contained ordered restore. On FAIL stop with findings, never `## Plan repair` or writing-plans. |
|
|
196
196
|
| implement with task plan | `start brainstorm`; `skip brainstorm S`; `start plan`; `plan_check({ planPath })`; on FAIL run `## Plan repair`; on PASS `skip plan R`; `start implement`; `substep` if shown; `plan_tracker init` |
|
|
197
197
|
| verify | `start brainstorm`; `skip brainstorm S`; `start plan`; `plan_check({ planPath })`; on FAIL run `## Plan repair`; on PASS `skip plan R`, then `skip implement R`; `start verify`; `substep` if shown; `plan_tracker init`. Never call `start implement`. |
|
|
198
198
|
| ship | With a correctly stripped coordinated plan, apply "Coordinated admission"'s fully receipt-validated retry route instead of plan repair; otherwise as verify. Restoration stops at verify in_progress: `complete verify` needs a fresh conformance dispatch and `skip verify` would bypass a real gate. Announce that the closure review re-runs before ship |
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
Apply reconstruction's pending recovery before dispatch or verification on every ledger restore, including verify/ship. Use the home return contract for a home tuple and the companion protocol only for an actual multi-member companion; record a validated interrupted return before continuing.
|
|
201
201
|
|
|
202
|
-
For a ledger brief, pair by the ledger's path-only `**Spec:**` header, not basename; require the resolved spec under `flowGuards.specDirs` and the ledger under its sibling plans directory. Keep same-repository binding and human confirmation. For every ledger brief apply the shared cap boundary in `reconstruction.md` "Spec with ledger" step 4 before dispatch or verification, retaining its legacy stop and original-phase grant continuation. For verify/ship ledger briefs, use the ledger restore sequence through `skip plan`, then `skip implement R`; `start verify
|
|
202
|
+
For a ledger brief, pair by the ledger's path-only `**Spec:**` header, not basename; require the resolved spec under `flowGuards.specDirs` and the ledger under its sibling plans directory. Keep same-repository binding and human confirmation. For every ledger brief apply the shared cap boundary in `reconstruction.md` "Spec with ledger" step 4 before dispatch or verification, retaining its legacy stop and original-phase grant continuation. For verify/ship ledger briefs, use the ledger restore sequence through `skip plan`, then `skip implement R`; `start verify`; restore the full saved task list with `plan_tracker init` (names, order and mixed statuses unchanged), then apply pending recovery and one ledger-path refresh through reconstruction step 4. A legacy ledger brief without task rows goes directly to refresh after phase restoration and pending recovery. Never call `start implement` on this route or invent completed Gn tasks; refresh replaces only ledger entries and preserves every non-ledger name/status. Never send a ledger task list through task-plan repair, even on a failed ledger check. Continue verify/ship ledger briefs in `/skill:loop-implementation` `After the loop` from step 1 (the ledger gate once per leg), after the already-restored `start verify`, retaining whole-diff code review, happy path when triggered, fresh conformance review and finishing. Ship still stops at verify for fresh conformance.
|
|
203
203
|
|
|
204
204
|
`planPath` (task-plan implement onward): the brief carries none. Resolve as reconstruction does
|
|
205
205
|
(`reconstruction.md`, "Candidates", including its `flowGuards.specDirs` resolution): the
|
|
@@ -267,7 +267,7 @@ Resume never edits the spec or writes an Open Question itself; the dispatched pl
|
|
|
267
267
|
"Exact" restoration binds: the active phase identity and substep, and the plan task list
|
|
268
268
|
(names, order, statuses) verbatim. Prior phases show `⊘ (resume: ...)` regardless of the
|
|
269
269
|
brief's glyph - skip-arming is the mechanism and completing priors would fabricate gated
|
|
270
|
-
history. `No plan active.` -> no `init` call.
|
|
270
|
+
history. For a ledger, restore the saved mixed list only on verify/ship, then refresh its ledger entries from disk; on implement, refresh without `init`. `No plan active.` -> no `init` call; a ledger still refreshes.
|
|
271
271
|
|
|
272
272
|
Before `init`, validate feasibility against the tracker's own rules:
|
|
273
273
|
|