@pmelab/gtd 24.2.0 → 24.3.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 (38) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +26 -2
  3. package/dist/chunks/{Schema-Dh-mkwkq.mjs → Schema-B5yK7QrT.mjs} +47 -1
  4. package/dist/chunks/{reserved-ejn-LVYO.mjs → reserved-DJwU5C4z.mjs} +1 -1
  5. package/dist/gtd.bundle.mjs +337 -55
  6. package/dist/testkit.mjs +477 -65
  7. package/dist/workflows/feature.mjs +1 -1
  8. package/dist/workflows/fix.mjs +1 -1
  9. package/dist/workflows/review.mjs +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/Cli.test.ts +57 -0
  12. package/src/cli/Cli.ts +89 -9
  13. package/src/cli/History.test.ts +225 -0
  14. package/src/cli/History.ts +143 -0
  15. package/src/cli/Output.ts +68 -0
  16. package/src/cli/Program.test.ts +7 -1
  17. package/src/cli/Program.ts +23 -64
  18. package/src/edge/Edge.test.ts +256 -1
  19. package/src/edge/Edge.ts +217 -39
  20. package/src/edge/index.ts +1 -0
  21. package/src/emit/GitScript.test.ts +27 -0
  22. package/src/emit/GitScript.ts +18 -2
  23. package/src/emit/index.ts +1 -0
  24. package/src/platform/Git.ts +48 -10
  25. package/src/platform/RetainedHistory.test.ts +14 -0
  26. package/src/platform/RetainedHistory.ts +5 -0
  27. package/src/platform/index.ts +7 -1
  28. package/src/replay/Trailers.test.ts +32 -0
  29. package/src/replay/Trailers.ts +17 -0
  30. package/src/step/LandStep.ts +3 -1
  31. package/src/step/RepoSnapshot.ts +7 -1
  32. package/src/step/planStart.test.ts +6 -3
  33. package/src/step/planStep.test.ts +29 -1
  34. package/src/step/planStep.ts +8 -0
  35. package/src/testkit/EmittedScriptRecognizer.ts +1 -1
  36. package/src/testkit/FakeGitOperations.ts +6 -2
  37. package/src/testkit/GitTiers.ts +133 -2
  38. package/src/testkit/InMemRepo.ts +26 -6
@@ -6826,7 +6826,7 @@ var init_label = __esmMin((() => {
6826
6826
  }));
6827
6827
  //#endregion
6828
6828
  //#region node_modules/effect/dist/esm/internal/core-effect.js
6829
- var annotateLogs$1, asSome, try_$1, catchTag$1, diffFiberRefs, bind, bindTo, match$4, fiberRefs, ignore$1, logWithLevel, logWarning$1, logError$1, mapErrorCause, negate$1, patchFiberRefs, promise$1, provideService$1, provideServiceEffect, sandbox$1, sleep$1, succeedNone, summarized, tapErrorCause$1, tryPromise$1, tryMap$1, updateFiberRefs, when$1, serviceOption$1;
6829
+ var annotateLogs$1, asSome, try_$1, catchTag$1, diffFiberRefs, bind, bindTo, match$4, fiberRefs, ignore$1, logWithLevel, logWarning$1, logError$1, mapErrorCause, negate$1, orElseSucceed$1, patchFiberRefs, promise$1, provideService$1, provideServiceEffect, sandbox$1, sleep$1, succeedNone, summarized, tapErrorCause$1, tryPromise$1, tryMap$1, updateFiberRefs, when$1, serviceOption$1;
6830
6830
  var init_core_effect = __esmMin((() => {
6831
6831
  init_Clock();
6832
6832
  init_Context();
@@ -6910,6 +6910,7 @@ var init_core_effect = __esmMin((() => {
6910
6910
  onSuccess: succeed$8
6911
6911
  }));
6912
6912
  negate$1 = (self) => map$8(self, (b) => !b);
6913
+ orElseSucceed$1 = /*#__PURE__*/ dual(2, (self, evaluate) => orElse$2(self, () => sync$2(evaluate)));
6913
6914
  patchFiberRefs = (patch) => updateFiberRefs((fiberId, fiberRefs) => pipe(patch, patch$2(fiberId, fiberRefs)));
6914
6915
  promise$1 = (evaluate) => evaluate.length >= 1 ? async_((resolve, signal) => {
6915
6916
  try {
@@ -11426,7 +11427,7 @@ var init_MutableQueue = __esmMin((() => {
11426
11427
  }));
11427
11428
  //#endregion
11428
11429
  //#region node_modules/effect/dist/esm/Effect.js
11429
- var isEffect, all$1, forEach$2, async, withFiberRuntime, fail$7, failCause$2, die, dieMessage, gen, promise, succeed$3, suspend$5, sync$1, _void, catchAll, catchAllCause, catchTag, ignore, sandbox, retry, try_, tryMap, tryPromise, interrupt, interruptible, uninterruptible, uninterruptibleMask, as, asVoid, map$4, mapBoth$1, mapError$1, negate, acquireRelease, acquireUseRelease$2, addFinalizer, ensuring$2, onError, scope$1, scopeWith, scopedWith$1, scoped$3, fiberIdWith, forkDaemon, forkIn, forkScoped, timeout, provide$1, provideService, serviceOption, either, exit$1, intoDeferred, when, flatMap$4, andThen, flatten$5, race, raceWith, tap, tapErrorCause, repeat, match$2, matchCause, matchCauseEffect, matchEffect, logWarning, logError, annotateLogs, orDie, orElse$1, runtime, unsafeMakeSemaphore, makeSemaphore, unsafeMakeLatch, runFork$1, runSync, zip$1, zipLeft, zipRight$1, zipWith, fnUntraced;
11430
+ var isEffect, all$1, forEach$2, async, withFiberRuntime, fail$7, failCause$2, die, dieMessage, gen, promise, succeed$3, suspend$5, sync$1, _void, catchAll, catchAllCause, catchTag, ignore, sandbox, retry, try_, tryMap, tryPromise, interrupt, interruptible, uninterruptible, uninterruptibleMask, as, asVoid, map$4, mapBoth$1, mapError$1, negate, acquireRelease, acquireUseRelease$2, addFinalizer, ensuring$2, onError, scope$1, scopeWith, scopedWith$1, scoped$3, fiberIdWith, forkDaemon, forkIn, forkScoped, timeout, provide$1, provideService, serviceOption, either, exit$1, intoDeferred, when, flatMap$4, andThen, flatten$5, race, raceWith, tap, tapErrorCause, repeat, match$2, matchCause, matchCauseEffect, matchEffect, logWarning, logError, annotateLogs, orDie, orElse$1, orElseSucceed, runtime, unsafeMakeSemaphore, makeSemaphore, unsafeMakeLatch, runFork$1, runSync, zip$1, zipLeft, zipRight$1, zipWith, fnUntraced;
11430
11431
  var init_Effect = __esmMin((() => {
11431
11432
  init_core_effect();
11432
11433
  init_core();
@@ -11507,6 +11508,7 @@ var init_Effect = __esmMin((() => {
11507
11508
  annotateLogs = annotateLogs$1;
11508
11509
  orDie = orDie$1;
11509
11510
  orElse$1 = orElse$2;
11511
+ orElseSucceed = orElseSucceed$1;
11510
11512
  runtime = runtime$1;
11511
11513
  unsafeMakeSemaphore = unsafeMakeSemaphore$1;
11512
11514
  makeSemaphore = makeSemaphore$1;
@@ -20243,8 +20245,17 @@ const resolvedRefOrCorrupted = (ref, hash) => /^[0-9a-f]{40}$/.test(hash) ? succ
20243
20245
  const makeGitImpl = (executor, root) => {
20244
20246
  const exec = (...args) => run$4(root, ...args).pipe(provide$1(succeed$2(CommandExecutor, executor)));
20245
20247
  return {
20246
- lastCommitSubject: (ref = "HEAD") => exec("git", "log", "-1", "--pretty=%s", ref).pipe(map$4((s) => s.trim())),
20247
- lastCommitMessage: () => exec("git", "log", "-1", "--pretty=%B").pipe(map$4((s) => s.trim())),
20248
+ lastCommitSubject: (ref = "HEAD") => exec("git", "log", "-1", "--pretty=%s", "--end-of-options", ref).pipe(map$4((s) => s.trim())),
20249
+ lastCommitMessage: (ref = "HEAD") => exec("git", "log", "-1", "--pretty=%B", "--end-of-options", ref).pipe(map$4((s) => s.trim())),
20250
+ currentBranch: () => exec("git", "symbolic-ref", "--quiet", "--short", "HEAD").pipe(map$4((s) => s.trim()), map$4((name) => some(name)), catchAll(() => succeed$3(none$4()))),
20251
+ listRefs: (prefix) => exec("git", "for-each-ref", "--sort=-committerdate", "--format=%(refname)%09%(objectname)%09%(committerdate:iso-strict)", prefix).pipe(map$4((out) => out.split("\n").filter((line) => line.length > 0).map((line) => {
20252
+ const [ref = "", hash = "", date = ""] = line.split(" ");
20253
+ return {
20254
+ ref,
20255
+ hash,
20256
+ date
20257
+ };
20258
+ }))),
20248
20259
  hasCommits: () => exec("git", "rev-parse", "--verify", "HEAD").pipe(map$4(() => true), catchAll(() => succeed$3(false))),
20249
20260
  readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
20250
20261
  changedPaths: (base) => gen(function* () {
@@ -20264,7 +20275,7 @@ const makeGitImpl = (executor, root) => {
20264
20275
  }
20265
20276
  return all;
20266
20277
  }),
20267
- resolveRef: (ref) => exec("git", "rev-parse", "--verify", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => resolvedRefOrCorrupted(ref, hash))),
20278
+ resolveRef: (ref) => exec("git", "rev-parse", "--verify", "--end-of-options", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => resolvedRefOrCorrupted(ref, hash))),
20268
20279
  readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
20269
20280
  isAncestor: (a, b) => run$4(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$1(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
20270
20281
  mergeBase: (a, b) => exec("git", "merge-base", a, b).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
@@ -20920,6 +20931,9 @@ const loopLogPath = gen(function* () {
20920
20931
  init_esm();
20921
20932
  /** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
20922
20933
  const HISTORY_REF = "refs/worktree/gtd/history";
20934
+ /** One ref per finished process, kept past the branch's deletion; distinct from the per-worktree `HISTORY_REF`. */
20935
+ const PROCESS_HISTORY_PREFIX = "refs/gtd/history/";
20936
+ const processHistoryRef = (hash) => `${PROCESS_HISTORY_PREFIX}${hash}`;
20923
20937
  const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
20924
20938
  /**
20925
20939
  * Whether resetting to `tipHash` is safe: HEAD must be an ancestor of
@@ -37547,6 +37561,12 @@ const TRAILER_READERS = {
37547
37561
  "Gtd-Review-Base": (value, into) => {
37548
37562
  into.reviewBase ??= value.split(/\s/)[0];
37549
37563
  },
37564
+ "Gtd-Branch": (value, into) => {
37565
+ if (value !== "") into.branch ??= value;
37566
+ },
37567
+ "Gtd-Version": (value, into) => {
37568
+ if (value !== "") into.version ??= value;
37569
+ },
37550
37570
  "Gtd-Format": (value, into) => {
37551
37571
  if (FORMAT_RE.test(value)) into.format ??= Number(value);
37552
37572
  },
@@ -37565,6 +37585,8 @@ const parseCommitMessage = (message) => {
37565
37585
  step: void 0,
37566
37586
  workflow: void 0,
37567
37587
  reviewBase: void 0,
37588
+ branch: void 0,
37589
+ version: void 0,
37568
37590
  judge: [],
37569
37591
  vars: {},
37570
37592
  config: {},
@@ -37590,6 +37612,8 @@ const formatCommitMessage = (spec) => {
37590
37612
  for (const [path, hash] of sortedEntries(spec.config)) lines.push(`Gtd-Config: ${hash} ${encodePath(path)}`);
37591
37613
  if (spec.cost !== void 0) lines.push(`Gtd-Cost: ${spec.cost.cost}${spec.cost.model !== void 0 ? ` ${spec.cost.model}` : ""}`);
37592
37614
  for (const verdict of spec.judge ?? []) lines.push(`Gtd-Judge: ${JSON.stringify(verdict)}`);
37615
+ if (spec.branch !== void 0) lines.push(`Gtd-Branch: ${spec.branch}`);
37616
+ if (spec.version !== void 0) lines.push(`Gtd-Version: ${spec.version}`);
37593
37617
  lines.push(`Gtd-Format: 1`);
37594
37618
  return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
37595
37619
  };
@@ -38902,6 +38926,14 @@ const planStep = (snapshot, opts = {}) => {
38902
38926
  message
38903
38927
  }
38904
38928
  },
38929
+ ...landing.keepHistory !== void 0 ? [{
38930
+ kind: "gitWrite",
38931
+ write: {
38932
+ kind: "updateRef",
38933
+ ref: landing.keepHistory,
38934
+ target: "HEAD"
38935
+ }
38936
+ }] : [],
38905
38937
  outcome
38906
38938
  ]
38907
38939
  };
@@ -39214,25 +39246,38 @@ const currentRun = gen(function* () {
39214
39246
  const config = yield* (yield* ConfigService).load;
39215
39247
  return yield* computeProcessRun(git, config.workflow);
39216
39248
  });
39217
- /** The process HEAD closes or sits inside — `gtd summary`'s run. */
39218
- const summaryRun = gen(function* () {
39219
- const git = yield* GitService;
39220
- const def = (yield* (yield* ConfigService).load).workflow;
39221
- const headEntry = (yield* git.subjectHistory(1, 0))[0];
39222
- if (headEntry === void 0) {
39249
+ const runAtTip = (git, def, tip) => gen(function* () {
39250
+ if (tip === void 0) {
39223
39251
  const history = yield* historyUpTo(git, def, void 0);
39224
39252
  return yield* runOf(history, locateEpisode(def, history));
39225
39253
  }
39226
- if (!(parseCommitMessage(headEntry.subject).parsed?.to === def.initial)) {
39227
- const history = yield* historyUpTo(git, def, headEntry.hash);
39254
+ if (!(parseCommitMessage(tip.subject).parsed?.to === def.initial)) {
39255
+ const history = yield* historyUpTo(git, def, tip.hash);
39228
39256
  return yield* runOf(history, locateEpisode(def, history));
39229
39257
  }
39230
- const parentRef = `${headEntry.hash}~1`;
39258
+ const parentRef = `${tip.hash}~1`;
39231
39259
  const before = yield* historyUpTo(git, def, parentRef);
39232
39260
  const location = locateEpisode(def, before);
39233
- let closing = yield* git.commitHistory(parentRef, headEntry.hash);
39234
- if (closing.length === 0) closing = yield* git.commitHistory(void 0, headEntry.hash);
39235
- return yield* runOf([...before, ...closing], { ...location }, headEntry.hash);
39261
+ let closing = yield* git.commitHistory(parentRef, tip.hash);
39262
+ if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
39263
+ return yield* runOf([...before, ...closing], { ...location }, tip.hash);
39264
+ });
39265
+ /** The process HEAD closes or sits inside — `gtd summary`'s run. */
39266
+ const summaryRun = gen(function* () {
39267
+ const git = yield* GitService;
39268
+ const def = (yield* (yield* ConfigService).load).workflow;
39269
+ return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
39270
+ });
39271
+ /** The process whose finishing commit is `tip` — reads from `tip`'s own ancestry, so it works when no branch reaches it. */
39272
+ const closedRunAt = (tip) => gen(function* () {
39273
+ const git = yield* GitService;
39274
+ const def = (yield* (yield* ConfigService).load).workflow;
39275
+ const hash = yield* git.resolveRef(tip);
39276
+ const subject = yield* git.lastCommitSubject(hash);
39277
+ return yield* runAtTip(git, def, {
39278
+ hash,
39279
+ subject
39280
+ });
39236
39281
  });
39237
39282
  const commitTree = (workspace, hash) => {
39238
39283
  let entries;
@@ -39452,13 +39497,7 @@ const costByModel = (entries) => {
39452
39497
  cost
39453
39498
  })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
39454
39499
  };
39455
- /** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
39456
- const restAt = (ref, startOverrides = {}) => gen(function* () {
39457
- const git = yield* GitService;
39458
- const config = yield* (yield* ConfigService).load;
39459
- const workspace = yield* Workspace;
39460
- const host = yield* Host;
39461
- const run = yield* computeProcessRun(git, config.workflow, ref);
39500
+ const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(function* () {
39462
39501
  const resolved = yield* workflowOf(config, run);
39463
39502
  const def = resolved.def;
39464
39503
  const { vars, env } = settingsFor(resolved, run.pinnedVars, host.env, startOverrides);
@@ -39482,19 +39521,34 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
39482
39521
  tree: commitTree(workspace, c.hash)
39483
39522
  }));
39484
39523
  let pendingMemo;
39485
- const setup = {
39524
+ return {
39525
+ resolved,
39486
39526
  def,
39487
- run,
39488
39527
  vars,
39489
39528
  env,
39490
- budget,
39491
- skills,
39492
- access,
39493
- workspace,
39494
- base,
39495
- episode,
39496
- pendingWorktree: () => pendingMemo ??= workspace.worktreeSync()
39529
+ setup: {
39530
+ def,
39531
+ run,
39532
+ vars,
39533
+ env,
39534
+ budget,
39535
+ skills,
39536
+ access,
39537
+ workspace,
39538
+ base,
39539
+ episode,
39540
+ pendingWorktree: () => pendingMemo ??= workspace.worktreeSync()
39541
+ }
39497
39542
  };
39543
+ });
39544
+ /** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
39545
+ const restAt = (ref, startOverrides = {}) => gen(function* () {
39546
+ const git = yield* GitService;
39547
+ const config = yield* (yield* ConfigService).load;
39548
+ const workspace = yield* Workspace;
39549
+ const host = yield* Host;
39550
+ const run = yield* computeProcessRun(git, config.workflow, ref);
39551
+ const { resolved, def, vars, env, setup } = yield* setupFor(config, run, workspace, host, startOverrides);
39498
39552
  const outcome = yield* promise(() => replayFor(setup));
39499
39553
  const error = replayError(outcome);
39500
39554
  if (error !== void 0 || outcome.kind !== "rest") return yield* fail$7(error ?? /* @__PURE__ */ new Error("gtd: replay found no rest"));
@@ -39636,14 +39690,21 @@ const initialStepRefusal = (workflow, step, initial) => workflow !== void 0 && s
39636
39690
  kind: "refusal",
39637
39691
  message: `gtd: workflow "${workflow}" reaches step "${step}", the default workflow's first step — that is where a finished process waits, so a started process must never land there; rename the step`
39638
39692
  } : void 0;
39639
- const landingTo = (rest, to) => {
39640
- if (rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state) return {
39693
+ const closingTrailers = (branch) => ({
39694
+ version: GTD_VERSION,
39695
+ ...isSome(branch) ? { branch: branch.value } : {}
39696
+ });
39697
+ const isSettledNoop = (rest, to) => rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state;
39698
+ const landingTo = (rest, to, branch) => {
39699
+ if (isSettledNoop(rest, to)) return {
39641
39700
  kind: "noop",
39642
39701
  settled: true
39643
39702
  };
39644
- const starts = noProcessUnderway(rest) && to !== rest.def.initial;
39703
+ const underway = !noProcessUnderway(rest);
39704
+ const starts = !underway && to !== rest.def.initial;
39645
39705
  const refused = starts ? multilineRefusal(rest.vars) : void 0;
39646
39706
  if (refused !== void 0) return refused;
39707
+ const finishes = underway && to === rest.def.initial;
39647
39708
  return {
39648
39709
  kind: "commit",
39649
39710
  to,
@@ -39655,8 +39716,10 @@ const landingTo = (rest, to) => {
39655
39716
  ...starts ? {
39656
39717
  vars: rest.vars,
39657
39718
  config: rest.config
39658
- } : {}
39659
- }
39719
+ } : {},
39720
+ ...finishes ? closingTrailers(branch) : {}
39721
+ },
39722
+ ...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {}
39660
39723
  };
39661
39724
  };
39662
39725
  /**
@@ -39664,7 +39727,7 @@ const landingTo = (rest, to) => {
39664
39727
  * replay with the working tree as the pending turn. The target step is
39665
39728
  * computed by replaying, never matched.
39666
39729
  */
39667
- const decideLanding = (rest, verdicts) => gen(function* () {
39730
+ const decideLanding = (rest, verdicts, branch) => gen(function* () {
39668
39731
  const early = cleanLanding(rest);
39669
39732
  if (early !== void 0) return early;
39670
39733
  if (rest.stepDef.kind === "prompt" && rest.stepDef.access?.write != null) {
@@ -39680,13 +39743,14 @@ const decideLanding = (rest, verdicts) => gen(function* () {
39680
39743
  }));
39681
39744
  const to = yield* landingTarget(outcome, rest);
39682
39745
  if (typeof to !== "string") return to;
39683
- return landingTo(rest, to);
39746
+ return landingTo(rest, to, branch);
39684
39747
  });
39685
39748
  /** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
39686
- const previewLanding = (rest) => decideLanding(rest, void 0).pipe(map$4((landing) => landing.kind === "commit" ? landing.to : void 0), catchAll(() => succeed$3(void 0)));
39749
+ const previewLanding = (rest) => decideLanding(rest, void 0, none$4()).pipe(map$4((landing) => landing.kind === "commit" ? landing.to : void 0), catchAll(() => succeed$3(void 0)));
39687
39750
  /** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
39688
39751
  const snapshotFromRest = (rest, verdicts) => gen(function* () {
39689
- const landing = yield* decideLanding(rest, verdicts);
39752
+ const branch = yield* (yield* GitService).currentBranch();
39753
+ const landing = yield* decideLanding(rest, verdicts, branch);
39690
39754
  return {
39691
39755
  stepDef: rest.stepDef,
39692
39756
  state: rest.state,
@@ -39717,6 +39781,66 @@ const summaryFor = (run) => gen(function* () {
39717
39781
  ...settingsFor(resolved, run.pinnedVars, host.env)
39718
39782
  });
39719
39783
  });
39784
+ /** Repository-relative paths of the config files today's config and the process's own record name; one outside the repository cannot be read at a ref, so it is left out. */
39785
+ const inRepoConfigPaths = (root, files, recorded) => {
39786
+ const paths = new Set(Object.keys(recorded));
39787
+ for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
39788
+ return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
39789
+ };
39790
+ const configRefusal = (tip, paths) => /* @__PURE__ */ new Error(`gtd history: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. A history ref replays under today's config, so it is unreadable after the config changes`);
39791
+ const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
39792
+ const differing = [];
39793
+ for (const path of inRepoConfigPaths(root, config.workflowFiles, run.pinnedConfig?.files ?? {})) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
39794
+ return differing;
39795
+ });
39796
+ const notClosedError = (hash, outcome) => outcome.kind === "rest" ? /* @__PURE__ */ new Error(`gtd history: ${hash.slice(0, 7)} does not close a process under today's config`) : replayError(outcome) ?? /* @__PURE__ */ new Error("gtd history: replay failed");
39797
+ const historySteps = (run) => {
39798
+ const messages = new Map(run.episode.commits.map((c) => [c.hash, c.message]));
39799
+ return run.trace.map((entry) => {
39800
+ const message = parseCommitMessage(messages.get(entry.hash) ?? "");
39801
+ return {
39802
+ hash: entry.hash,
39803
+ subject: (messages.get(entry.hash) ?? "").split("\n")[0] ?? "",
39804
+ actor: entry.actor,
39805
+ from: message.parsed?.from,
39806
+ to: entry.state,
39807
+ verdicts: message.judge
39808
+ };
39809
+ });
39810
+ };
39811
+ const versionWarningFor = (recorded) => recorded === GTD_VERSION ? void 0 : `gtd: warning: this history was recorded by ${recorded === void 0 ? "an unknown gtd version" : `gtd ${recorded}`} and is replayed by gtd ${GTD_VERSION} — a workflow imported from @pmelab/gtd/workflows may have changed since`;
39812
+ /**
39813
+ * Replays the closed run at `tip` under today's config. The workflow is not
39814
+ * fingerprinted for a version change: one imported from `@pmelab/gtd/workflows`
39815
+ * moves with every upgrade, and the version warning is the only signal.
39816
+ */
39817
+ const historyTrail = (tip) => gen(function* () {
39818
+ const git = yield* GitService;
39819
+ const config = yield* (yield* ConfigService).load;
39820
+ const workspace = yield* Workspace;
39821
+ const host = yield* Host;
39822
+ const hash = yield* git.resolveRef(tip);
39823
+ const run = yield* closedRunAt(hash);
39824
+ const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
39825
+ if (differing.length > 0) return yield* fail$7(configRefusal(hash, differing));
39826
+ const { setup } = yield* setupFor(config, run, workspace, host);
39827
+ yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
39828
+ const outcome = yield* promise(() => replayFor(setup));
39829
+ if (outcome.kind !== "ended") return yield* fail$7(notClosedError(hash, outcome));
39830
+ const steps = historySteps(run);
39831
+ const recorded = parseCommitMessage(yield* git.lastCommitMessage(hash)).version;
39832
+ const versionWarning = versionWarningFor(recorded);
39833
+ return {
39834
+ tip: hash,
39835
+ steps,
39836
+ ended: outcome.via,
39837
+ version: {
39838
+ recorded,
39839
+ running: GTD_VERSION
39840
+ },
39841
+ ...versionWarning !== void 0 ? { versionWarning } : {}
39842
+ };
39843
+ });
39720
39844
  //#endregion
39721
39845
  //#region src/emit/GitScript.ts
39722
39846
  const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
@@ -39738,13 +39862,24 @@ const commitAllowEmpty = (message) => {
39738
39862
  `fi`
39739
39863
  ].join("\n");
39740
39864
  };
39741
- /** `git add -A` then `commitAllowEmpty`, joined with `&&` so a failing `git add` never reaches the commit. */
39742
- const commitAll = (message) => `git add -A &&\n${commitAllowEmpty(message)}`;
39865
+ /** `git add -A || exit 1` (an `&&` left side would not abort under `set -e`) then `commitAllowEmpty`. */
39866
+ const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
39743
39867
  const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
39744
39868
  const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
39745
39869
  const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
39746
39870
  /** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
39747
39871
  const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
39872
+ /**
39873
+ * No `+` on the refspec: a local ref the remote's tip does not descend from
39874
+ * fails the fetch instead of being overwritten (a fast-forward still applies).
39875
+ */
39876
+ const fetchHistoryRefs = (remote) => [
39877
+ `set -eu`,
39878
+ `before=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
39879
+ `git fetch ${shellQuote$1(remote)} 'refs/gtd/history/*:refs/gtd/history/*'`,
39880
+ `after=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
39881
+ `printf '%s history refs arrived\\n' "$((after - before))"`
39882
+ ].join("\n");
39748
39883
  /** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/testkit/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
39749
39884
  const OUTCOME_MARKER$1 = "# gtd: outcome (print-only)";
39750
39885
  const printfLine$1 = (fmt, args) => `${OUTCOME_MARKER$1}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
@@ -39762,6 +39897,7 @@ const renderOutcome = (outcome) => {
39762
39897
  const renderGitWrite = (write) => {
39763
39898
  switch (write.kind) {
39764
39899
  case "commitAll": return commitAll(write.message);
39900
+ case "updateRef": return updateRef(write.ref, write.target);
39765
39901
  }
39766
39902
  };
39767
39903
  /**
@@ -40133,10 +40269,9 @@ init_esm();
40133
40269
  */
40134
40270
  var ToolCommands = class extends Tag("ToolCommands")() {};
40135
40271
  //#endregion
40136
- //#region src/cli/Program.ts
40272
+ //#region src/cli/Output.ts
40137
40273
  init_esm();
40138
40274
  init_wire();
40139
- init_steering();
40140
40275
  /**
40141
40276
  * A command-level failure that should exit like a CLI usage error (2), not a
40142
40277
  * generic runtime failure (1): an unknown `--json=<path>` selector, or a
@@ -40146,12 +40281,97 @@ init_steering();
40146
40281
  * about the resolved rest's own state.
40147
40282
  */
40148
40283
  var SelectorUsageError = class extends Error {};
40149
- /** The `--json=<path>` select branch, shared by all three emitting commands so none drift on the unknown-selector message. `absent` writes nothing and lets the caller's success path continue. */
40284
+ /** The `--json=<path>` select branch, shared by the emitting commands so none drift on the unknown-selector message. `absent` writes nothing and lets the caller's success path continue. */
40150
40285
  const writeSelection = (out, fields, path) => gen(function* () {
40151
40286
  const selection = selectPath(fields, path);
40152
40287
  if (selection.kind === "value") out.write(`${selection.text}\n`);
40153
40288
  else if (selection.kind === "unknown") return yield* fail$7(new SelectorUsageError(`gtd: unknown --json selector "${selection.path}" — see \`gtd --help\``));
40154
40289
  });
40290
+ //#endregion
40291
+ //#region src/cli/History.ts
40292
+ init_esm();
40293
+ /** Newest tip first. */
40294
+ const historyEntries = gen(function* () {
40295
+ const git = yield* GitService;
40296
+ const refs = (yield* git.listRefs(PROCESS_HISTORY_PREFIX)).filter(({ ref }) => ENTRY_HASH.test(ref.slice(17)));
40297
+ return yield* forEach$2(refs, ({ ref, hash, date }) => gen(function* () {
40298
+ const entry = ref.slice(17);
40299
+ return {
40300
+ ref,
40301
+ entry,
40302
+ branch: parseCommitMessage(yield* git.lastCommitMessage(hash)).branch ?? null,
40303
+ tip: hash,
40304
+ date,
40305
+ subject: yield* git.lastCommitSubject(entry)
40306
+ };
40307
+ }));
40308
+ });
40309
+ const ENTRY_HASH = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
40310
+ const HEX = /^[0-9a-f]+$/i;
40311
+ /**
40312
+ * Lookup order: full ref; then entry hash or hex prefix (unique → it, several
40313
+ * → fail listing them); then branch name (newest tip first, as `entries` is
40314
+ * ordered). A hex-looking name that matches no entry still resolves as a
40315
+ * branch. No match → fail.
40316
+ */
40317
+ const resolveHistoryRef = (arg, entries) => {
40318
+ const byRef = entries.find((e) => e.ref === arg);
40319
+ if (byRef !== void 0) return succeed$3(byRef);
40320
+ if (HEX.test(arg)) {
40321
+ const lower = arg.toLowerCase();
40322
+ const matches = entries.filter((e) => e.entry.toLowerCase().startsWith(lower));
40323
+ if (matches.length === 1) return succeed$3(matches[0]);
40324
+ if (matches.length > 1) return fail$7(/* @__PURE__ */ new Error(`gtd history: "${arg}" is ambiguous — it matches:\n${matches.map((e) => ` ${e.entry} ${e.branch ?? "-"}`).join("\n")}`));
40325
+ }
40326
+ const byBranch = entries.find((e) => e.branch === arg);
40327
+ if (byBranch !== void 0) return succeed$3(byBranch);
40328
+ return fail$7(/* @__PURE__ */ new Error(`gtd history: no history ref matches "${arg}"`));
40329
+ };
40330
+ const seven = (hash) => hash.slice(0, 7);
40331
+ /** Writes `document` per `json` (bare → whole, `=path` → one value); returns false for `off`. */
40332
+ const writeJson = (json, out, document) => gen(function* () {
40333
+ if (json.kind === "off") return false;
40334
+ if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
40335
+ else yield* writeSelection(out, document, json.path);
40336
+ return true;
40337
+ });
40338
+ const runHistoryListCommand = (json, out) => gen(function* () {
40339
+ const entries = yield* historyEntries;
40340
+ if (yield* writeJson(json, out, entries)) return;
40341
+ if (entries.length === 0) {
40342
+ out.write("no history refs; run gtd history fetch | sh to pull them\n");
40343
+ return;
40344
+ }
40345
+ out.write(entries.map((e) => `${seven(e.entry)} ${e.branch ?? "-"} ${seven(e.tip)} ${e.date} ${e.subject}\n`).join(""));
40346
+ });
40347
+ const runHistoryShowCommand = (ref, json, out) => gen(function* () {
40348
+ const entry = yield* resolveHistoryRef(ref, yield* historyEntries);
40349
+ const trail = yield* historyTrail(entry.tip);
40350
+ const document = {
40351
+ ref: entry.ref,
40352
+ entry: entry.entry,
40353
+ branch: entry.branch,
40354
+ ...trail
40355
+ };
40356
+ if (yield* writeJson(json, out, document)) return;
40357
+ if (trail.versionWarning !== void 0) yield* (yield* Narrator).warn(trail.versionWarning);
40358
+ const lines = [`${entry.ref} (branch ${entry.branch ?? "-"}, tip ${seven(entry.tip)})`];
40359
+ for (const step of trail.steps) {
40360
+ lines.push(`${seven(step.hash)} ${step.subject}`);
40361
+ for (const v of step.verdicts) lines.push(` judge ${v.id} = ${v.answer} (p=${v.p})`);
40362
+ }
40363
+ lines.push(`ended (${trail.ended})`);
40364
+ out.write(`${lines.join("\n")}\n`);
40365
+ });
40366
+ /** Prints the script and never runs git: gtd's promise that it only decides and prints. */
40367
+ const runHistoryFetchCommand = (remote, out) => sync$1(() => {
40368
+ out.write(`${combinedScript(fetchHistoryRefs(remote), "")}\n`);
40369
+ });
40370
+ //#endregion
40371
+ //#region src/cli/Program.ts
40372
+ init_esm();
40373
+ init_wire();
40374
+ init_steering();
40155
40375
  const selfGtd = () => {
40156
40376
  const script = process.argv[1];
40157
40377
  return script === void 0 ? ["gtd"] : [process.execPath, script];
@@ -40773,6 +40993,9 @@ const dispatchVoidCommand = (command, json, out) => {
40773
40993
  case "judge": return runJudgeCommand(json, out);
40774
40994
  case "judgeAnswer": return runJudgeAnswerCommand(json, out);
40775
40995
  case "judgeRun": return runJudgeRunCommand(command, out);
40996
+ case "historyList": return runHistoryListCommand(json, out);
40997
+ case "historyShow": return runHistoryShowCommand(command.ref, json, out);
40998
+ case "historyFetch": return runHistoryFetchCommand(command.remote, out);
40776
40999
  }
40777
41000
  };
40778
41001
  /**
@@ -40813,13 +41036,13 @@ const FLAGS = [
40813
41036
  name: "--json",
40814
41037
  arity: "optional",
40815
41038
  repeatable: false,
40816
- scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors",
41039
+ scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "historyList" || kind === "historyShow",
40817
41040
  decode: ([raw]) => right(raw ?? ""),
40818
- scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
41041
+ scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd history list`/`gtd history show` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
40819
41042
  valueHint: "<path>",
40820
41043
  help: [
40821
- "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors only) output",
40822
- "structured JSON. Bare --json prints the whole document;",
41044
+ "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
41045
+ "list/gtd history show only) output structured JSON. Bare --json prints the whole document;",
40823
41046
  "--json=<path> (a dotted key path into that document, e.g.",
40824
41047
  "kind, content, session.id) prints just that value: a",
40825
41048
  "scalar raw and unquoted, a boolean as true/false, a list",
@@ -41327,6 +41550,44 @@ const COMMAND_ROWS = [
41327
41550
  "repository. jev and llm exit 1 when they cannot answer",
41328
41551
  "every question, with nothing on stdout"
41329
41552
  ]
41553
+ },
41554
+ {
41555
+ token: "history list",
41556
+ kind: "historyList",
41557
+ arity: "none",
41558
+ details: [
41559
+ "List the finished processes kept behind refs/gtd/history/*,",
41560
+ "newest first: entry, branch, tip, date and subject per line.",
41561
+ "--json prints one array of { ref, entry, branch, tip, date,",
41562
+ "subject }. Writes nothing and never replays; with no refs it",
41563
+ "hints at `gtd history fetch | sh` and still exits 0"
41564
+ ]
41565
+ },
41566
+ {
41567
+ token: "history show",
41568
+ kind: "historyShow",
41569
+ arity: { name: "ref" },
41570
+ details: [
41571
+ "Replay one kept process under today's config and print its",
41572
+ "steps and judge verdicts. <ref> is a full ref, an entry hash",
41573
+ "(or unique prefix) or a branch name. Refuses (exit 1) when a",
41574
+ "config file differs from its content at the ref's tip. A",
41575
+ "version mismatch only warns on stderr. --json prints the",
41576
+ "trail as one document; writes nothing"
41577
+ ]
41578
+ },
41579
+ {
41580
+ token: "history fetch",
41581
+ kind: "historyFetch",
41582
+ arity: {
41583
+ name: "remote",
41584
+ optional: true
41585
+ },
41586
+ details: [
41587
+ "Print a script that fetches every refs/gtd/history/* from",
41588
+ "<remote> (default origin) and reports how many arrived — run",
41589
+ "it with `gtd history fetch | sh`. Never runs git itself"
41590
+ ]
41330
41591
  }
41331
41592
  ];
41332
41593
  const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
@@ -41346,7 +41607,7 @@ const flagHeader = (row) => {
41346
41607
  };
41347
41608
  const renderHelp = () => {
41348
41609
  const commandBlocks = COMMAND_ROWS.map((row) => {
41349
- const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : "variadic" in row.arity ? `${row.token} <${row.arity.name}> [${row.arity.variadic}...]` : `${row.token} <${row.arity.name}>`;
41610
+ const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : "variadic" in row.arity ? `${row.token} <${row.arity.name}> [${row.arity.variadic}...]` : "optional" in row.arity ? `${row.token} [${row.arity.name}]` : `${row.token} <${row.arity.name}>`;
41350
41611
  return renderBlock(header, row.details);
41351
41612
  });
41352
41613
  return `Usage: gtd [command] [options]\n\nCommands:\n${[
@@ -41457,6 +41718,7 @@ const conflictViolation = (present) => {
41457
41718
  const arityError = (cmd, rest, arity) => {
41458
41719
  if (arity === "none") return rest.length > 0 ? `gtd ${cmd}: too many arguments — expected none, got: ${rest.join(", ")}` : void 0;
41459
41720
  if ("names" in arity) return pairArityError(cmd, rest, arity.names);
41721
+ if ("optional" in arity) return rest.length > 1 ? `gtd ${cmd}: too many arguments — expected at most one ${arity.name}, got: ${rest.join(", ")}` : void 0;
41460
41722
  if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
41461
41723
  if (!("variadic" in arity) && rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
41462
41724
  };
@@ -41507,7 +41769,9 @@ const parseArgv = (argv) => {
41507
41769
  kind: "output",
41508
41770
  stdout: renderHelp()
41509
41771
  };
41510
- const row = first === void 0 ? void 0 : commandByToken(first);
41772
+ const isHistory = first === "history";
41773
+ const row = first === void 0 ? void 0 : commandByToken(isHistory ? `history ${positionals[1] ?? ""}` : first);
41774
+ if (isHistory && row === void 0) return usagePlan("gtd history: expected a subcommand — list, show <ref> or fetch [remote]", jsonSeen);
41511
41775
  const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
41512
41776
  const selectsStart = workflowPresent && first === void 0;
41513
41777
  const judgeRun = row?.kind === "judge" && positionals[1] === "run";
@@ -41518,11 +41782,11 @@ const parseArgv = (argv) => {
41518
41782
  if (first === void 0) return usagePlan("gtd: missing command — gtd decides and prints, a driver executes; run `gtd install`, or copy one from https://github.com/pmelab/gtd/blob/main/docs/driver.md's \"A complete minimal driver\" section and run that, or see usage above (`gtd --help`)", jsonSeen, renderHelp());
41519
41783
  return usagePlan(`unknown command '${first}'`, jsonSeen);
41520
41784
  }
41521
- const restPositionals = first === void 0 ? positionals : positionals.slice(judgeAnswer || judgeRun ? 2 : 1);
41785
+ const restPositionals = first === void 0 ? positionals : positionals.slice(judgeAnswer || judgeRun || isHistory ? 2 : 1);
41522
41786
  if (kind === "start" && first === void 0) {
41523
41787
  if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
41524
41788
  } else {
41525
- const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(first, restPositionals, row.arity);
41789
+ const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(isHistory ? row.token : first, restPositionals, row.arity);
41526
41790
  if (arityMsg !== void 0) return usagePlan(arityMsg, jsonSeen);
41527
41791
  }
41528
41792
  const violation = scopeViolation(kind, present);
@@ -41611,6 +41875,24 @@ const parseArgv = (argv) => {
41611
41875
  verbose
41612
41876
  };
41613
41877
  }
41878
+ if (kind === "historyShow") return {
41879
+ kind: "command",
41880
+ command: {
41881
+ kind: "historyShow",
41882
+ ref: restPositionals[0]
41883
+ },
41884
+ json,
41885
+ verbose
41886
+ };
41887
+ if (kind === "historyFetch") return {
41888
+ kind: "command",
41889
+ command: {
41890
+ kind: "historyFetch",
41891
+ remote: restPositionals[0] ?? "origin"
41892
+ },
41893
+ json,
41894
+ verbose
41895
+ };
41614
41896
  if (kind === "uncheck") return {
41615
41897
  kind: "command",
41616
41898
  command: {