pi-gauntlet 8.0.2 → 8.1.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 +9 -0
- package/README.md +7 -5
- package/agents/loop-implementer.md +33 -9
- 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/finishing-a-development-branch/SKILL.md +5 -5
- package/skills/finishing-a-development-branch/reference/legs.md +4 -4
- package/skills/finishing-a-development-branch/reference/verification-set.md +34 -0
- package/skills/gauntlet-handoff/SKILL.md +1 -1
- package/skills/gauntlet-resume/SKILL.md +4 -5
- package/skills/gauntlet-resume/reference/brief-contract.md +7 -7
- package/skills/gauntlet-resume/reference/reconstruction.md +28 -6
- package/skills/loop-implementation/SKILL.md +48 -32
- package/skills/loop-implementation/reference/coordinated-legs.md +8 -18
- package/skills/verification-before-completion/reference/conformance-check.md +9 -3
|
@@ -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.
|
|
3
|
+
"version": "8.1.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",
|
|
@@ -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
|
|
|
@@ -33,8 +33,7 @@ Then call `phase_tracker({ action: "start", phase: "ship" })`.
|
|
|
33
33
|
**Hard verification gate.** Tests/format/lint must pass before presenting any options — including Discard. The user's stated intent to throw the branch away does not change whether the diff is in a verifiable state; verifying first surfaces accidental damage to unrelated code before the branch is gone forever. The skip rule below is the only exception.
|
|
34
34
|
Read [coordinated finish](reference/legs.md) once for `## Legs` activation, recorded contracts, per-leg Steps 1/3.5, the single Step 4 choice, Step 5 receipts and home strip-and-seal, and Step 6 cleanup. Apply its coordinated mechanics for multiple members; retain the ordinary path for one member or no section.
|
|
35
35
|
Reuse the selected run spec read in Input. Detect a rework PR subject by its `## Rework record`; use Input's header-linked route for `**PR:**`, never discover by an open PR on `$FEATURE`. For the rework route only, read every top-level `PR:`/`Pre-rework head:` pair, home first, retaining each full SHA; missing pre-head -> stop naming the spec path. Set `PRE_REWORK_HEAD` to home's SHA. Read only combined capitalized `Base:`, `Keeps:`, `Changes:`, `Dropped claims:`, `Scoped tests:` fields; ignore lowercase provenance under `### PR #<N>`. For N=1, read `Threads:` directly after `Pre-rework head:` and retain the provenance subsection without changing finish output. Confirm only home with `gh pr view <N> --json number,url,headRefName,baseRefName,state` (use the record's repository): non-`OPEN` -> stop: `PR #N is <state>; the rework cannot be pushed to it.`; headRefName != `$FEATURE` -> stop naming both branches. Retain baseRefName for Step 3.
|
|
36
|
-
|
|
37
|
-
**Skip rule.** Skip the run only when this set passed on a known clean commit in the verify phase of this session and no edit or write landed outside `<telemetry.dir>` since. Check the current tree: `git -C "$WORKTREE" diff --quiet <commit the run passed on> -- . ':!<telemetry.dir>'` must exit 0, and `git -C "$WORKTREE" status --porcelain --untracked-files=all -- . ':!<telemetry.dir>'` must exit 0 with empty output (`<telemetry.dir>` defaults to `.pi/gauntlet/telemetry`). These checks cover committed, staged, unstaged, and untracked changes outside telemetry. Otherwise run it once; a missing verified commit, a failed check, or uncertainty means run.
|
|
36
|
+
Resolve the verification set and skip rule now: use `read` at the absolute path of [verification-set.md](reference/verification-set.md), resolved from this skill's location. Retain the resolved set through minors fixes, plan stripping and delivery; apply that reference's ledger recovery and log handling.
|
|
38
37
|
**Scoping caveat — pre-existing findings.** Some services carry lint findings unrelated to the diff. If verification fails on lines you didn't touch:
|
|
39
38
|
Confirm with `git -C "$WORKTREE" diff <base>...HEAD --name-only` that the offending file isn't in your diff. Surface the pre-existing finding to the user as a separate issue - do **not** auto-fix it in this completion ("surface, don't auto-fix"). Proceed only after the user acknowledges.
|
|
40
39
|
**If tests fail (within your diff):**
|
|
@@ -221,7 +220,7 @@ Rows 1-2 run the Option 1/2 blocks; row 3 runs the Keep block and row 4 the Disc
|
|
|
221
220
|
|
|
222
221
|
**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.
|
|
223
222
|
|
|
224
|
-
**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` = for PR subjects, the executable scoped union captured in Step 1, excluding `none - PR subject, scoped`; for other subjects, the plan-header verification set). For a ledger, supply
|
|
223
|
+
**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` = for PR subjects, the executable scoped union captured in Step 1, excluding `none - PR subject, scoped`; for other subjects, the plan-header verification set). For a ledger, supply the gate set as `SCOPED_TEST_COMMANDS`, retaining owning checkouts per [verification-set.md](reference/verification-set.md). For PR subjects, require successful evidence for every supplied command after the fix; failed or missing evidence stops before returning to the menu. Commit `whole-diff minors fix` and return 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.
|
|
225
224
|
|
|
226
225
|
### Step 5: Execute Choice
|
|
227
226
|
|
|
@@ -319,7 +318,7 @@ EOF
|
|
|
319
318
|
)")
|
|
320
319
|
```
|
|
321
320
|
|
|
322
|
-
When the spec's `## Acceptance criteria` has at least one `venue:`, `deferred:`, or `elsewhere:` row, append this block after `## Test Plan`, listing those rows verbatim with their disposition, so the reader knows what `/skill:check-delivery` verifies before the prod deploy and what a follow-up or another repo owns. With no such rows the body ends at `## Test Plan`, byte-identical to today. Option 3's squash commit message is unchanged.
|
|
321
|
+
For a ledger, render `Verification: scoped - <commands>; CI on this push holds the full run` as the last `## Summary` bullet, listing that PR's gate commands; keep `## Test Plan` and the rest of the body unchanged. Keep the `Telemetry: whole PR against <baseRefName>.` suffix rework-only; print no verification line on squash or discard. When the spec's `## Acceptance criteria` has at least one `venue:`, `deferred:`, or `elsewhere:` row, append this block after `## Test Plan`, listing those rows verbatim with their disposition, so the reader knows what `/skill:check-delivery` verifies before the prod deploy and what a follow-up or another repo owns. With no such rows the body ends at `## Test Plan`, byte-identical to today. Option 3's squash commit message is unchanged.
|
|
323
322
|
|
|
324
323
|
```markdown
|
|
325
324
|
## Acceptance criteria
|
|
@@ -343,12 +342,13 @@ git -C "$PRIMARY" checkout <base-branch>
|
|
|
343
342
|
git -C "$PRIMARY" pull
|
|
344
343
|
git -C "$PRIMARY" merge --squash "$FEATURE"
|
|
345
344
|
git -C "$PRIMARY" commit -m "<imperative summary> (ref <ticket-id>)"
|
|
345
|
+
# Non-ledger subjects only; for ledgers use the per-pair post-squash recipe below.
|
|
346
346
|
(cd "$PRIMARY" && <the Step 1 set>)
|
|
347
347
|
```
|
|
348
348
|
|
|
349
349
|
The plan was already removed and the telemetry record sealed on the branch, so the staged squash tree carries the spec, the telemetry record, and the implementation - never the plan.
|
|
350
350
|
|
|
351
|
-
The post-squash re-verify is not optional - `git merge --squash` can surface conflict-resolution mistakes the worktree-side run couldn't catch.
|
|
351
|
+
The post-squash re-verify is not optional - `git merge --squash` can surface conflict-resolution mistakes the worktree-side run couldn't catch. For a ledger, run the per-pair post-squash recipe in [verification-set.md](reference/verification-set.md) instead of the non-ledger subshell above.
|
|
352
352
|
|
|
353
353
|
Cleanup worktree (Step 6), then, if Step 6 removed the worktree, `git -C "$PRIMARY" branch -D "$FEATURE"`.
|
|
354
354
|
|
|
@@ -20,7 +20,7 @@ delivered: pr <url> <full delivered PR head> | squash <full squash SHA>
|
|
|
20
20
|
verification: passed | <actual verification failure>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Choose one `delivered:` alternative. Use `verification: passed` only for successful verification; preserve the error otherwise. Retain the selected spec path, recorded commands, membership and accepted task evidence before stripping the plan, and carry the receipt locations into the brief. On fresh-session retry admit a stripped plan only when every companion has a receipt; validate each receipt rather than inferring delivery from missing files.
|
|
23
|
+
Choose one `delivered:` alternative. Use `verification: passed` only for successful verification; preserve the error otherwise. Retain the selected spec path, recorded commands, membership and accepted task evidence before stripping the plan, and carry the receipt locations into the brief. On fresh-session retry admit a stripped plan only when every companion has a receipt; validate each receipt rather than inferring delivery from missing files. For a ledger subject, recover the gate pairs from the committed pre-strip ledger per [verification-set.md](verification-set.md#post-strip-recovery), not the spec's full verification contract. For non-ledger subjects, recover commands from the spec's recorded contract, not the absent plan.
|
|
24
24
|
|
|
25
25
|
Before checkout-based verification and conformance fan-out, validate receipt identity against the selected spec, repo and recorded delivery choice; reconcile it with the selected action before delivery. Malformed, mismatched or unverifiable receipts leave the leg unlanded; stop on an unavailable boundary rather than creating duplicate delivery. Apply these proofs:
|
|
26
26
|
|
|
@@ -35,11 +35,11 @@ Never use feature-head ancestry as proof of squash delivery. Resolve the actual
|
|
|
35
35
|
|
|
36
36
|
Exempt a leg with a validated receipt and a missing worktree from checkout-based verification and conformance. Run fresh checks for every unlanded leg; stop on a missing unlanded worktree. Handle failed-squash verification in the delivered primary through the receipt retry action above; replace feature-checkout checks with that primary verification even when the feature tree still exists.
|
|
37
37
|
|
|
38
|
-
Before Step 4, run every leg's recorded verification set with that leg's worktree as command cwd, except the receipt-proven exemptions above,
|
|
38
|
+
Before Step 4, run every leg's recorded verification set with that leg's worktree as command cwd, except the receipt-proven exemptions above; for a ledger subject, use the gate pairs resolved from the ledger (the pre-strip ledger on retry) per [verification-set.md](verification-set.md). Then perform conformance review with that leg's `cwd`, Rules and owned AC rows. Use original AC indices from `scope:`; shared rows belong to each named owner and unclaimed rows default home. Apply Step 1's hard gate and skip proof independently per leg. Route fixes and commits through `git -C <owning worktree>`, never home's checkout for companion code.
|
|
39
39
|
|
|
40
40
|
Retain each present member's audited head and tree, including staged, unstaged and untracked state. Immediately before delivery compare them to the current checkout; re-verify and re-review every changed leg before its delivery. Apply the same freshness check after disposition fixes and minors fixes. Aggregate closure before presenting one menu; no leg bypasses open dispositions.
|
|
41
41
|
|
|
42
|
-
Apply leg-local `piGauntlet.verifyBeforeShip` evidence to commands targeting that checkout. Read each primary's `.pi/settings.json` by file and merge the preset using the existing settings rules; do not use home's `gauntlet_setting` result or home's successful test flag as companion evidence. Use the
|
|
42
|
+
Apply leg-local `piGauntlet.verifyBeforeShip` evidence to commands targeting that checkout. Read each primary's `.pi/settings.json` by file and merge the preset using the existing settings rules; do not use home's `gauntlet_setting` result or home's successful test flag as companion evidence. Use the resolved gate pairs for ledger subjects, including pre-strip ledger recovery when a retry has no plan; use the recorded verification set for non-ledger subjects. Apply [verification-set.md](verification-set.md)'s matcher advisory without adding a full-suite run.
|
|
43
43
|
|
|
44
44
|
## Execute one choice - Steps 4 and 5
|
|
45
45
|
|
|
@@ -50,7 +50,7 @@ For a PR-subject home, compose the supported existing home action with companion
|
|
|
50
50
|
For PR, draft and squash:
|
|
51
51
|
|
|
52
52
|
1. Validate existing receipts first. Skip only receipt-proven delivery. Check freshness for every unlanded leg before its delivery.
|
|
53
|
-
2. Deliver each companion: for PR/draft, push with `git -C <worktree> push -u origin <branch>`, then run `gh pr create` in that worktree's cwd, adding `--draft` on every draft create. On retry of an existing PR use its URL and edit rather than duplicate create. For squash, use the existing Option 3 primary checkout/pull/merge/commit recipe and run that leg's recorded post-squash commands in the primary. Write its receipt even when those commands fail. No companion plan strip or telemetry seal runs.
|
|
53
|
+
2. Deliver each companion: for PR/draft, push with `git -C <worktree> push -u origin <branch>`, then run `gh pr create` in that worktree's cwd, adding `--draft` on every draft create. On retry of an existing PR use its URL and edit rather than duplicate create. For squash, use the existing Option 3 primary checkout/pull/merge/commit recipe and run that leg's recorded post-squash commands in the primary; for a ledger, run that leg's gate pairs through [verification-set.md](verification-set.md#post-squash-execution). Write its receipt even when those commands fail. No companion plan strip or telemetry seal runs.
|
|
54
54
|
3. Collect complete companion results, including validated skips. Capture the spec path and evidence before the home plan strip. Run home's strip-and-seal block with repeated `--leg <repo>=<sha|pr-url>`, one per companion. Never seal partial results. Compare collected `{ repo, sha }` or `{ repo, pr }` entries with an existing seal's `legs:` set; differences stop naming stored and collected results. Retain all receipts when the bin fails and print its output. Never write companion `.pi/gauntlet/telemetry/`.
|
|
55
55
|
4. Deliver home through the selected existing recipe after successful sealing, without running a second seal lacking the companion arguments. For PR-subject row 1 retain the whole-PR base and all body-edit/closure rules. Write home's receipt, including post-squash failures. Run cleanup only for squash, after successful post-squash verification.
|
|
56
56
|
|