pi-gauntlet 5.18.7 → 5.20.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 +16 -0
- package/README.md +5 -2
- package/bin/gauntlet-telemetry-seal.mjs +346 -0
- package/bin/gauntlet-telemetry-seal.test.mjs +271 -0
- package/extensions/lib/telemetry-diff.test.ts +108 -0
- package/extensions/lib/telemetry-diff.ts +61 -0
- package/extensions/lib/telemetry-paths.test.ts +11 -35
- package/extensions/lib/telemetry-paths.ts +13 -42
- package/extensions/lib/telemetry-record.test.ts +0 -6
- package/extensions/lib/telemetry-ship.test.ts +1 -5
- package/extensions/lib/telemetry-ship.ts +1 -4
- package/extensions/telemetry.test.ts +243 -135
- package/extensions/telemetry.ts +148 -201
- package/package.json +2 -2
- package/skills/brainstorming/SKILL.md +1 -1
- package/skills/finishing-a-development-branch/SKILL.md +19 -16
- package/skills/gatekeep-pr/reference/assessment.md +0 -13
- package/skills/gatekeep-pr/reference/decision-menu.md +1 -2
- package/skills/gatekeep-pr/reference/post-selection-loop.md +3 -3
- package/skills/gauntlet-performance/SKILL.md +1 -1
- package/skills/gauntlet-resume/SKILL.md +46 -32
- package/skills/gauntlet-resume/reference/brief-contract.md +12 -8
- package/skills/gauntlet-resume/reference/reconstruction.md +10 -3
- package/skills/gauntlet-resume/reference/seed.md +47 -0
- package/bin/gauntlet-telemetry-salvage.mjs +0 -273
- package/bin/gauntlet-telemetry-salvage.test.mjs +0 -466
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
gitCwds.push(cwd);
|
|
41
41
|
const forced = o.gitFail?.(args, cwd);
|
|
42
42
|
if (forced) return forced;
|
|
43
|
+
if (args[0] === "rev-parse" && args.includes("--git-common-dir") && !args.includes("--show-toplevel")) {
|
|
44
|
+
const primary = /^(.*\/\.worktrees\/[^/]+)(\/|$)/.test(cwd) ? cwd.replace(/\/\.worktrees\/.*$/, "") : cwd === root || cwd.startsWith(root + "/") ? root : cwd.replace(/\/(?:doc|\.pi)(?:\/.*)?$/, "");
|
|
45
|
+
return { code: 0, stdout: `${primary}/.git\n`, stderr: "" };
|
|
46
|
+
}
|
|
43
47
|
if (args[0] === "rev-parse" && args.includes("--path-format=absolute")) {
|
|
44
48
|
const wt = /^(.*\/\.worktrees\/[^/]+)(\/|$)/.exec(cwd)?.[1];
|
|
45
49
|
const primary = cwd === root || cwd.startsWith(root + "/") ? root : cwd.replace(/\/(?:doc|\.pi)(?:\/.*)?$/, "");
|
|
@@ -79,14 +83,19 @@
|
|
|
79
83
|
};
|
|
80
84
|
const recordPath = (spec = "doc/specs/a.md") => join(ctx.cwd, ".pi/gauntlet/telemetry", spec.replace(/\.md$/, ".yaml"));
|
|
81
85
|
const readRecord = (spec?: string) => parseRecord(readFileSync(recordPath(spec), "utf8"))!;
|
|
82
|
-
const
|
|
86
|
+
const gitWrites = () => gitCalls.filter((a) => a[0] === "add" || a[0] === "commit");
|
|
87
|
+
const excludeFile = () => join(root, ".git/info/exclude");
|
|
88
|
+
const bash = async (id: string, command: string, isError = false) => {
|
|
89
|
+
await emit("tool_call", { toolName: "bash", toolCallId: id, input: { command } });
|
|
90
|
+
await emit("tool_result", { toolName: "bash", toolCallId: id, input: { command }, content: [], isError, details: undefined });
|
|
91
|
+
};
|
|
83
92
|
const phaseResult = (action: string, phases: Record<string, unknown>) => emit("tool_result", { toolName: "phase_tracker", toolCallId: "pt", input: {}, content: [], isError: false, details: { action, phases } });
|
|
84
93
|
const writeSpec = async (rel: string, body = "# Spec\n") => {
|
|
85
94
|
writeFileSync(join(root, rel), body);
|
|
86
95
|
await emit("tool_call", { toolName: "write", toolCallId: "w", input: { path: rel, content: body } });
|
|
87
96
|
return emit("tool_result", { toolName: "write", toolCallId: "w", input: { path: rel, content: body }, content: [], isError: false, details: undefined });
|
|
88
97
|
};
|
|
89
|
-
return { root, handlers, emit, gitCalls, gitCwds, jjCalls, readFileCalls,
|
|
98
|
+
return { root, handlers, emit, gitCalls, gitCwds, jjCalls, readFileCalls, gitWrites, excludeFile, bash, recordPath, readRecord, phaseResult, writeSpec, setBranch: (b: unknown[]) => (branch = b), ctx };
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
const P = (over: Record<string, string> = {}) => Object.fromEntries(["brainstorm", "plan", "implement", "verify", "ship"].map((p) => [p, { status: over[p] ?? "pending" }]));
|
|
@@ -125,7 +134,7 @@
|
|
|
125
134
|
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
126
135
|
await h.writeSpec("doc/specs/a.md");
|
|
127
136
|
assert.equal(existsSync(h.recordPath()), false);
|
|
128
|
-
assert.equal(h.
|
|
137
|
+
assert.equal(h.gitWrites().length, 0);
|
|
129
138
|
});
|
|
130
139
|
|
|
131
140
|
test("PI_SUBAGENT_DEPTH=1 no-ops every handler", async () => {
|
|
@@ -140,7 +149,7 @@
|
|
|
140
149
|
}
|
|
141
150
|
});
|
|
142
151
|
|
|
143
|
-
test("bind on first spec write flushes buffered phase event, mints run_id,
|
|
152
|
+
test("bind on first spec write flushes buffered phase event, mints run_id, writes the exclude line, never stages or commits", async () => {
|
|
144
153
|
const h = harness();
|
|
145
154
|
await h.emit("session_start", { type: "session_start", reason: "startup" });
|
|
146
155
|
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
@@ -159,19 +168,16 @@
|
|
|
159
168
|
assert.equal((rec.events[0] as any).model, "p/m");
|
|
160
169
|
assert.equal((rec.events[0] as any).thinking, "high");
|
|
161
170
|
assert.equal(rec.derived.spec_writes.brainstorm?.count, 1);
|
|
162
|
-
const afterBind = h.commits().length;
|
|
163
|
-
assert.equal(afterBind, 1, "binding flushes the buffered start checkpoint once");
|
|
164
171
|
await h.phaseResult("substep", P({ brainstorm: "in_progress" }));
|
|
165
|
-
await h.phaseResult("status", P({ brainstorm: "in_progress" }));
|
|
166
|
-
assert.equal(h.commits().length, afterBind);
|
|
167
172
|
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
168
|
-
assert.equal(h.commits().length, afterBind + 1);
|
|
169
173
|
const rec2 = h.readRecord();
|
|
170
174
|
assert.equal(rec2.approved_at, rec2.events.at(-1)!.ts);
|
|
171
|
-
|
|
172
|
-
assert.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
assert.equal(h.gitWrites().length, 0, "the recorder never runs git add or git commit");
|
|
176
|
+
assert.equal(readFileSync(h.excludeFile(), "utf8"), ".pi/gauntlet/telemetry/\n");
|
|
177
|
+
await h.phaseResult("reset", P());
|
|
178
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
179
|
+
await h.writeSpec("doc/specs/a.md", "# Spec again\n");
|
|
180
|
+
assert.equal(readFileSync(h.excludeFile(), "utf8"), ".pi/gauntlet/telemetry/\n", "exclude line is written once after rebind");
|
|
175
181
|
});
|
|
176
182
|
|
|
177
183
|
test("bind derives approved_at from a buffered brainstorm completion", async () => {
|
|
@@ -185,19 +191,34 @@
|
|
|
185
191
|
assert.equal(rec.approved_at, rec.events[1].ts);
|
|
186
192
|
});
|
|
187
193
|
|
|
194
|
+
test("armed plan_check and bound shutdown flush the record without git add or commit", async () => {
|
|
195
|
+
const h = harness();
|
|
196
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
197
|
+
assert.equal(h.gitWrites().length, 0);
|
|
198
|
+
await h.writeSpec("doc/specs/a.md");
|
|
199
|
+
assert.equal(h.gitWrites().length, 0);
|
|
200
|
+
await h.emit("tool_result", { toolName: "plan_check", toolCallId: "pc", input: {}, content: [], isError: false, details: { status: "pass", specPath: join(h.root, "doc/specs/a.md"), planPath: join(h.root, "doc/plans/a.md") } });
|
|
201
|
+
assert.equal(h.gitWrites().length, 0);
|
|
202
|
+
await h.emit("session_shutdown", { type: "session_shutdown", reason: "quit" });
|
|
203
|
+
assert.equal(h.gitWrites().length, 0);
|
|
204
|
+
const rec = h.readRecord();
|
|
205
|
+
assert.deepEqual(Object.keys(rec.accumulators), ["total"]);
|
|
206
|
+
});
|
|
207
|
+
|
|
188
208
|
test("session shutdown before binding writes nothing", async () => {
|
|
189
209
|
const h = harness();
|
|
190
210
|
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
191
211
|
await h.emit("session_shutdown", { type: "session_shutdown", reason: "quit" });
|
|
192
212
|
assert.equal(existsSync(join(h.root, ".pi")), false);
|
|
193
|
-
assert.equal(h.
|
|
213
|
+
assert.equal(h.gitWrites().length, 0);
|
|
194
214
|
});
|
|
195
215
|
|
|
196
216
|
test("replayBranch uses the last successful phase snapshot, plan pass, and paired successful spec write", () => {
|
|
197
217
|
const successfulPhases = P({ brainstorm: "complete", plan: "in_progress" });
|
|
198
218
|
const ignoredPhases = P({ brainstorm: "complete", plan: "complete" });
|
|
199
219
|
const replay = replayBranch([
|
|
200
|
-
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { phases:
|
|
220
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
221
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: successfulPhases } } },
|
|
201
222
|
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { phases: ignoredPhases, error: "failed" } } },
|
|
202
223
|
{ type: "message", message: { role: "toolResult", toolName: "plan_check", details: { status: "pass", specPath: "/repo/doc/specs/old.md" } } },
|
|
203
224
|
{ type: "message", message: { role: "toolResult", toolName: "plan_check", details: { status: "fail", specPath: "/repo/doc/specs/ignored.md" } } },
|
|
@@ -212,17 +233,50 @@
|
|
|
212
233
|
{ type: "message", message: { role: "toolResult", toolName: "write", toolCallId: "w3", isError: true } },
|
|
213
234
|
]);
|
|
214
235
|
assert.deepEqual(replay.phases, successfulPhases);
|
|
236
|
+
assert.equal(replay.gauntletEntered, true);
|
|
215
237
|
assert.equal(replay.planCheckSpec, "/repo/doc/specs/a.md");
|
|
216
238
|
assert.equal(replay.lastSpecWrite, "doc/specs/a.md");
|
|
217
239
|
});
|
|
218
240
|
|
|
241
|
+
test("replayBranch: candidates are only taken while armed; reset clears them; edit counts; skip brainstorm resume: binds", () => {
|
|
242
|
+
const write = (id: string, path: string, tool = "write") => [
|
|
243
|
+
{ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id, name: tool, arguments: { path } }] } },
|
|
244
|
+
{ type: "message", message: { role: "toolResult", toolName: tool, toolCallId: id, isError: false } },
|
|
245
|
+
];
|
|
246
|
+
const unarmed = replayBranch([
|
|
247
|
+
...write("w1", "doc/specs/a.md"),
|
|
248
|
+
{ type: "message", message: { role: "toolResult", toolName: "plan_check", details: { status: "pass", specPath: "/repo/doc/specs/a.md" } } },
|
|
249
|
+
]);
|
|
250
|
+
assert.equal(unarmed.gauntletEntered, false);
|
|
251
|
+
assert.equal(unarmed.lastSpecWrite, undefined);
|
|
252
|
+
assert.equal(unarmed.planCheckSpec, undefined);
|
|
253
|
+
const armed = replayBranch([
|
|
254
|
+
...write("w0", "doc/specs/before.md"),
|
|
255
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
256
|
+
...write("w1", "doc/specs/a.md", "edit"),
|
|
257
|
+
]);
|
|
258
|
+
assert.deepEqual([armed.gauntletEntered, armed.lastSpecWrite], [true, "doc/specs/a.md"]);
|
|
259
|
+
const reset = replayBranch([
|
|
260
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
261
|
+
...write("w1", "doc/specs/a.md"),
|
|
262
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "reset", phases: P() } } },
|
|
263
|
+
]);
|
|
264
|
+
assert.deepEqual([reset.gauntletEntered, reset.lastSpecWrite], [false, undefined]);
|
|
265
|
+
const resumed = replayBranch([
|
|
266
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
267
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "skip", phases: { ...P({ brainstorm: "skipped" }), brainstorm: { status: "skipped", reason: "resume: /repo/doc/specs/a.md" } } } } },
|
|
268
|
+
]);
|
|
269
|
+
assert.deepEqual([resumed.gauntletEntered, resumed.lastSpecWrite], [true, "/repo/doc/specs/a.md"]);
|
|
270
|
+
});
|
|
271
|
+
|
|
219
272
|
test("session_start replay binds from plan_check details with no spec write, extends sessions, preserves run_id", async () => {
|
|
220
273
|
const first = harness();
|
|
221
274
|
await first.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
222
275
|
await first.writeSpec("doc/specs/a.md");
|
|
223
276
|
const runId = first.readRecord().run_id;
|
|
224
277
|
const second = harness({ sessionId: "s2", branch: [
|
|
225
|
-
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", toolCallId: "
|
|
278
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", toolCallId: "0", isError: false, details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
279
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", toolCallId: "1", isError: false, details: { action: "skip", phases: P({ brainstorm: "skipped", plan: "in_progress" }) } } },
|
|
226
280
|
{ type: "message", message: { role: "toolResult", toolName: "plan_check", toolCallId: "2", isError: false, details: { status: "pass", planPath: join(first.root, "doc/plans/a.md"), specPath: join(first.root, "doc/specs/a.md"), planSha256: "x", specSha256: "y" } } },
|
|
227
281
|
] });
|
|
228
282
|
// point the second harness at the first root
|
|
@@ -234,7 +288,7 @@
|
|
|
234
288
|
assert.equal(rec.events.length, 1, "replay emits no duplicate events");
|
|
235
289
|
});
|
|
236
290
|
|
|
237
|
-
test("rename at spec-writing: write to a new spec while the bound one is a draft rebinds
|
|
291
|
+
test("rename at spec-writing: write to a new spec while the bound one is a draft rebinds and moves the record", async () => {
|
|
238
292
|
const h = harness();
|
|
239
293
|
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
240
294
|
await h.writeSpec("doc/specs/draft.md", "# CONTEXT DRAFT - NOT A SPEC - fully replaced at spec-writing\n");
|
|
@@ -246,8 +300,7 @@
|
|
|
246
300
|
assert.equal(rec.spec, "doc/specs/final.md");
|
|
247
301
|
assert.ok(rec.events.some((e) => e.kind === "spec_renamed"));
|
|
248
302
|
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
249
|
-
|
|
250
|
-
assert.ok(commit.some((a) => a.endsWith("draft.yaml")) && commit.some((a) => a.endsWith("final.yaml")));
|
|
303
|
+
assert.equal(h.gitWrites().length, 0);
|
|
251
304
|
});
|
|
252
305
|
|
|
253
306
|
test("rename refuses an untracked destination collision and keeps the draft binding", async () => {
|
|
@@ -298,16 +351,7 @@
|
|
|
298
351
|
assert.equal((rec.events[1] as any).action, "reset");
|
|
299
352
|
});
|
|
300
353
|
|
|
301
|
-
test("
|
|
302
|
-
let fail = true;
|
|
303
|
-
const h = harness({ gitFail: (args) => (args[0] === "commit" && fail ? { code: 1, stdout: "", stderr: "fatal: hook rejected\nmore" } : undefined) });
|
|
304
|
-
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
305
|
-
await h.writeSpec("doc/specs/a.md");
|
|
306
|
-
let rec = h.readRecord();
|
|
307
|
-
assert.deepEqual(rec.events.filter((e) => e.kind === "warning").map((e: any) => e.message), ["git commit failed: fatal: hook rejected"]);
|
|
308
|
-
fail = false;
|
|
309
|
-
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
310
|
-
assert.equal(h.commits().length, 2);
|
|
354
|
+
test("a spec outside any checkout warns once bound; nothing is written for it", async () => {
|
|
311
355
|
const outside = mkdtempSync(join(tmpdir(), "telemetry-outside-"));
|
|
312
356
|
tempDirs.push(outside);
|
|
313
357
|
const outsideSpec = join(outside, "doc/specs/outside.md");
|
|
@@ -318,7 +362,14 @@
|
|
|
318
362
|
assert.deepEqual(nogit.readRecord().events.filter((e) => e.kind === "warning").map((e: any) => e.message), [`spec ${outsideSpec} is outside a git checkout; telemetry not recorded`]);
|
|
319
363
|
});
|
|
320
364
|
|
|
321
|
-
test("
|
|
365
|
+
test("failed git-common-dir lookup warns in the record", async () => {
|
|
366
|
+
const h = harness({ gitFail: (args) => args.includes("--git-common-dir") && !args.includes("--show-toplevel") ? { code: 1, stdout: "", stderr: "unsupported flag\nmore detail" } : undefined });
|
|
367
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
368
|
+
await h.writeSpec("doc/specs/a.md");
|
|
369
|
+
assert.deepEqual(h.readRecord().events.filter((e) => e.kind === "warning").map((e: any) => e.message), ["exclude skipped: git rev-parse --git-common-dir failed: unsupported flag"]);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("jj-bound checkout: record is written with no warning, no exclude line, and no git add/commit", async () => {
|
|
322
373
|
const h = harness({
|
|
323
374
|
jjWorkspace: true,
|
|
324
375
|
gitFail: (args) =>
|
|
@@ -330,14 +381,27 @@
|
|
|
330
381
|
await h.writeSpec("doc/specs/a.md");
|
|
331
382
|
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
332
383
|
const rec = h.readRecord();
|
|
333
|
-
assert.deepEqual(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
384
|
+
assert.deepEqual(rec.events.filter((e) => e.kind === "warning"), []);
|
|
385
|
+
assert.equal(h.gitWrites().length, 0);
|
|
386
|
+
assert.equal(existsSync(h.excludeFile()), false);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("a sealed spec in another checkout leaves the current spec bound", async () => {
|
|
390
|
+
const h = harness();
|
|
391
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
392
|
+
await h.writeSpec("doc/specs/a.md");
|
|
393
|
+
const wt = join(h.root, ".worktrees/x");
|
|
394
|
+
const sealedPath = join(wt, ".pi/gauntlet/telemetry/doc/specs/b.yaml");
|
|
395
|
+
mkdirSync(join(wt, ".pi/gauntlet/telemetry/doc/specs"), { recursive: true });
|
|
396
|
+
writeFileSync(sealedPath, serializeRecord({ ...h.readRecord(), spec: "doc/specs/b.md", status: "shipped", shipped_at: "2026-09-17T18:00:00Z" }));
|
|
397
|
+
const before = readFileSync(sealedPath, "utf8");
|
|
398
|
+
await h.emit("tool_result", { toolName: "write", toolCallId: "w", input: { path: join(wt, "doc/specs/b.md"), content: "# B\n" }, content: [], isError: false, details: undefined });
|
|
399
|
+
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
400
|
+
assert.ok(h.readRecord().events.some((e) => e.kind === "phase" && e.action === "complete"));
|
|
401
|
+
assert.equal(readFileSync(sealedPath, "utf8"), before);
|
|
338
402
|
});
|
|
339
403
|
|
|
340
|
-
test("worktree spec from a primary cwd: record keyed under the worktree,
|
|
404
|
+
test("worktree spec from a primary cwd: record keyed under the worktree, exclude written to the common dir (AC 8)", async () => {
|
|
341
405
|
const h = harness();
|
|
342
406
|
await h.emit("session_start", { type: "session_start", reason: "startup" });
|
|
343
407
|
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
@@ -346,10 +410,8 @@
|
|
|
346
410
|
assert.ok(existsSync(join(wt, ".pi/gauntlet/telemetry/doc/specs/a.yaml")), "record lives under the worktree toplevel");
|
|
347
411
|
assert.equal(existsSync(join(h.root, ".pi/gauntlet")), false, "nothing written under the primary checkout");
|
|
348
412
|
assert.ok(h.gitCwds.every((c) => c === wt || c.startsWith(wt + "/")), "no git call runs in the primary");
|
|
349
|
-
|
|
350
|
-
assert.
|
|
351
|
-
assert.equal(h.gitCwds[commitIdx], wt);
|
|
352
|
-
assert.deepEqual(h.gitCalls[commitIdx].slice(-1), [".pi/gauntlet/telemetry/doc/specs/a.yaml"]);
|
|
413
|
+
assert.equal(h.gitWrites().length, 0);
|
|
414
|
+
assert.equal(readFileSync(join(h.root, ".git/info/exclude"), "utf8"), ".pi/gauntlet/telemetry/\n", "exclude lands in the primary's common dir");
|
|
353
415
|
});
|
|
354
416
|
|
|
355
417
|
test("a spec in another checkout is a fresh bind, never a git mv", async () => {
|
|
@@ -393,6 +455,7 @@
|
|
|
393
455
|
|
|
394
456
|
const h2 = harness({ sessionId: "s2" });
|
|
395
457
|
h2.ctx.cwd = h.root;
|
|
458
|
+
await h2.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
396
459
|
await h2.phaseResult("start", P({ brainstorm: "complete", plan: "in_progress" }));
|
|
397
460
|
await h2.writeSpec("doc/specs/a.md");
|
|
398
461
|
rec = h2.readRecord();
|
|
@@ -578,16 +641,35 @@
|
|
|
578
641
|
assert.equal(rec.spec, "doc/specs/a.md");
|
|
579
642
|
await h.writeSpec("doc/specs/a.md", "# A without banners\n");
|
|
580
643
|
assert.deepEqual(h.readRecord().supersedes, ["doc/specs/pred.md"]);
|
|
581
|
-
// post-ship edit warns
|
|
644
|
+
// post-ship edit warns without changing the sealed record
|
|
645
|
+
rec.status = "shipped";
|
|
582
646
|
rec.shipped_at = "2026-09-17T12:00:00Z";
|
|
583
647
|
writeFileSync(h.recordPath(), serializeRecord(rec));
|
|
584
|
-
const
|
|
648
|
+
const before = readFileSync(h.recordPath(), "utf8");
|
|
649
|
+
const h2 = harness();
|
|
585
650
|
h2.ctx.cwd = h.root;
|
|
586
|
-
await h2.phaseResult("start", P({ brainstorm: "
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
assert.
|
|
590
|
-
assert.equal(
|
|
651
|
+
await h2.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
652
|
+
const [blocked] = await h2.emit("tool_call", { toolName: "write", toolCallId: "w", input: { path: "doc/specs/a.md" } });
|
|
653
|
+
assert.deepEqual(blocked, { block: true, reason: guardReason("doc/specs/a.md", rec.shipped_at, ".pi/gauntlet/telemetry/doc/specs/a.yaml") });
|
|
654
|
+
assert.equal(readFileSync(h.recordPath(), "utf8"), before);
|
|
655
|
+
assert.equal(h.readRecord().derived.spec_edits_after_ship, 0);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("a sealed record is skipped at bind and a later spec starts a fresh run", async () => {
|
|
659
|
+
const first = harness();
|
|
660
|
+
await first.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
661
|
+
await first.writeSpec("doc/specs/old.md");
|
|
662
|
+
const sealed = { ...first.readRecord("doc/specs/old.md"), status: "shipped" as const, shipped_at: "2026-09-17T18:00:00Z" };
|
|
663
|
+
writeFileSync(first.recordPath("doc/specs/old.md"), serializeRecord(sealed));
|
|
664
|
+
const before = readFileSync(first.recordPath("doc/specs/old.md"), "utf8");
|
|
665
|
+
const h = harness({ sessionId: "s2" });
|
|
666
|
+
h.ctx.cwd = first.root;
|
|
667
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
668
|
+
await h.writeSpec("doc/specs/old.md");
|
|
669
|
+
assert.equal(readFileSync(first.recordPath("doc/specs/old.md"), "utf8"), before);
|
|
670
|
+
assert.equal(existsSync(h.excludeFile()), false);
|
|
671
|
+
await h.writeSpec("doc/specs/new.md");
|
|
672
|
+
assert.deepEqual(parseRecord(readFileSync(first.recordPath("doc/specs/new.md"), "utf8"))!.sessions, ["s2"]);
|
|
591
673
|
});
|
|
592
674
|
|
|
593
675
|
test("rehydration drops a body-only supersedes link removed by the new session's first write", async () => {
|
|
@@ -596,6 +678,7 @@
|
|
|
596
678
|
|
|
597
679
|
const second = harness({ sessionId: "s2" });
|
|
598
680
|
second.ctx.cwd = first.root;
|
|
681
|
+
await second.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
599
682
|
await second.phaseResult("start", P({ brainstorm: "complete", plan: "in_progress" }));
|
|
600
683
|
const body = "# A without banners\n";
|
|
601
684
|
writeFileSync(join(first.root, "doc/specs/a.md"), body);
|
|
@@ -626,7 +709,8 @@
|
|
|
626
709
|
await first.writeSpec("doc/specs/a.md", "# A\n\n> **Supersedes:** doc/specs/old.md\n");
|
|
627
710
|
|
|
628
711
|
const second = harness({ sessionId: "s2", branch: [
|
|
629
|
-
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", toolCallId: "1", isError: false, details: { action: "start", phases: P({ brainstorm: "
|
|
712
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", toolCallId: "1", isError: false, details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
713
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", details: { action: "start", phases: P({ brainstorm: "complete", plan: "in_progress" }) } } },
|
|
630
714
|
{ type: "message", message: { role: "toolResult", toolName: "plan_check", toolCallId: "2", isError: false, details: { status: "pass", planPath: join(first.root, "doc/plans/a.md"), specPath: join(first.root, "doc/specs/a.md"), planSha256: "x", specSha256: "y" } } },
|
|
631
715
|
] });
|
|
632
716
|
second.ctx.cwd = first.root;
|
|
@@ -654,111 +738,133 @@
|
|
|
654
738
|
assert.deepEqual(h.readRecord().derived.tests, { command: "npm test", result: "pass" });
|
|
655
739
|
});
|
|
656
740
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (args[0] === "merge-base") return { code: 0, stdout: "abc123\n", stderr: "" };
|
|
660
|
-
if (args[0] === "diff" && args.includes("--name-only")) return { code: 0, stdout: "extensions/telemetry.ts\nextensions/telemetry.test.ts\ndoc/specs/a.md\ndoc/plans/a.md\n.pi/gauntlet/telemetry/doc/specs/a.yaml\nREADME.md\n", stderr: "" };
|
|
661
|
-
if (args[0] === "diff" && args.includes("--numstat")) return { code: 0, stdout: "100\t5\textensions/telemetry.ts\n40\t0\textensions/telemetry.test.ts\n9\t9\tdoc/specs/a.md\n2\t1\tREADME.md\n", stderr: "" };
|
|
662
|
-
if (args[0] === "rev-list") return { code: 0, stdout: "9\n", stderr: "" };
|
|
663
|
-
return undefined;
|
|
664
|
-
};
|
|
665
|
-
|
|
666
|
-
async function boundInShip(gitFail?: (args: string[]) => GitResult | undefined) {
|
|
667
|
-
const h = harness({ gitFail: (args) => gitFail?.(args) ?? shipGit(args) });
|
|
741
|
+
async function boundInShip() {
|
|
742
|
+
const h = harness();
|
|
668
743
|
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
669
744
|
await h.writeSpec("doc/specs/a.md");
|
|
670
745
|
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
671
746
|
await h.phaseResult("start", P({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "complete", ship: "in_progress" }));
|
|
672
|
-
|
|
673
|
-
const commitsBefore = h.commits().length;
|
|
674
|
-
const blocked = await h.emit("tool_call", { toolName: "bash", toolCallId: id, input: { command } });
|
|
675
|
-
const commitsAtCall = h.commits().length;
|
|
676
|
-
await h.emit("tool_result", { toolName: "bash", toolCallId: id, input: { command }, content: [], isError, details: undefined });
|
|
677
|
-
return { blocked: blocked.find(Boolean), committedBeforeRun: commitsAtCall > commitsBefore };
|
|
678
|
-
};
|
|
679
|
-
return { ...h, bash };
|
|
747
|
+
return h;
|
|
680
748
|
}
|
|
681
749
|
|
|
682
|
-
test("ship
|
|
750
|
+
test("ship-phase git commands leave the record untouched: no ship event, no shipped_at, no git add/commit, not frozen", async () => {
|
|
683
751
|
const h = await boundInShip();
|
|
684
|
-
const r = await h.bash("s1", "git push -u origin HEAD");
|
|
685
|
-
assert.equal(r.committedBeforeRun, true);
|
|
686
|
-
let rec = h.readRecord();
|
|
687
|
-
assert.equal(rec.status, "shipped");
|
|
688
|
-
assert.ok(rec.shipped_at);
|
|
689
|
-
assert.deepEqual(rec.derived.modified_files, ["README.md", "extensions/telemetry.test.ts", "extensions/telemetry.ts"]);
|
|
690
|
-
assert.deepEqual(rec.derived.diff, { base: "abc123", commits: 9, buckets: { code: { files: 1, insertions: 100, deletions: 5 }, test: { files: 1, insertions: 40, deletions: 0 }, docs: { files: 1, insertions: 2, deletions: 1 } } });
|
|
691
|
-
assert.equal(rec.derived.gates.ship_option, "pr");
|
|
692
|
-
const shipEvents = rec.events.filter((e) => e.kind === "ship") as any[];
|
|
693
|
-
assert.deepEqual([shipEvents.length, shipEvents[0].option, shipEvents[0].command], [1, "pr", "git push -u origin HEAD"]);
|
|
694
752
|
const before = readFileSync(h.recordPath(), "utf8");
|
|
695
|
-
|
|
753
|
+
await h.bash("s1", "git push -u origin HEAD");
|
|
696
754
|
await h.bash("s2", "gh pr create --fill");
|
|
697
|
-
await h.
|
|
698
|
-
await h.
|
|
699
|
-
assert.equal(readFileSync(h.recordPath(), "utf8"), before
|
|
700
|
-
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
test("ship command fails: ship_failed, terminal fields cleared, diff dropped, guard inactive, new attempt allowed", async () => {
|
|
704
|
-
const h = await boundInShip();
|
|
705
|
-
await h.bash("s1", "git merge --squash gh-33", true);
|
|
706
|
-
let rec = h.readRecord();
|
|
755
|
+
await h.bash("s3", "git merge --squash gh-33");
|
|
756
|
+
await h.bash("d1", "git worktree remove .worktrees/gh-33 && git branch -D gh-33");
|
|
757
|
+
assert.equal(readFileSync(h.recordPath(), "utf8"), before);
|
|
758
|
+
const rec = h.readRecord();
|
|
707
759
|
assert.equal(rec.status, "in_progress");
|
|
708
760
|
assert.equal(rec.shipped_at, undefined);
|
|
709
|
-
assert.equal(rec.
|
|
710
|
-
assert.equal(
|
|
711
|
-
assert.deepEqual(
|
|
712
|
-
await h.
|
|
713
|
-
|
|
714
|
-
assert.equal(rec.status, "shipped");
|
|
715
|
-
assert.equal(rec.derived.gates.ship_option, "squash");
|
|
761
|
+
assert.equal(rec.events.some((e) => e.kind === "ship"), false);
|
|
762
|
+
assert.equal(h.gitWrites().length, 0);
|
|
763
|
+
assert.deepEqual(h.jjCalls, []);
|
|
764
|
+
await h.phaseResult("complete", P({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "complete", ship: "complete" }));
|
|
765
|
+
assert.equal(h.readRecord().status, "in_progress", "git keep path no longer stamps; the seal bin owns shipped_at");
|
|
716
766
|
});
|
|
717
767
|
|
|
718
|
-
test("
|
|
768
|
+
test("a sealed on-disk record freezes the recorder: shutdown does not overwrite it", async () => {
|
|
719
769
|
const h = await boundInShip();
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
const rec = h.readRecord();
|
|
723
|
-
assert.equal(rec.status, "abandoned");
|
|
724
|
-
assert.ok(rec.abandoned_at);
|
|
725
|
-
assert.equal(rec.shipped_at, undefined);
|
|
726
|
-
assert.equal((rec.events.at(-1) as any).option, "discard");
|
|
770
|
+
const sealed = { ...h.readRecord(), status: "shipped" as const, shipped_at: "2026-09-17T18:00:00Z" };
|
|
771
|
+
writeFileSync(h.recordPath(), serializeRecord(sealed));
|
|
727
772
|
const before = readFileSync(h.recordPath(), "utf8");
|
|
773
|
+
await h.emit("session_compact", { type: "session_compact", reason: "manual" });
|
|
728
774
|
await h.emit("session_shutdown", { type: "session_shutdown", reason: "quit" });
|
|
729
775
|
assert.equal(readFileSync(h.recordPath(), "utf8"), before);
|
|
730
776
|
});
|
|
731
777
|
|
|
732
|
-
test("
|
|
778
|
+
test("a frozen record cannot bind another spec through plan_check or another checkout write", async () => {
|
|
733
779
|
const h = await boundInShip();
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
await h.emit("tool_result", { toolName: "
|
|
738
|
-
|
|
739
|
-
await h.
|
|
740
|
-
assert.equal(existsSync(h.
|
|
780
|
+
writeFileSync(h.recordPath(), serializeRecord({ ...h.readRecord(), status: "shipped", shipped_at: "2026-09-17T18:00:00Z" }));
|
|
781
|
+
const before = readFileSync(h.recordPath());
|
|
782
|
+
await h.emit("session_compact", { type: "session_compact", reason: "manual" });
|
|
783
|
+
await h.emit("tool_result", { toolName: "plan_check", toolCallId: "pc", input: {}, content: [], isError: false, details: { status: "pass", specPath: join(h.root, "doc/specs/b.md"), planPath: join(h.root, "doc/plans/b.md") } });
|
|
784
|
+
await h.writeSpec("doc/specs/b.md");
|
|
785
|
+
await h.writeSpec(".worktrees/x/doc/specs/b.md");
|
|
786
|
+
assert.equal(existsSync(h.recordPath("doc/specs/b.md")), false);
|
|
787
|
+
assert.equal(existsSync(join(h.root, ".worktrees/x/.pi/gauntlet/telemetry/doc/specs/b.yaml")), false);
|
|
788
|
+
assert.deepEqual(readFileSync(h.recordPath()), before);
|
|
741
789
|
});
|
|
742
790
|
|
|
743
|
-
test("
|
|
744
|
-
const h =
|
|
745
|
-
|
|
746
|
-
await h.
|
|
791
|
+
test("marker down: spec writes, plan_check passes, and phase events create no file and buffer nothing", async () => {
|
|
792
|
+
const h = harness();
|
|
793
|
+
await h.writeSpec("doc/specs/a.md");
|
|
794
|
+
await h.emit("tool_result", { toolName: "plan_check", toolCallId: "pc", input: {}, content: [], isError: false, details: { status: "pass", specPath: join(h.root, "doc/specs/a.md"), planPath: join(h.root, "doc/plans/a.md") } });
|
|
795
|
+
await h.emit("tool_result", { toolName: "subagent", toolCallId: "sa", input: {}, content: [{ type: "text", text: "" }], isError: false, details: { results: [{ agent: "worker", exitCode: 0, model: "p/m", usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: 0.1 } }] } });
|
|
796
|
+
assert.equal(existsSync(join(h.root, ".pi")), false);
|
|
797
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
798
|
+
await h.writeSpec("doc/specs/a.md");
|
|
747
799
|
const rec = h.readRecord();
|
|
748
|
-
assert.
|
|
749
|
-
assert.
|
|
750
|
-
assert.equal(
|
|
751
|
-
await h.emit("session_compact", { type: "session_compact", reason: "manual" });
|
|
752
|
-
await h.emit("session_shutdown", { type: "session_shutdown", reason: "quit" });
|
|
753
|
-
assert.equal(h.readRecord().derived.phases.unphased?.compactions, 1, "keep does not freeze");
|
|
800
|
+
assert.deepEqual(rec.events.map((e) => e.kind), ["phase"], "nothing from before arming is charged to the run");
|
|
801
|
+
assert.deepEqual(rec.derived.personas, {});
|
|
802
|
+
assert.equal(rec.derived.gates.plan_rounds, 0);
|
|
754
803
|
});
|
|
755
804
|
|
|
756
|
-
test("
|
|
757
|
-
const
|
|
758
|
-
await
|
|
805
|
+
test("skip brainstorm with reason resume: <spec> binds to the existing spec and reloads its record", async () => {
|
|
806
|
+
const first = harness();
|
|
807
|
+
await first.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
808
|
+
await first.writeSpec("doc/specs/a.md");
|
|
809
|
+
const runId = first.readRecord().run_id;
|
|
810
|
+
const second = harness({ sessionId: "s2" });
|
|
811
|
+
second.ctx.cwd = first.root;
|
|
812
|
+
await second.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
813
|
+
await second.phaseResult("skip", { ...P({ brainstorm: "skipped" }), brainstorm: { status: "skipped", reason: `resume: ${join(first.root, "doc/specs/a.md")}` } });
|
|
814
|
+
const rec = parseRecord(readFileSync(first.recordPath(), "utf8"))!;
|
|
815
|
+
assert.equal(rec.run_id, runId);
|
|
816
|
+
assert.deepEqual(rec.sessions, ["s1", "s2"]);
|
|
817
|
+
assert.equal(rec.approved_at, rec.events.at(-1)!.ts);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("reset then a new spec binds a fresh run with an empty buffer; the old record is untouched", async () => {
|
|
821
|
+
const h = harness();
|
|
822
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
823
|
+
await h.writeSpec("doc/specs/a.md");
|
|
824
|
+
await h.phaseResult("reset", P());
|
|
825
|
+
const aBefore = readFileSync(h.recordPath("doc/specs/a.md"), "utf8");
|
|
826
|
+
await h.emit("tool_result", { toolName: "subagent", toolCallId: "sa", input: {}, content: [{ type: "text", text: "" }], isError: false, details: { results: [{ agent: "worker", exitCode: 0, model: "p/m", usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: 0.1 } }] } });
|
|
827
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
828
|
+
await h.writeSpec("doc/specs/b.md");
|
|
829
|
+
const b = h.readRecord("doc/specs/b.md");
|
|
830
|
+
assert.deepEqual(b.events.map((e) => e.kind), ["phase"]);
|
|
831
|
+
assert.deepEqual(b.derived.personas, {});
|
|
832
|
+
assert.equal(readFileSync(h.recordPath("doc/specs/a.md"), "utf8"), aBefore);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
test("session_tree flushes observations and clears abandoned plan tracker state", async () => {
|
|
836
|
+
const h = await boundInPlan({ contextTokens: 4321 });
|
|
837
|
+
await h.emit("input", { type: "input", text: "review", source: "interactive" });
|
|
838
|
+
await h.emit("message_end", { type: "message_end", message: { role: "assistant", usage: usage(10) } });
|
|
839
|
+
await h.emit("turn_end", { type: "turn_end" });
|
|
840
|
+
await h.emit("tool_result", { toolName: "plan_tracker", input: {}, details: { action: "update", tasks: [{ status: "complete" }] }, isError: false });
|
|
841
|
+
h.setBranch([]);
|
|
842
|
+
await h.emit("session_tree", { type: "session_tree" });
|
|
759
843
|
const rec = h.readRecord();
|
|
760
|
-
assert.equal(rec.derived.
|
|
761
|
-
assert.
|
|
844
|
+
assert.equal(rec.derived.phases.plan?.user_messages, 1);
|
|
845
|
+
assert.equal(rec.derived.phases.plan?.tokens?.input, 10);
|
|
846
|
+
assert.equal(rec.derived.phases.plan?.peak_context, 4321);
|
|
847
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
848
|
+
await h.writeSpec("doc/specs/b.md");
|
|
849
|
+
await h.emit("tool_result", { toolName: "plan_tracker", input: {}, details: { action: "update", tasks: [{ status: "in_progress" }] }, isError: false });
|
|
850
|
+
assert.equal(h.readRecord("doc/specs/b.md").derived.gates.task_reopens, 0);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
test("session_switch reconstructs marker and binding from the target branch", async () => {
|
|
854
|
+
const first = harness();
|
|
855
|
+
await first.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
856
|
+
await first.writeSpec("doc/specs/a.md");
|
|
857
|
+
const h = harness({ sessionId: "s2", branch: [] });
|
|
858
|
+
h.ctx.cwd = first.root;
|
|
859
|
+
await h.emit("session_start", { type: "session_start", reason: "startup" });
|
|
860
|
+
assert.equal(parseRecord(readFileSync(first.recordPath(), "utf8"))!.sessions.includes("s2"), false, "unarmed branch does not bind");
|
|
861
|
+
h.setBranch([
|
|
862
|
+
{ type: "message", message: { role: "toolResult", toolName: "phase_tracker", toolCallId: "1", isError: false, details: { action: "start", phases: P({ brainstorm: "in_progress" }) } } },
|
|
863
|
+
{ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "w1", name: "write", arguments: { path: join(first.root, "doc/specs/a.md") } }] } },
|
|
864
|
+
{ type: "message", message: { role: "toolResult", toolName: "write", toolCallId: "w1", isError: false } },
|
|
865
|
+
]);
|
|
866
|
+
await h.emit("session_switch", { type: "session_switch", reason: "switch" });
|
|
867
|
+
assert.deepEqual(parseRecord(readFileSync(first.recordPath(), "utf8"))!.sessions, ["s1", "s2"]);
|
|
762
868
|
});
|
|
763
869
|
|
|
764
870
|
async function boundInJjShip(jjFail?: (args: string[]) => GitResult | undefined) {
|
|
@@ -785,6 +891,11 @@
|
|
|
785
891
|
const rec = h.readRecord();
|
|
786
892
|
assert.equal(rec.status, "shipped");
|
|
787
893
|
assert.equal(rec.derived.gates.ship_option, "keep");
|
|
894
|
+
const beforeWrite = readFileSync(h.recordPath(), "utf8");
|
|
895
|
+
const out = await h.writeSpec("doc/specs/a.md");
|
|
896
|
+
assert.deepEqual(out.filter(Boolean), [{ content: [{ type: "text", text: `⚠️ spec shipped at ${rec.shipped_at}; write a follow-up spec that supersedes it` }] }]);
|
|
897
|
+
assert.equal(readFileSync(h.recordPath(), "utf8"), beforeWrite);
|
|
898
|
+
assert.equal(h.readRecord().derived.spec_edits_after_ship, 0);
|
|
788
899
|
assert.deepEqual(rec.derived.modified_files, ["src/a.ts", "test/a.test.ts"]);
|
|
789
900
|
assert.deepEqual(rec.derived.diff, { base: "jjbase0001", commits: 2, buckets: { code: { files: 1, insertions: 10, deletions: 2 }, test: { files: 1, insertions: 5, deletions: 0 } } });
|
|
790
901
|
assert.deepEqual(diffWarnings(h), []);
|
|
@@ -793,6 +904,10 @@
|
|
|
793
904
|
assert.ok(diffSteps.every((a) => a.includes("--color=never")));
|
|
794
905
|
assert.deepEqual(diffSteps[1].slice(0, 4), ["--color=never", "--config", "diff.git.show-path-prefix=true", "diff"]);
|
|
795
906
|
assert.ok(diffSteps[0].some((a) => a.includes("fork_point(") && a.includes("~ root() & ::")));
|
|
907
|
+
const before = readFileSync(h.recordPath(), "utf8");
|
|
908
|
+
await h.emit("session_compact", { type: "session_compact", reason: "manual" });
|
|
909
|
+
await h.emit("session_shutdown", { type: "session_shutdown", reason: "quit" });
|
|
910
|
+
assert.equal(readFileSync(h.recordPath(), "utf8"), before, "sealed on disk => frozen");
|
|
796
911
|
});
|
|
797
912
|
|
|
798
913
|
test("jj ship (AC 2): empty mainline -> jj-named warning, both derived fields absent, record written", async () => {
|
|
@@ -848,16 +963,9 @@
|
|
|
848
963
|
assert.equal(h.readRecord().derived.diff, undefined);
|
|
849
964
|
});
|
|
850
965
|
|
|
851
|
-
test("git ship never calls jj", async () => {
|
|
852
|
-
const h = await boundInShip();
|
|
853
|
-
await h.bash("s1", "git push");
|
|
854
|
-
assert.deepEqual(h.readRecord().derived.modified_files, ["README.md", "extensions/telemetry.test.ts", "extensions/telemetry.ts"]);
|
|
855
|
-
assert.deepEqual(h.jjCalls, []);
|
|
856
|
-
});
|
|
857
|
-
|
|
858
966
|
test("guard: write to a shipped spec during brainstorm is blocked before any state change; edit passes; other phases pass", async () => {
|
|
859
967
|
const h = await boundInShip();
|
|
860
|
-
|
|
968
|
+
writeFileSync(h.recordPath(), serializeRecord({ ...h.readRecord(), status: "shipped", shipped_at: "2026-09-17T18:00:00Z" }));
|
|
861
969
|
const before = readFileSync(h.recordPath(), "utf8");
|
|
862
970
|
const h2 = harness();
|
|
863
971
|
h2.ctx.cwd = h.root;
|