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.
@@ -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 commits = () => gitCalls.filter((a) => a[0] === "commit");
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, commits, recordPath, readRecord, phaseResult, writeSpec, setBranch: (b: unknown[]) => (branch = b), ctx };
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.commits().length, 0);
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, commits at checkpoints only", async () => {
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
- const commit = h.commits().at(-1)!;
172
- assert.deepEqual(commit.slice(0, 4), ["commit", "-q", "-m", "telemetry: doc/specs/a.md"]);
173
- assert.ok(commit.includes("--") && commit.at(-1)!.endsWith(".pi/gauntlet/telemetry/doc/specs/a.yaml"));
174
- assert.ok(h.gitCalls.some((a) => a[0] === "add" && a[1] === "-f"));
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.commits().length, 0);
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: successfulPhases } } },
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: "1", isError: false, details: { action: "start", phases: P({ brainstorm: "skipped", plan: "in_progress" }) } } },
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, moves the record, commits both paths", async () => {
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
- const commit = h.commits().at(-1)!;
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("commit failure becomes a warning event and is retried at the next checkpoint; not-a-git-repo skips commits", async () => {
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("jj-bound checkout: record is written, never committed, and the warning fires once", async () => {
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
- rec.events.filter((e) => e.kind === "warning").map((e: any) => e.message),
335
- ["record written, not committed: not a git checkout"],
336
- );
337
- assert.equal(h.commits().length, 0);
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, committed there (AC 8)", async () => {
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
- const commitIdx = h.gitCalls.findIndex((a) => a[0] === "commit");
350
- assert.ok(commitIdx >= 0);
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 and counts
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 h2 = harness({ sessionId: "s2" });
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: "complete", plan: "in_progress" }));
587
- const out = await h2.writeSpec("doc/specs/a.md", "# A v5\n");
588
- const patched = out.find(Boolean) as { content: { type: string; text: string }[] };
589
- assert.match(patched.content[0].text, /spec shipped at 2026-09-17T12:00:00Z; write a follow-up spec that supersedes it/);
590
- assert.equal(h2.readRecord().derived.spec_edits_after_ship, 1);
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: "complete", plan: "in_progress" }) } } },
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
- const shipGit = (args: string[]): GitResult | undefined => {
658
- if (args[0] === "rev-parse" && args.includes("--verify")) return args.includes("origin/HEAD") ? { code: 128, stdout: "", stderr: "" } : { code: 0, stdout: "main\n", stderr: "" };
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
- const bash = async (id: string, command: string, isError = false) => {
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 attempt: shipped_at + diff flushed and committed before the bash command; git push then gh pr create yields one ship event; frozen afterwards", async () => {
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
- const commitsBefore = h.commits().length;
753
+ await h.bash("s1", "git push -u origin HEAD");
696
754
  await h.bash("s2", "gh pr create --fill");
697
- await h.phaseResult("complete", P({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "complete", ship: "complete" }));
698
- await h.emit("session_shutdown", { type: "session_shutdown", reason: "quit" });
699
- assert.equal(readFileSync(h.recordPath(), "utf8"), before, "frozen: no writes after a successful ship");
700
- assert.equal(h.commits().length, commitsBefore);
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.derived.diff, undefined);
710
- assert.equal(rec.derived.gates.ship_option, undefined);
711
- assert.deepEqual(rec.events.slice(-2).map((e) => e.kind), ["ship", "ship_failed"]);
712
- await h.bash("s2", "git merge --squash gh-33");
713
- rec = h.readRecord();
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("discard: abandoned committed on the branch before the command; frozen afterwards", async () => {
768
+ test("a sealed on-disk record freezes the recorder: shutdown does not overwrite it", async () => {
719
769
  const h = await boundInShip();
720
- const r = await h.bash("d1", "git worktree remove .worktrees/gh-33 && git branch -D gh-33");
721
- assert.equal(r.committedBeforeRun, true);
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("failed discard does not recreate a removed worktree", async () => {
778
+ test("a frozen record cannot bind another spec through plan_check or another checkout write", async () => {
733
779
  const h = await boundInShip();
734
- const command = "git worktree remove .worktrees/gh-33 && git branch -D gh-33";
735
- await h.emit("tool_call", { toolName: "bash", toolCallId: "d1", input: { command } });
736
- rmSync(h.root, { recursive: true, force: true });
737
- await h.emit("tool_result", { toolName: "bash", toolCallId: "d1", input: { command }, content: [], isError: true, details: undefined });
738
- assert.equal(existsSync(h.root), false);
739
- await h.emit("session_shutdown", { type: "session_shutdown", reason: "quit" });
740
- assert.equal(existsSync(h.root), false);
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("keep (phase complete ship with no live ship event): shipped_at set and committed, not frozen", async () => {
744
- const h = await boundInShip();
745
- const commitsBefore = h.commits().length;
746
- await h.phaseResult("complete", P({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "complete", ship: "complete" }));
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.equal(rec.status, "shipped");
749
- assert.equal(rec.derived.gates.ship_option, "keep");
750
- assert.equal(h.commits().length, commitsBefore + 1);
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("no base ref: diff omitted with a warning", async () => {
757
- const h = await boundInShip((args) => (args[0] === "rev-parse" && args.includes("--verify") ? { code: 128, stdout: "", stderr: "" } : undefined));
758
- await h.bash("s1", "git push");
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.diff, undefined);
761
- assert.ok(rec.events.some((e) => e.kind === "warning" && /base/.test((e as any).message)));
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
- await h.bash("s1", "git push");
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;