pi-gauntlet 8.2.1 → 8.4.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -5
  3. package/agents/loop-implementer.md +19 -4
  4. package/bin/gauntlet-performance.mjs +477 -64
  5. package/bin/gauntlet-performance.test.mjs +27 -27
  6. package/bin/gauntlet-spec-index.mjs +290 -47
  7. package/bin/gauntlet-spec-index.test.mjs +42 -18
  8. package/bin/gauntlet-telemetry-seal.mjs +220 -77
  9. package/bin/gauntlet-telemetry-seal.test.mjs +222 -457
  10. package/extensions/lib/gauntlet-settings.test.ts +12 -14
  11. package/extensions/lib/gauntlet-settings.ts +7 -16
  12. package/extensions/lib/plan-check.test.ts +10 -0
  13. package/extensions/lib/plan-check.ts +2 -2
  14. package/extensions/lib/telemetry-branch.test.ts +102 -0
  15. package/extensions/lib/telemetry-branch.ts +145 -0
  16. package/extensions/lib/telemetry-diff.test.ts +2 -2
  17. package/extensions/lib/telemetry-diff.ts +1 -1
  18. package/extensions/lib/telemetry-leg.ts +163 -0
  19. package/extensions/lib/telemetry-record.ts +2 -0
  20. package/extensions/lib/vcs.ts +6 -23
  21. package/extensions/telemetry-leg-failures.test.ts +138 -0
  22. package/extensions/telemetry-leg-fields.test.ts +130 -0
  23. package/extensions/telemetry-leg-no-write.test.ts +90 -0
  24. package/extensions/telemetry-leg-rounds.test.ts +157 -0
  25. package/extensions/telemetry-leg-trackers.test.ts +114 -0
  26. package/extensions/telemetry-leg.test.ts +151 -0
  27. package/extensions/telemetry-settlement-contract.test.ts +67 -0
  28. package/extensions/telemetry.test.ts +44 -27
  29. package/extensions/telemetry.ts +68 -48
  30. package/extensions/test-support/git-env.mjs +13 -0
  31. package/package.json +1 -1
  32. package/skills/brainstorming/SKILL.md +3 -1
  33. package/skills/check-delivery/SKILL.md +2 -0
  34. package/skills/finishing-a-development-branch/SKILL.md +13 -13
  35. package/skills/finishing-a-development-branch/reference/verification-set.md +2 -2
  36. package/skills/gatekeep-pr/reference/batch.md +3 -1
  37. package/skills/gatekeep-pr/reference/cleanup.md +1 -1
  38. package/skills/gatekeep-pr/reference/post-selection-loop.md +7 -1
  39. package/skills/gauntlet-resume/reference/reconstruction.md +6 -6
  40. package/skills/gauntlet-resume/reference/seed.md +1 -6
  41. package/skills/loop-implementation/SKILL.md +40 -21
  42. package/skills/loop-implementation/reference/action-spend.md +10 -0
  43. package/skills/loop-implementation/reference/coordinated-legs.md +5 -5
  44. package/skills/loop-implementation/reference/interrupted-recovery.md +10 -0
  45. package/skills/loop-implementation/reference/unreachable-returns.md +13 -0
  46. package/skills/shape-ticket/SKILL.md +1 -1
  47. package/skills/shape-ticket/reference/repo-audit.md +2 -2
  48. package/skills/verification-before-completion/reference/conformance-check.md +1 -1
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Telemetry extension: records one YAML record per gauntlet run, keyed by spec path,
3
- * from pi events alone. Binds only after `phase_tracker start brainstorm` armed the
2
+ * Telemetry extension: records run and session-leg YAML from pi events alone.
3
+ * Session legs seal through telemetry-leg.ts at terminal settlement. Runs bind
4
+ * only after `phase_tracker start brainstorm` armed the
4
5
  * flow, writes the record to disk without ever staging or committing it (the seal bin
5
6
  * commits once at finish), and blocks `write` into an already-shipped spec while
6
7
  * brainstorm is in progress. Pure helpers: extensions/lib/telemetry-*.ts. The default
@@ -14,7 +15,7 @@
14
15
  import * as piRuntime from "@earendil-works/pi-coding-agent";
15
16
  import { SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent";
16
17
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
17
- import { DEFAULT_TEST_COMMANDS, planDirsFor, resolveFlowGuards, resolveTelemetry, settingsErrorWarning, type TelemetryResolved } from "./lib/gauntlet-settings.ts";
18
+ import { DEFAULT_TELEMETRY_DIR, DEFAULT_TEST_COMMANDS, planDirsFor, resolveFlowGuards, resolveTelemetry, settingsErrorWarning, type TelemetryResolved } from "./lib/gauntlet-settings.ts";
18
19
  import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
19
20
  import { checkoutOf } from "./lib/checkout.ts";
20
21
  import { CONTEXT_DRAFT_MARKER, nextGauntletEntered } from "./lib/phase-tracker-helpers.ts";
@@ -24,6 +25,8 @@
24
25
  import { COUNTED_USER_PHASES, REVIEWER_AGENTS, countFindings, countStructuredFindings, countOpenGaps, countReopens, insertedText, planTotals, textOf, usageToTokens } from "./lib/telemetry-collect.ts";
25
26
  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
27
  import { guardReason } from "./lib/telemetry-ship.ts";
28
+ import { readTelemetryBranch, telemetryRepo, type syncTelemetryBranch } from "./lib/telemetry-branch.ts";
29
+ import { registerLegTelemetry } from "./lib/telemetry-leg.ts";
27
30
  import { buildCouncil, parseAudit, parseChairReport, resolveMembers, type Cluster, type MemberResult } from "./lib/telemetry-council.ts";
28
31
 
29
32
  // ---- deps seam -------------------------------------------------------------------
@@ -51,6 +54,9 @@
51
54
  testCommands?: string[];
52
55
  }
53
56
  export interface Deps {
57
+ syncTelemetryBranch?: typeof syncTelemetryBranch;
58
+ telemetryRepo?: typeof telemetryRepo;
59
+ readTelemetryBranch?: typeof readTelemetryBranch;
54
60
  fs: FsPort;
55
61
  git: (args: string[], cwd: string) => Promise<GitResult>;
56
62
  // jj runner; production uses realJj for both checkout detection and the ship-time diff.
@@ -205,13 +211,16 @@
205
211
  let boundSpec: string | undefined; // repo-relative
206
212
  let record: TelemetryRecord | undefined;
207
213
  const predecessorLinks = new Set<string>();
208
- let recordRel: string | undefined; // repo-relative record path
214
+ let recordRel: string | undefined; // absolute record path outside the working tree
209
215
  let buffer: TelemetryEvent[] = [];
210
216
  let pending: Accumulators = emptyAccumulators();
211
217
  let frozen = false;
218
+ let recordWritten = false;
212
219
  let gauntletEntered = false;
213
220
  let settingsWarned = false;
214
221
  let sessionId = "";
222
+ const guardCache = new Map<string, Map<string, string>>();
223
+ let guardWarned = false;
215
224
  let currentDir = "";
216
225
  let councilPending: { memberResults: MemberResult[]; chair?: { model: string; dispatches: number; clusters?: Cluster[] } } | undefined;
217
226
 
@@ -235,7 +244,7 @@
235
244
  // Disarmed and unbound: activity outside the gauntlet is never charged to the next run.
236
245
  const live = (): Accumulators => record
237
246
  ? (record.accumulators[sessionId] ??= emptyAccumulators())
238
- : gauntletEntered ? pending : emptyAccumulators();
247
+ : legAcc() ?? (gauntletEntered ? pending : emptyAccumulators());
239
248
 
240
249
  const canBind = (): boolean => gauntletEntered && !frozen && phases.ship.status !== "complete";
241
250
 
@@ -252,8 +261,8 @@
252
261
  const warn = (message: string) => pushEvent({ kind: "warning", message });
253
262
 
254
263
  // ---- record store ----
255
- // The record is the file on disk; nothing here stages or commits. A record the seal bin
256
- // already stamped is frozen so a later flush cannot overwrite the seal.
264
+ // The seal either keeps a stamped file for retry or removes it after committing.
265
+ // Neither may be overwritten by a later recorder flush.
257
266
  const sealedAt = (path: string): boolean => {
258
267
  const text = deps.fs.readFile(path);
259
268
  const parsed = text ? parseRecord(text) : undefined;
@@ -266,8 +275,8 @@
266
275
  frozen = true;
267
276
  return;
268
277
  }
269
- const target = abs(recordRel);
270
- if (sealedAt(target)) {
278
+ const target = recordRel;
279
+ if ((recordWritten && !deps.fs.exists(target)) || sealedAt(target)) {
271
280
  frozen = true;
272
281
  return;
273
282
  }
@@ -281,6 +290,7 @@
281
290
  deps.fs.mkdirp(dirname(target));
282
291
  deps.fs.writeFile(target + ".tmp", serializeRecord(record));
283
292
  deps.fs.rename(target + ".tmp", target);
293
+ recordWritten = true;
284
294
  } catch (e) {
285
295
  pushEvent({ kind: "warning", message: `record write failed: ${String((e as Error).message ?? e).split("\n")[0]}` });
286
296
  }
@@ -288,27 +298,9 @@
288
298
 
289
299
  const flush = writeRecord;
290
300
 
291
- // Excluded untracked paths are invisible to `git status --porcelain`, skipped by
292
- // `git add -A`, and never block `git worktree remove`; the seal stages with `git add -f`.
293
- const ensureExcluded = async () => {
294
- const common = await deps.git(["rev-parse", "--path-format=absolute", "--git-common-dir"], toplevel!);
295
- const commonDir = common.stdout.trim();
296
- if (common.code !== 0 || !commonDir) return void warn(`exclude skipped: git rev-parse --git-common-dir failed: ${common.stderr.trim().split("\n")[0]}`);
297
- const line = `${currentDir.replace(/\/+$/, "")}/`;
298
- const file = join(commonDir, "info", "exclude");
299
- const existing = deps.fs.readFile(file) ?? "";
300
- if (existing.split("\n").some((l) => l.trim() === line)) return;
301
- try {
302
- deps.fs.mkdirp(dirname(file));
303
- deps.fs.writeFile(file, existing + (existing && !existing.endsWith("\n") ? "\n" : "") + line + "\n");
304
- } catch (e) {
305
- warn(`exclude write failed: ${String((e as Error).message ?? e).split("\n")[0]}`);
306
- }
307
- };
308
-
309
301
  const loadOrCreate = (specRel: string): TelemetryRecord => {
310
302
  const rel = recordPathFor(currentDir, specRel);
311
- const existing = deps.fs.readFile(abs(rel));
303
+ const existing = deps.fs.readFile(rel);
312
304
  const parsed = existing ? parseRecord(existing) : undefined;
313
305
  if (parsed) {
314
306
  // fold stale live blocks (sessions that died without shutdown) into total
@@ -324,13 +316,40 @@
324
316
  return newRecord({ spec: specRel, session: sessionId, now: buffer[0]?.ts ?? deps.now(), runId: randomUUID() });
325
317
  };
326
318
 
327
- const sealedOnDisk = (top: string, rel: string, dir: string): boolean => sealedAt(join(top, recordPathFor(dir, rel)));
319
+ const shippedRecords = (top: string, snap: SettingsSnapshot): Map<string, string> => {
320
+ const key = JSON.stringify([top, snap.telemetry.remote]);
321
+ let records = guardCache.get(key);
322
+ if (!records) {
323
+ records = new Map();
324
+ guardCache.set(key, records);
325
+ const repo = (deps.telemetryRepo ?? telemetryRepo)(top);
326
+ const result = (deps.readTelemetryBranch ?? readTelemetryBranch)(top, { remote: snap.telemetry.remote, prefix: `${repo}/brainstorming/` });
327
+ for (const entry of result.records) {
328
+ const parsed = parseRecord(entry.bytes);
329
+ if (parsed?.shipped_at) records.set(parsed.spec, parsed.shipped_at);
330
+ }
331
+ if (result.warning && !guardWarned) {
332
+ guardWarned = true;
333
+ pi.sendMessage({ customType: "telemetry-warning", content: `telemetry guard unavailable: ${result.warning}`, display: true });
334
+ }
335
+ }
336
+ return records;
337
+ };
328
338
 
329
339
  const bind = async (specRel: string, snap: SettingsSnapshot, specToplevel: string, via: "git" | "jj") => {
330
- if (sealedOnDisk(specToplevel, specRel, snap.telemetry.dir)) return;
340
+ if (via === "git" && shippedRecords(specToplevel, snap).has(specRel)) return;
341
+ let metadataDir = join(specToplevel, ".jj");
342
+ if (via === "git") {
343
+ const common = await deps.git(["rev-parse", "--path-format=absolute", "--git-common-dir"], specToplevel);
344
+ if (common.code !== 0 || !common.stdout.trim()) {
345
+ warn(`record bind skipped: git common dir unavailable: ${common.stderr.trim().split("\n")[0]}`);
346
+ return;
347
+ }
348
+ metadataDir = common.stdout.trim();
349
+ }
331
350
  toplevel = specToplevel;
332
351
  checkoutVia = via;
333
- currentDir = snap.telemetry.dir;
352
+ currentDir = join(metadataDir, "gauntlet", "telemetry");
334
353
  boundSpec = specRel;
335
354
  recordRel = recordPathFor(currentDir, specRel);
336
355
  predecessorLinks.clear();
@@ -363,7 +382,6 @@
363
382
  if (snap.errors.length) warn(settingsErrorWarning(snap.errors));
364
383
  if (snap.telemetry.warning) warn(snap.telemetry.warning);
365
384
  }
366
- if (via === "git") await ensureExcluded();
367
385
  refreshLinks();
368
386
  };
369
387
 
@@ -374,6 +392,7 @@
374
392
  record = undefined;
375
393
  recordRel = undefined;
376
394
  frozen = false;
395
+ recordWritten = false;
377
396
  councilPending = undefined;
378
397
  };
379
398
 
@@ -400,18 +419,13 @@
400
419
  const rebind = async (newSpec: string): Promise<boolean> => {
401
420
  const from = recordRel!;
402
421
  const to = recordPathFor(currentDir, newSpec);
403
- if (deps.fs.exists(abs(to))) {
422
+ if (deps.fs.exists(to)) {
404
423
  warn(`record move skipped: ${to} already exists`);
405
424
  return false;
406
425
  }
407
- const trackedResult = await deps.git(["ls-files", "--error-unmatch", "--", from], toplevel!);
408
- const tracked = trackedResult.code === 0 && trackedResult.stdout.trim() !== "";
409
426
  try {
410
- deps.fs.mkdirp(dirname(abs(to)));
411
- if (tracked) {
412
- const mv = await deps.git(["mv", "--", from, to], toplevel!);
413
- if (mv.code !== 0) deps.fs.rename(abs(from), abs(to));
414
- } else deps.fs.rename(abs(from), abs(to));
427
+ deps.fs.mkdirp(dirname(to));
428
+ deps.fs.rename(from, to);
415
429
  } catch (e) {
416
430
  warn(`record move failed: ${String((e as Error).message ?? e).split("\n")[0]}`);
417
431
  return false;
@@ -493,8 +507,11 @@
493
507
  lastPlanTasks = undefined;
494
508
  amendmentOpen = false;
495
509
  pendingTest.clear();
510
+ if (sessionId !== ctx.sessionManager.getSessionId()) {
511
+ guardCache.clear();
512
+ guardWarned = false;
513
+ }
496
514
  sessionId = ctx.sessionManager.getSessionId();
497
- currentDir = snap.telemetry.dir;
498
515
  const replay = replayBranch(ctx.sessionManager.getBranch(), snap.specDirs);
499
516
  lastPlanTasks = replay.lastPlanTasks;
500
517
  phases = replay.phases;
@@ -535,6 +552,10 @@
535
552
  await applyPhaseDetails(event.details as never, snap);
536
553
  return undefined;
537
554
  }
555
+ if (event.toolName === "subagent" && legAcc()) {
556
+ await onSubagentResult(event.details, event.content, event.isError);
557
+ return undefined;
558
+ }
538
559
  if (!record && !canBind()) return undefined;
539
560
  if (event.toolName === "plan_check" && !event.isError) {
540
561
  const d = event.details as { status?: string; specPath?: string; planPath?: string } | undefined;
@@ -605,7 +626,7 @@
605
626
  }
606
627
  if (loc.toplevel !== toplevel) {
607
628
  // Another checkout: a fresh run, never a rename of this record.
608
- if ((tool === "write" || tool === "edit") && isSpecPath(rel, snap.specDirs) && canBind() && !sealedOnDisk(loc.toplevel, rel, snap.telemetry.dir)) {
629
+ if ((tool === "write" || tool === "edit") && isSpecPath(rel, snap.specDirs) && canBind() && (loc.via === "jj" || !shippedRecords(loc.toplevel, snap).has(rel))) {
609
630
  unbind();
610
631
  await bind(rel, snap, loc.toplevel, loc.via);
611
632
  if (!boundSpec) return undefined;
@@ -751,7 +772,7 @@
751
772
  if (!record || !toplevel || liveShipEvent(record.events)) return;
752
773
  record.status = "shipped";
753
774
  record.shipped_at = deps.now();
754
- const out = await computeJjDiff({ jj: deps.jj ?? realJj, cwd: toplevel, spec: record.spec, dir: currentDir, buckets: snap.telemetry.buckets, planDirs: snap.planDirs });
775
+ const out = await computeJjDiff({ jj: deps.jj ?? realJj, cwd: toplevel, spec: record.spec, dir: DEFAULT_TELEMETRY_DIR, buckets: snap.telemetry.buckets, planDirs: snap.planDirs });
755
776
  if (out.warning) warn(out.warning);
756
777
  record.derived.modified_files = out.modified_files;
757
778
  record.derived.diff = out.diff;
@@ -766,11 +787,9 @@
766
787
  const rawPath = (event.input as { path?: unknown }).path;
767
788
  if (typeof rawPath === "string" && phases.brainstorm.status === "in_progress") {
768
789
  const loc = await locate(ctx, rawPath);
769
- if (loc && isSpecPath(loc.rel, snap.specDirs)) {
770
- const recRel = recordPathFor(snap.telemetry.dir, loc.rel);
771
- const existing = deps.fs.readFile(join(loc.toplevel, recRel));
772
- const parsed = existing ? parseRecord(existing) : undefined;
773
- if (parsed?.shipped_at) return { block: true, reason: guardReason(loc.rel, parsed.shipped_at, recRel) };
790
+ if (loc?.via === "git" && isSpecPath(loc.rel, snap.specDirs)) {
791
+ const shippedAt = shippedRecords(loc.toplevel, snap).get(loc.rel);
792
+ if (shippedAt) return { block: true, reason: guardReason(loc.rel, shippedAt, "gauntlet/telemetry") };
774
793
  }
775
794
  }
776
795
  return undefined;
@@ -874,4 +893,5 @@
874
893
  refreshLinks();
875
894
  };
876
895
 
896
+ const legAcc = registerLegTelemetry(pi, deps, () => !!record);
877
897
  }
@@ -0,0 +1,13 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export function isolatedGitEnv(scratch) {
5
+ const env = {
6
+ GIT_CONFIG_GLOBAL: join(scratch, "gitconfig"),
7
+ GIT_CONFIG_NOSYSTEM: "1",
8
+ GIT_AUTHOR_NAME: "test", GIT_AUTHOR_EMAIL: "test@example.test",
9
+ GIT_COMMITTER_NAME: "test", GIT_COMMITTER_EMAIL: "test@example.test",
10
+ };
11
+ writeFileSync(env.GIT_CONFIG_GLOBAL, "");
12
+ return env;
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "8.2.1",
3
+ "version": "8.4.0",
4
4
  "description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
5
5
  "author": "Jacek Juraszek",
6
6
  "type": "module",
@@ -69,7 +69,7 @@ The spec is the first commit in a dedicated worktree, never a separate commit on
69
69
 
70
70
  Read [reference/legs.md](reference/legs.md) and settle membership before creation. Invoke `/skill:using-git-worktrees` for each member with explicit `$ROOT` = its primary and `$BRANCH` = the shared branch; carry every `Worktree ready at <full-path>` value, home first. Root the home spec at `<home worktree>/<home spec dir>/<filename>.md`; root each dispatch and `git -C` in its owning leg's worktree. Keep the process cwd unchanged. Gather each leg's rules and record portable contracts and companion artifacts using that reference; a one-member run adds only its one-block `## Legs` section.
71
71
 
72
- Spec, plan, and implementation share this worktree. The spec and its telemetry record (`<telemetry.dir>/<spec path with .md -> .yaml>`, default `.pi/gauntlet/telemetry/doc/specs/<spec>.yaml`) are deliverables and ship in the squash; only the plan is stripped. `/skill:finishing-a-development-branch` strips the plan, then seals the record before landing (a `telemetry:` commit under git; the working-copy snapshot in a plain jj workspace). Explicit trivial one-off edits outside this flow need no worktree.
72
+ Keep spec, plan, and implementation in this worktree. Ship the spec and implementation; strip only the plan. Let `/skill:finishing-a-development-branch` seal and publish the telemetry record to the orphan `gauntlet/telemetry` branch before landing; keep recorder state in Git common-dir metadata, outside the working tree. In a plain jj workspace, retain local metadata and report that publication is unavailable. Use no worktree for explicit trivial one-off edits outside this flow.
73
73
 
74
74
  ## The Process
75
75
 
@@ -155,6 +155,8 @@ Start the message with `gauntlet: question` alone at column zero on the first ph
155
155
 
156
156
  Use two rounds, targeting 300-500 words each, with one approval each; revisions remain within that approval point. Ask once per round; round-1 approval without correction confirms the predecessor. Round 1 covers architecture, responsibilities, data flow, and `supersedes <path>, <scope>` when applicable. Round 2 covers errors, edges, tests, and `## Documentation impact`. In round 2, read and apply [Documentation impact](reference/documentation-impact.md), including its verbatim template; cite that portable relative path in the spec; do not restate its categories. Ask the user when doc impact is unclear. Unreadable -> stop the step with a blocking error. Cite the draft's `Docs touched:` entries as candidates for "Materially amended existing docs"; admit or drop each by that owner's materiality bar, never list them automatically. In round 1, carry chosen `Removes:`/`Adds:` into Design as exhaustive `Remove:`/`Add:` imperatives naming every moving part; route unlisted parts through `reference/amendment-surface.md`. In round 2, write `## Tests`: one `<observable> - <test name or file>` per acceptance row and Design edge/error clause; `<observable> - none: <why>` for untestable.
157
157
 
158
+ In round 2, name every paid or external action needed for implementation or verification in Design with its runner, inputs and count for the whole flow across seed, rows, repairs and finish. Treat design approval as authorization within those bounds without per-step permission prompts; state that the loop treats an action without such a clause as unreachable, and require separate explicit authorization to add an unnamed action to Design.
159
+
158
160
  ## Ticket Handling
159
161
 
160
162
  Read [Ticket Handling](reference/ticket-acceptance.md) at first use - before gathering context, even with no ticket. Steps 4 and 8 carry the same read behind an in-context check so a restored session (`gauntlet-resume` routes there) loads it once; a same-session redraw or a second brainstorm in one session reuses the content already in context. Unreadable -> stop with a blocking error, never reconstruct from memory.
@@ -262,6 +262,8 @@ Start the message with `gauntlet: gate=check-delivery/confirmation-gate` before
262
262
 
263
263
  Start the message with `gauntlet: done delivered` when the batched write landed. Start the message with `gauntlet: done already-recorded` for idempotent delivery. Start the message with `gauntlet: done manual` when the write halves are emitted for manual execution. Start the message with `gauntlet: stuck` followed on that line by the concrete reason once the findings-comment gate is answered or no comment is offered; use this for any partial write failure too. A declined success write ends with `gauntlet: stuck write declined`.
264
264
 
265
+ After every selected write or follow-on step finishes, put one plain `<ref>: <outcome>` line below the terminal marker, including singleton and early STOP paths. Use the supplied ticket ref and exactly one outcome: `delivered`, `manual`, `declined`, `already recorded`, `skipped`, or `failed - <reason>`. Map a declined success write to `declined`, skipped-no-tracker to `skipped`, and a failed stage, findings-only path or partial write to `failed - <reason>`; report a terminal ticket without recorded delivery as `skipped`. Keep rows outside tracker payloads, without bullets, code fences or emphasis. In set mode use `reference/batch.md`'s one final roster settlement, never a per-ticket terminal marker while siblings continue.
266
+
265
267
  Any `unexplained gap` or failed stage -> **failure path**: no status
266
268
  advance, no delivery comment, no marker. The findings may be offered as a
267
269
  **findings comment** (no `Delivered:` line, so it never trips idempotency) -
@@ -226,7 +226,7 @@ Rows 1-2 run the Option 1/2 blocks; row 3 runs the Keep block and row 4 the Disc
226
226
 
227
227
  #### Strip the plan, seal the record (Options 1-3)
228
228
 
229
- Run this on the feature branch before any landing path. The spec and its telemetry record (`<telemetry.dir>/<spec path with .md -> .yaml>`, default `.pi/gauntlet/telemetry/doc/specs/<spec>.yaml`) are deliverables and ship in the squash; only the plan is stripped. The record is an untracked, git-excluded file until this step commits it (in a plain jj workspace there is no commit; jj snapshots the stamped record into the working-copy change). `<bin>` is `<directory of this skill's SKILL.md>/../../bin`, resolved from the skill's `<location>` in the system prompt.
229
+ Run this on the feature branch before any landing path. Ship the spec and implementation; strip only the plan. Let the seal publish the telemetry record to the orphan `gauntlet/telemetry` branch without changing the product checkout, index or HEAD. Keep in-progress records in Git common-dir metadata, outside the working tree. Resolve `<bin>` as `<directory of this skill's SKILL.md>/../../bin` from the skill's `<location>` in the system prompt.
230
230
 
231
231
  Read the existing artifact's path-only `**Spec:**` header (repo-relative, backticks stripped) before removal; pass that value as `--spec`, including for a line-1 ledger. Resolve the spec dirs with `gauntlet_setting({ key: "flowGuards" })` and use their sibling `plans` dirs for plan paths. Enumerate both PLAN_PATH candidates: the task plan paired by basename and `<sibling plans dir>/<spec stem>-ledger.md`. Preserve the spec and telemetry.
232
232
 
@@ -242,13 +242,13 @@ elif [ -f "$WORKTREE/$PLAN_PATH" ] && jj -R "$WORKTREE" root >/dev/null 2>&1; th
242
242
  fi
243
243
  done
244
244
 
245
- # Seal the telemetry record: stamp shipped, compute the diff, commit it once.
246
- # The bin detects a plain jj workspace (no .git) itself: jj diff, no commit step.
245
+ # Seal the telemetry record: stamp shipped, compute the diff, publish to gauntlet/telemetry.
246
+ # The bin reports publication unavailable in a plain jj workspace and keeps local metadata.
247
247
  # --option is pr for Options 1-2 and squash for Option 3; --spec is the artifact header's **Spec:** value (omit when no artifact is known).
248
248
  node <bin>/gauntlet-telemetry-seal.mjs --worktree "$WORKTREE" --option <pr|squash> --base <base-branch> --spec <spec path>
249
249
  ```
250
250
 
251
- The seal prints one line per record (`sealed <path>`, `already sealed <path>`) or one bare outcome (`no telemetry run`, `telemetry disabled`) and exits 0; print its stdout verbatim in the ship completion message. Any nonzero exit stops the flow before the first landing command, quoting the bin's line: exit 2 (`no record at <path>`, `unparseable record <path>: <reason>`) means this gauntlet run's recorder never armed or its record is corrupt; exit 1 is a usage, environment, or git/jj failure (`seal failed <path>: <reason>` restored the pre-seal bytes). The user decides; never land without the seal.
251
+ Print the seal's stdout verbatim in the ship completion message. Continue landing on exit 0 for every telemetry outcome, including `no telemetry run`, `telemetry: skipped (unparseable record <path>)` and `telemetry: not pushed (<reason>)`; missing or malformed records and publication failures never block shipping. Retain an unparseable record in place and preserve unpublished telemetry for retry. In a plain jj workspace, report `telemetry: not pushed (jj workspace: no git remote)` and retain local metadata. Stop before the first landing command only on exit 1 for a usage error (invalid arguments or base), quote the bin's line and ask the user how to proceed.
252
252
  #### PR-subject row 1: Push to the PR branch
253
253
  For the close-absorbed-PR question, start the message with `gauntlet: gate=finishing-a-development-branch/pr-subject-row-1`. Read and apply [menu-layout](../brainstorming/reference/menu-layout.md). Do not report `done pushed` while this gate awaits an answer. Capture the scoped verification set and accepted-complete task/thread evidence before the strip. Refetch threads per recorded PR using the paginated single-`$n` query (one invocation per PR) owned by [pr-head](../using-git-worktrees/reference/pr-head.md); retain PR attribution and use the query's location rendering. For home, use the table below and omit threads now resolved on GitHub. For members, copy recorded dispositions and mark newly resolved or newly opened states from the refresh; never use home's first-parent history as member-fix proof.
254
254
  | Refetched thread / evidence | Resolution |
@@ -346,7 +346,7 @@ git -C "$PRIMARY" commit -m "<imperative summary> (ref <ticket-id>)"
346
346
  (cd "$PRIMARY" && <the Step 1 set>)
347
347
  ```
348
348
 
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.
349
+ Keep only the spec and implementation in the staged squash tree; the stripped plan stays in feature history and the sealed telemetry record belongs to `gauntlet/telemetry`.
350
350
 
351
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
 
@@ -356,7 +356,7 @@ Cleanup worktree (Step 6), then, if Step 6 removed the worktree, `git -C "$PRIMA
356
356
 
357
357
  #### Option 4: Keep As-Is
358
358
 
359
- Report: "Keeping branch <name>. Worktree preserved at <path>." The telemetry record stays untracked and in_progress in the worktree; a later finishing run on this worktree seals it.
359
+ Report: "Keeping branch <name>. Worktree preserved at <path>." Leave telemetry in local metadata; Keep As-Is publishes no record.
360
360
 
361
361
  **Don't cleanup worktree.**
362
362
 
@@ -374,7 +374,7 @@ Type 'discard' to confirm.
374
374
 
375
375
  Wait for exact confirmation.
376
376
 
377
- If confirmed: Cleanup worktree (Step 6), then, if Step 6 removed the worktree, `git -C "$PRIMARY" branch -D "$FEATURE"`. The record is git-excluded, so git worktree remove needs no --force; it goes with the worktree.
377
+ If confirmed: Cleanup worktree (Step 6), then, if Step 6 removed the worktree, `git -C "$PRIMARY" branch -D "$FEATURE"`. Publish no telemetry for Discard; leave common-dir metadata outside worktree cleanup.
378
378
 
379
379
  ### Step 6: Cleanup Workspace
380
380
 
@@ -449,12 +449,12 @@ A host-owned worktree (Step 6: no `home:` match, or the legacy "Otherwise") keep
449
449
  - **Fix:** Require typed "discard" confirmation
450
450
 
451
451
  **Skipping the strip-and-seal block in Options 1-3 (any path that lands on base)**
452
- - **Problem:** Plan docs are ephemeral and shouldn't land on `<base-branch>`; the telemetry record is a deliverable and must. Skipping the block ships the plan, or drops the record the spec index reads.
453
- - **Fix:** Run the block on `$WORKTREE` before the squash or the push. The plan stays in the branch's git history (`git -C "$PRIMARY" log --all -- doc/plans/...`). Spec and telemetry record stay on `<base-branch>`; plan does not.
452
+ - **Problem:** Skipping the block ships an ephemeral plan or skips publication of the record the spec index reads.
453
+ - **Fix:** Run the block on `$WORKTREE` before the squash or push. Preserve the spec on `<base-branch>` and the telemetry record on `gauntlet/telemetry`; retain the plan only in feature history.
454
454
 
455
- **Widening the plan strip to the telemetry record**
456
- - **Problem:** `.pi/gauntlet/telemetry/**` looks like scaffolding next to the plan and gets deleted in the same commit - main then has no record for the run.
457
- - **Fix:** Never `git rm` under the telemetry dir. Until the seal the record is untracked and git-excluded, so a plan strip cannot reach it; after the seal it is a committed deliverable.
455
+ **Treating telemetry as a checkout deliverable**
456
+ - **Problem:** Staging a recorder file pollutes the product diff and bypasses branch publication.
457
+ - **Fix:** Let the seal own publication from common-dir metadata; stage no telemetry record in the product checkout.
458
458
 
459
459
  ## Completion
460
460
  Start the final outcome message with exactly one terminal line before summaries. Start the message with `gauntlet: done pr-created` after successful normal or draft PR creation. Start the message with `gauntlet: done merged` after successful squash-merge and post-merge verification. Start the message with `gauntlet: done kept` after Keep. Start the message with `gauntlet: done discarded` only after typed discard authorization and successful discard. Start the message with `gauntlet: done pushed` after successful PR-subject push and body edit, only after the close-member gate is answered or no members qualify. Start the message with `gauntlet: stuck` followed on that line by a short concrete reason for an executed operation that fails, before error evidence or a return menu. Do not combine a terminal marker with a gate marker in one message; a subsequent menu starts a new gate message. Pending choices, implementation stalls and conformance escalation are not terminal outcomes. Read and apply [menu-layout](../brainstorming/reference/menu-layout.md). Preserve strip, seal, cleanup and ship-state rules below; terminal reporting does not complete ship after Discard or failure. Once the chosen option (Options 1-4 or PR-subject row 1 - not Discard) is executed successfully, mark the ship phase complete:
@@ -478,7 +478,7 @@ Once the merge (and any deploy) has landed, `/skill:check-delivery <ticket-ref>`
478
478
  - Run any step without the `<worktree-path>` argument
479
479
  - Auto-proceed past an undispositioned carried-open gap
480
480
  - Skip the strip-and-seal block before the Option 3 squash or the Option 1/2 push
481
- - Delete the telemetry record (`.pi/gauntlet/telemetry/**` or the configured `telemetry.dir`) on any path
481
+ - Delete common-dir telemetry metadata or stage it into the product checkout
482
482
 
483
483
  **Always:**
484
484
  - Verify, or apply the Step 1 skip rule, before offering options
@@ -19,7 +19,7 @@ For ledger commands outside a checkout's configured test-command matcher, verify
19
19
 
20
20
  ## Skip proof
21
21
 
22
- **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.
22
+ **Skip rule.** Skip the run only when this set passed on a known clean commit in the verify phase of this session and no checkout edit or write landed since. Check the entire current tree: `git -C "$WORKTREE" diff --quiet <commit the run passed on> -- .` must exit 0, and `git -C "$WORKTREE" status --porcelain --untracked-files=all -- .` must exit 0 with empty output. Cover committed, staged, unstaged, and untracked changes without path exclusions; recorder metadata lives outside the working tree. Otherwise run it once; a missing verified commit, a failed check, or uncertainty means run.
23
23
 
24
24
  Apply the proof independently in each owning checkout. Post-squash verification is mandatory; the skip proof does not cover a changed primary tree.
25
25
 
@@ -31,4 +31,4 @@ For a fresh-session retry after a ledger was stripped, recover the committed pre
31
31
 
32
32
  ## Post-squash execution
33
33
 
34
- For Option 3, run each delivered leg's retained pairs in order as `(cd "<leg primary>" && <command>) > "$LOG_DIR/gate-<i>.log" 2>&1`, one command per pair, retaining its gate index and fail-fast exit reporting. For coordinated delivery, run only the just-squashed leg's pairs at that leg's delivery step; every remaining leg gets its own post-squash run when delivered. A failed-squash receipt retry runs those same pairs against the delivered primary, not the feature worktree, and never repeats the squash. Retain the Step 1 command set and execution form for non-ledger subjects.
34
+ For Option 3, run each delivered leg's retained pairs in order as `(cd "<leg primary>" && <command>) > "$LOG_DIR/gate-<i>.log" 2>&1`, one command per pair; print `gate <i> exit <code> <log path>` after each and stop on the first failure. For coordinated delivery, run only the just-squashed leg's pairs at that leg's delivery step; every remaining leg gets its own post-squash run when delivered. A failed-squash receipt retry runs those same pairs against the delivered primary, not the feature worktree, and never repeats the squash. Retain the Step 1 command set and execution form for non-ledger subjects.
@@ -92,6 +92,8 @@ Show evidence only in-place from record/helper paths, then re-ask. Queue all oth
92
92
 
93
93
  Run parent cleanup/stop only after all children finish, never on a live child's tree. For cleanup apply cleanup guards and ordered overrides. Keep the PR and post rows in the roster, set cleanup_done, and re-render with cleanup gone. Cleanup does not retire.
94
94
 
95
- For non-final stop run `cleanup.md` `## Non-final stop` after all children finish. Under terminal overlays leave checkout as found and print leftover path/branch. Delete only that PR's record, helper outputs, and log_path files under existing teardown rules, then retire it. Await all next-batch results and re-render every nonretired PR; repeat until all retire.
95
+ For non-final stop run `cleanup.md` `## Non-final stop` after all children finish. Under terminal overlays leave checkout as found and print leftover path/branch. Retain the PR's row from `post-selection-loop.md` `### Settlement rows` in the parent roster before deleting only that PR's record, helper outputs, and log_path files under existing teardown rules, then retire it. While any PR remains, start a retirement message with `gauntlet: retired #<N> - <reason>`, never `gauntlet: done` or `gauntlet: stuck`; keep private child result lines unchanged. Await all next-batch results and re-render every nonretired PR; repeat until all retire.
96
+
97
+ After the last PR retires and every selected cleanup or follow-on step finishes, render one final message. Start the message with `gauntlet: stuck retired` when any retained outcome is `stuck - <reason>`, otherwise `gauntlet: done retired`. Print every retained PR row below it in roster order, including PRs retired in earlier rounds. Emit no per-PR terminal message before this set settlement.
96
98
 
97
99
  Initialize parent tracker tasks `pr-<N> state: <record path>`; mark in_progress on dispatch, complete on retirement, failed on unretried failure. Child trackers are session-local stages, not parent roster tasks. Use a plain checklist if plan_tracker is unavailable.
@@ -22,7 +22,7 @@ Consequence clause for the menu row (`decision-menu.md`): `remove <worktree.path
22
22
 
23
23
  Follow the [shared layout](../../brainstorming/reference/menu-layout.md).
24
24
 
25
- Start the message with `gauntlet: stuck` followed on that line by the reason: use the obstacle's `stuck - ` text as the reason, or `stopped at <template id>` for initial, assess-terminal, and contract-exception. On a failed reset/assertion use the actual error instead. Put cleanup results below that one first line.
25
+ Finish the stop consequences below before the final message. Start the message with `gauntlet: stuck` followed on that line by the reason: use the obstacle's `stuck - ` text as the reason, or `stopped at <template id>` for initial, assess-terminal, and contract-exception. On a failed reset/assertion use the actual error instead. Print the PR row from `post-selection-loop.md` `### Settlement rows` with `stuck - <reason>` below the marker, then cleanup results. In set mode use `batch.md`'s per-PR retirement marker and retain the row for the final roster settlement.
26
26
 
27
27
  Enter here for `stop` on the initial, assess-terminal, obstacle, or contract-exception menu. On a `rebased locally` head, run `git -C <worktree.path> reset --hard <pre_head>` first; assert `HEAD == <pre_head>` and a clean tree. Stop on a failed assertion.
28
28
 
@@ -195,9 +195,15 @@ On completion or timeout, refresh then run steps 2-5 of Re-render plus step 1's
195
195
 
196
196
  Before posting or committing any external payload - review bodies, replies, commit subjects, tracker comments - re-read it against SKILL.md `## Wording rules` and any `## comms style` rules: ASCII only; no headings or template scaffolding under ~150 words; findings `file:line`-specific where one exists; ends on the fix or asked action, not a recap; never invents content to fill a section. The `AC coverage` block is the one exception: it posts verbatim, heading and checkbox rows included.
197
197
 
198
+ ### Settlement rows
199
+
200
+ Emit the terminal message only after every selected follow-on step finishes and the cleanup pick is answered (run or declined by stop); a report or menu is not settlement. Below its marker print one plain `#<N>@<head sha>: <outcome>[ (<ticket ref>)]` row per PR in roster order, singleton included. Use the full assessed `headRefOid`, not the merge commit or a shortened display SHA. Use the last gathered head when assessment could not start. Append the parenthetical only when the PR title or body's first line names a ticket under the overrides `## Issue tracker` `refs:` convention; otherwise omit it. Use exactly `approved`, `merged`, `closed-pr`, `final`, `merge-queued`, or `stuck - <reason>`, retaining the actual stop/error reason. Keep rows without bullets, code fences or emphasis; put checkout paths and cleanup results after them.
201
+
202
+ For set mode, retain each row before deleting its PR record and follow `batch.md`'s retirement and final-settlement rules. Keep private child result lines unchanged.
203
+
198
204
  ### Teardown
199
205
 
200
- Start the message with `gauntlet: done approved` for stop on approved. Start the message with `gauntlet: done merged` for stop on own-merge. Start the message with `gauntlet: done closed-pr` for stop on closed-pr. Start the message with `gauntlet: done final` for stop on final. Start the message with `gauntlet: done merge-queued` for stop on merge-queued. Put the checkout path and local branch below the terminal line. A report Verdict above a menu is not a terminal. Start the message with `gauntlet: stuck` followed by the actual error on any hard assertion failure, including a delegated fix-wave restore/assert failure, and end with the actual error; do not render a continuation menu.
206
+ Start the message with `gauntlet: done approved` for stop on approved. Start the message with `gauntlet: done merged` for stop on own-merge. Start the message with `gauntlet: done closed-pr` for stop on closed-pr. Start the message with `gauntlet: done final` for stop on final. Start the message with `gauntlet: done merge-queued` for stop on merge-queued. Print the row from `### Settlement rows`, then the checkout path and local branch. A report Verdict above a menu is not a terminal. Start the message with `gauntlet: stuck` followed by the actual error on any hard assertion failure, including a delegated fix-wave restore/assert failure; print the PR row with `stuck - <actual error>` below it and end with the actual error, without a continuation menu. In set mode use `batch.md`'s per-PR retirement marker until every PR retires.
201
207
 
202
208
  Delegate non-final stop (initial, assess terminal, obstacle, contract exception) to `cleanup.md` `## Non-final stop`. Keep terminal cleanup behind its own pick; terminal stop leaves the checkout as found and reports its path and local branch. Render cleanup only on terminal templates; never run cleanup steps 2-4 for a non-final stop.
203
209
 
@@ -94,16 +94,16 @@ After confirmation:
94
94
  2. Call `phase_tracker({ action: "start", phase: "brainstorm" })`, then `phase_tracker({ action: "skip", phase: "brainstorm", reason: "resume: <spec path>" })`, then `phase_tracker({ action: "start", phase: "plan" })`.
95
95
  3. Admit exactly one positive safe-integer `cap:` or `allowance:` header; both or neither is `ledger header malformed: <observed header lines>`, requiring human repair by hand. Call `plan_check({ planPath: <absolute ledger path> })` with legacy-compatible admission. On FAIL, stop with findings, retaining plan in_progress. Start the message with `gauntlet: stuck` followed by the findings reason on that line; never enter `## Plan repair` or writing-plans. On PASS, call `phase_tracker({ action: "skip", phase: "plan", reason: "resume: <spec> (loop engine)" })`, then `phase_tracker({ action: "start", phase: "implement" })`. Use the resolved absolute spec path as `<spec>` in the reason. Restore implement through refresh, not `init`.
96
96
  4. **Shared allowance boundary (every ledger restore, before phase continuation).** Read `/skill:loop-implementation`. Skip Entry step 1's allowance seeding; restore base, verification, iterations, row attempts and decisions from the ledger without reseeding. Resolve `gauntlet_setting({ key: "loopEngine" })` exactly once at the driver's authorized-restore grant boundary and retain `escalateAfter` in driver memory for this run; share this read with step 4.2, never refresh it inside the loop. On an unavailable tool, stop before any dispatch or write with `cannot resolve escalation threshold: settings tool unavailable (loopEngine.escalateAfter)`. Apply the following order on bare-worktree and brief routes alike:
97
- 1. Apply pending recovery below against the unmigrated ledger, including home returns whose child commit removed the tuple. Validate the original one-commit `<pre-dispatch HEAD>..HEAD` range against its unchanged before-image. For companions, retain unchanged home HEAD and admit the expected iterations-bump-plus-tuple dirt. On unrecoverable attribution or before-image, stop before refresh. Resolve ambiguous-history admission before tracker calls as required below. Retain every discard/reset and interrupted-dispatch gate; migration changes neither admission ranges nor the home HEAD used for recovery.
97
+ 1. Apply pending recovery below against the unmigrated ledger, including home returns whose child commit removed the tuple. Validate the original one-commit `<pre-dispatch HEAD>..HEAD` range against its unchanged before-image. For companions, retain unchanged home HEAD and admit the expected iterations-bump-plus-tuple dirt. On unrecoverable attribution or before-image, stop before refresh. Resolve ambiguous-history admission before tracker calls as required below. Apply the driver's ownership-fenced interrupted recovery before migration; migration changes neither admission ranges nor the home HEAD used for recovery.
98
98
  2. Migrate only an admitted, clean, tuple-free ledger with no retained recovery stop. With `cap:` and no `allowance:`, reuse the single restore-boundary settings read and replace the cap line with `allowance: <maxIterationsPerRow>`. Commit only that header replacement as parent `loop: migrate allowance`; preserve iterations, every attempts cell and all decisions. With an existing allowance, use it without allowance resolution or a migration commit; the restore read supplies `escalateAfter` only. Defer migration through any retained recovery stop or dirty tree; after authorized recovery reaches a clean tuple-free tree, migrate before the next dispatch or verification.
99
99
  3. Require `allowance:` and no `cap:` before continuation, including for all-closed ledgers; otherwise stop with `ledger has no allowance: line - resume through /skill:gauntlet-resume`. For a retained recovery stop or unresolved dirty tree, defer this final validation with migration and project the unmigrated ledger solely to report the stop.
100
100
  4. Call `plan_tracker({ action: "refresh", path: <absolute ledger path> })` exactly once after phase restoration, recovery and eligible migration/final validation, before any unresolved dirty-tree stop or dispatch. On verify/ship, restore the saved mixed task list first as `brief-contract.md` specifies; on implement, use no `init`. On refresh failure, stop with its findings and leave the existing task list untouched. Read `plan_tracker status` and report its exact observed state, never an inferred list.
101
- Render the allowance header, rows, iterations, attempts, effective allowances and all decisions; distinguish the latest operational `row <n> ...` decision from the last physical history line. Compute each effective allowance as the header plus one per matching retry decision; ignore the retry's audit ceiling. Keep `cap +N` history as inert text and restore no funded rounds. Report any retained recovery stop now with no redispatch, using the driver's unresolved-row counts and discard-authorization menu for dirty trees. Resume the original phase only after this boundary: implement at `Before each dispatch`; verify/ship at `After the loop` step 1.
101
+ Render the allowance header, rows, iterations, attempts, effective allowances and all decisions; distinguish the latest operational `row <n> ...` decision from the last physical history line. Compute each effective allowance as the header plus one per matching retry decision; ignore the retry's audit ceiling. Keep `cap +N` history as inert text and restore no funded rounds. For a recovered pending tuple with a recoverable before-image, apply the driver's ## Interrupted recovery below allowance before continuation; require exclusive checkout provenance and confirmed owned-job exits before any reset. Report unresolved cleanup or ownership with the driver's recovery-incomplete header, saved unresolved-row counts and discard-authorization menu, preserving changes and making no redispatch. Keep unrecoverable before-image and ambiguous-history stops. Resume the original phase only after this boundary: implement at `Before each dispatch`; verify/ship at `After the loop` step 1.
102
102
  5. Continue at loop step 5, the delivered driver's `Before each dispatch`, with durable iterations and retry data; an all-closed ledger completes implement through refresh and takes `After the loop` step 0. A migrated unresolved row at or above its effective allowance stops under rule 2 without an attempt; keep an allowance-stopped row `failed`. Render a row whose `pending:` tuple is restored or retained as `row <n> in_progress (pending tuple restored)`. Never reset counters or reseed a validated seed.
103
103
 
104
104
  ### Pending recovery
105
105
 
106
- Recover before dispatch or verification on every ledger restore. Run the read-only admission-evidence check below before phase/task restoration on every ledger route, including briefs; resolve its human question before any tracker call. Apply recovery writes only at the ordered pending-recovery step. Read the durable `pending:` tuple (target row or seed, repo, pre-dispatch HEAD); the parent's uncommitted iterations bump plus tuple is expected dispatch dirt, not a reason to skip recovery.
106
+ Recover before dispatch or verification on every ledger restore. Run the read-only admission-evidence check below before phase/task restoration on every ledger route, including briefs; resolve its human question before any tracker call. Apply recovery writes only at the ordered pending-recovery step. Read the durable `pending:` tuple (target row, seed or verify, repo, pre-dispatch HEAD); the parent's uncommitted iterations bump plus tuple is expected dispatch dirt, not a reason to skip recovery. Route `pending: verify` through the loop driver's After the loop step 1 repair admission/recovery before the row/seed paths below; preserve its reserved count and consumed iteration without a second marker or bump.
107
107
 
108
108
  With no `pending:` on disk, inspect home commit history before any refresh. Find the latest home child row/seed commit that changed this ledger, even when unrelated commits follow it; read its subject and body with `git -C <worktree> log --format=%H -- <ledger path>` and `git -C <worktree> show -s --format=%B <sha>`. Exclude returns superseded by a parent admission/repair or approved ledger mutation proven by the evidence below; never replay those over the parent's current ledger.
109
109
 
@@ -116,13 +116,13 @@ Revalidate a clean, exactly-one-commit return through the home row below. For la
116
116
  | Confirmed admission state | Action |
117
117
  |---|---|
118
118
  | Confirmed settled | Preserve the current ledger and continue normal restore; require fresh verification/conformance, not historical gate credit. |
119
- | Confirmed interrupted | Validate the entire `<pre-dispatch HEAD>..HEAD` range, never just `HEAD^..HEAD`, through the home row below. Render the interrupted/ambiguous-history answer in one sentence: validate the full `<HEAD_BEFORE>..HEAD` range (using the recovered pre-dispatch SHA, including later commits), restore every row the child changed (target and extras) and remove their unadmitted decisions from the pre-dispatch copy with the tuple before refresh on boundary rejection, state that iterations stay at the recovered count and child commits/changes are kept; use the saved cells for any other pre-dispatch status/attempts, or all dispatched non-cut rows for seed. Use the recovered tuple/count without another bump or a new commit. |
119
+ | Confirmed interrupted | Validate the entire `<pre-dispatch HEAD>..HEAD` range, never just `HEAD^..HEAD`, through the home row below. Use the recovered tuple and before-image without another bump. Confirmation that admission was interrupted proves no ownership of later writes: apply the driver's ## Interrupted recovery only with retained exclusive checkout/write provenance and owned-job exit evidence. Missing evidence preserves child and unrelated changes at recovery incomplete; project saved row cells without overwriting the ledger. A completed recovery writes the parent repair with iterations at the recovered consumed count. |
120
120
  | Still unknown | Stop without mutation; ask for the missing admission evidence, never turn uncertainty into a discard/reset decision. |
121
121
 
122
122
  | Tuple target | Recovery |
123
123
  |---|---|
124
- | Home leg or single-member `home` | Reconstruct the pre-dispatch ledger from `HEAD:<ledger path>` when HEAD is the tuple SHA, otherwise from `<pre-dispatch HEAD>:<ledger path>`. Compare live HEAD with that SHA. Admit exactly one new home commit with subject `loop: row <n> <status>` or `loop: seed evidence` only through the driver's `## Validate the return`: require a clean tree and the bounded row/decision result, allowing only the consumed iteration and pending removal header deltas. Clear the tuple through the admitted close or ordinary-miss repair. With zero new commits, keep the tuple and consumed iteration and take the interrupted-dispatch stop. Unrelated movement or multiple commits stop without redispatch or double counting. For a rejected terminal child result, restore every row the child changed (target and extras) and remove their unadmitted decisions in the working-tree ledger from the pre-dispatch copy with the tuple before refresh (all dispatched non-cut rows for seed); preserve other child changes, child commits and consumed iterations for the human's discard/reset decision. |
125
- | Actual multi-member companion | Apply `/skill:loop-implementation` `reference/coordinated-legs.md` `## Resume`: require unchanged home HEAD and exactly one new companion commit with tuple attribution, then validate its result. Record that leg's admitted rows, decisions or seeded state and SHA, removing the tuple in the home-ledger commit; never rerun the recovered row or its admitted extras. Stop preserving state on boundary/attribution failures (zero, unrelated or multiple commits or attribution mismatch); route post-boundary result-validation misses through ordinary `loop: repair row <n>` or seed `loop: repair seed` repair in that protocol, clear `pending:` and preserve the consumed iteration. |
124
+ | Home leg or single-member `home` | Reconstruct the pre-dispatch ledger from `HEAD:<ledger path>` when HEAD is the tuple SHA, otherwise from `<pre-dispatch HEAD>:<ledger path>`. Compare live HEAD with that SHA. Admit exactly one new home commit with subject `loop: row <n> <status>` or `loop: seed evidence` only through the driver's `## Validate the return`: require a clean tree and the bounded row/decision result, allowing only the consumed iteration and pending removal header deltas. Clear the tuple through the admitted close or ordinary-miss repair. Route zero new commits, multiple commits, dirty or non-DONE returns through the driver's ## Interrupted recovery against the tuple HEAD and its ledger before-image. Require exclusive checkout provenance and verified process identity/exit before reset; after clean/zero-commit observations write the repair with the already-consumed iteration, clear pending and continue at Before each dispatch. Preserve every change at recovery incomplete when ownership or cleanup is unresolved. Unrelated movement, attribution mismatch or an unrecoverable before-image stops without redispatch or double counting. |
125
+ | Actual multi-member companion | Apply `/skill:loop-implementation` `reference/coordinated-legs.md` `## Resume`: require unchanged home HEAD and exactly one new companion commit with tuple attribution, then validate its result. Record that leg's admitted rows, decisions or seeded state and SHA, removing the tuple in the home-ledger commit; never rerun the recovered row or its admitted extras. Route dirty, zero/multiple-commit or non-DONE boundaries through that reference's ownership-fenced interrupted recovery, using the companion tuple HEAD and saved home ledger; preserve state on unrelated movement, attribution mismatch, unrecoverable before-image or unresolved ownership/cleanup; route post-boundary result-validation misses through ordinary `loop: repair row <n>` or seed `loop: repair seed` repair in that protocol, clear `pending:` and preserve the consumed iteration. |
126
126
 
127
127
  ## Spec with plan
128
128
 
@@ -13,12 +13,7 @@ Before starting the seed sequence, read the seed's header. A `**Home:**` header
13
13
  stop: `not tracked: <seed> - commit it first`. Line 1 of `<seed>` equal to
14
14
  `# CONTEXT DRAFT - NOT A SPEC - fully replaced at spec-writing` -> stop:
15
15
  `context draft, not a spec: <seed> - finish brainstorming`.
16
- 3. **Not shipped.** Read `<primary>/<telemetry.dir>/<rel with .md -> .yaml>`
17
- (`piGauntlet.telemetry.dir`, default `.pi/gauntlet/telemetry`). A `shipped_at:` or
18
- `abandoned_at:` line carrying a value -> stop:
19
- `spec already shipped at <ts> - write a superseding spec`. No record, or
20
- `in_progress`: continue; the extension folds this run into the record on
21
- `plan_check` pass.
16
+ 3. **Not shipped.** Resolve `piGauntlet.telemetry` from preset and primary project settings; `enabled: false` skips this check. Run `git -C <primary> rev-parse --git-common-dir`; resolve a relative result against `<primary>` and read `<git common dir>/gauntlet/telemetry/<rel with .md -> .yaml>`. When git resolution fails and `jj root` in `<primary>` succeeds, read `<jj root>/.jj/gauntlet/telemetry/<rel with .md -> .yaml>` instead. When the local record is absent, fetch `gauntlet/telemetry` from `telemetry.remote` (default `origin`) and read `<owner>/<repo>/brainstorming/<rel with .md -> .yaml>` from the fetched ref, using the last fetched ref on fetch failure. Degrade to the local-only check when no fetched ref is available, including jj-only workspaces. A valued `shipped_at:` or `abandoned_at:` stops: `spec already shipped at <ts> - write a superseding spec`. No record or `in_progress`: continue; let the extension bind on `plan_check` pass.
22
17
  4. **Name.** `<name>` = basename of `<rel>` minus `.md`, minus a leading `YYYY-MM-DD-`:
23
18
  `2026-09-17-gh-31-gauntlet-resume.md` -> `gh-31-gauntlet-resume`. Same shape as
24
19
  brainstorming's Filename Convention slug; wrappers receive it as `<name>`.