@pmelab/gtd 27.7.1 → 27.9.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "27.7.1",
3
+ "version": "27.9.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -649,7 +649,7 @@ gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
649
649
 
650
650
  | Import | What it is |
651
651
  | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
652
- | `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), workflow factories, `defineConfig` and `FLOWS_PROTOCOL`. |
652
+ | `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), `scope` and `slice` (named spans of a process), workflow factories, `defineConfig` and `FLOWS_PROTOCOL`. |
653
653
  | `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
654
654
  | `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`) and the review-note readers. |
655
655
  | `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. |
@@ -662,10 +662,15 @@ install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
662
662
  `@pmelab/gtd/*` imports to its own copy.
663
663
 
664
664
  The `gtd` bin's commands — `next`, `land`, `abandon`, `restore`, `validate`,
665
- `check`, `uncheck`, `install`, `summary`, `base`, `door`, `doors`, `version`,
666
- `help`, `history list`, `history show`, `history fetch`, `strays`, and the tools
667
- `ui`, `lsp`, `loop` and `judge` (`judge answer`, `judge run`) — are listed in
668
- the [CLI reference](https://github.com/pmelab/gtd/blob/main/docs/cli.md).
665
+ `check`, `uncheck`, `install`, `summary`, `base`, `door`, `doors`, `slices`,
666
+ `version`, `help`, `history list`, `history show`, `history fetch`, `strays`,
667
+ and the tools `ui`, `lsp`, `loop`, `judge` (`judge answer`, `judge run`) — are
668
+ listed in the
669
+ [CLI reference](https://github.com/pmelab/gtd/blob/main/docs/cli.md).
670
+ `gtd slices` lists the spans a flow's `slice()` calls mark. It refuses (exit 1)
671
+ when HEAD neither closes nor sits inside a process, or when today's config
672
+ disagrees with history about the process; a version mismatch only warns on
673
+ stderr.
669
674
 
670
675
  ## License
671
676
 
@@ -475,4 +475,17 @@ const compile = (glob) => {
475
475
  };
476
476
  const globMatches = (path, glob) => compile(glob).test(path);
477
477
  //#endregion
478
- export { EXIT_SIGTERM as C, UNATTRIBUTED_MODEL as D, ACCESS_REFUSAL as E, WIRE_SCHEMA as O, EXIT_SIGINT as S, EXIT_USAGE_ERROR as T, demandOf as _, landFields as a, EXIT_OK as b, renderLandJson as c, renderBeatPlain as d, beatDocument as f, beatKindOf as g, BEAT_KINDS as h, judgeJson as i, renderFormat as k, renderLandPlain as l, statusOf as m, resolveSession as n, landProseText as o, renderBeatJson as p, selectPath as r, noopText as s, globMatches as t, DEMAND_BRIEFING as u, stallDiagnosis as v, EXIT_STRAYS_FOUND as w, EXIT_RUNTIME_ERROR as x, EXIT_CODES as y };
478
+ //#region src/wire/Slices.ts
479
+ /** The ONLY place `gtd slices --json`'s shape is built; copies exactly the four fields, in order. */
480
+ const slicesDocument = (slices) => ({
481
+ schema: 1,
482
+ slices: slices.map(({ name, base, tip, closed }) => ({
483
+ name,
484
+ base,
485
+ tip,
486
+ closed
487
+ }))
488
+ });
489
+ const renderSlicesJson = (document) => JSON.stringify(document) + "\n";
490
+ //#endregion
491
+ export { WIRE_SCHEMA as A, EXIT_RUNTIME_ERROR as C, EXIT_USAGE_ERROR as D, EXIT_STRAYS_FOUND as E, ACCESS_REFUSAL as O, EXIT_OK as S, EXIT_SIGTERM as T, BEAT_KINDS as _, selectPath as a, stallDiagnosis as b, landProseText as c, renderLandPlain as d, DEMAND_BRIEFING as f, statusOf as g, renderBeatJson as h, resolveSession as i, renderFormat as j, UNATTRIBUTED_MODEL as k, noopText as l, beatDocument as m, slicesDocument as n, judgeJson as o, renderBeatPlain as p, globMatches as r, landFields as s, renderSlicesJson as t, renderLandJson as u, beatKindOf as v, EXIT_SIGINT as w, EXIT_CODES as x, demandOf as y };
package/dist/flows.mjs CHANGED
@@ -86,6 +86,20 @@ const scope = async (scope, fn) => {
86
86
  context.popScope();
87
87
  }
88
88
  };
89
+ /**
90
+ * Mark the span `fn` covers as one named slice of the process. Lands no
91
+ * commit, prefixes no step name, opens no memory scope.
92
+ */
93
+ const slice = async (name, fn) => {
94
+ const context = ctx();
95
+ if (typeof context.enterSlice !== "function") return fn();
96
+ context.enterSlice(name);
97
+ try {
98
+ return await fn();
99
+ } finally {
100
+ context.leaveSlice?.();
101
+ }
102
+ };
89
103
  const read = (path) => ctx().read(path);
90
104
  /** Every path in the tree matching `pattern` (`*` stays within a segment, `**` crosses them). */
91
105
  const glob = (pattern) => ctx().glob(pattern);
@@ -652,10 +666,11 @@ var flows_exports = /* @__PURE__ */ __exportAll({
652
666
  sections: () => sections,
653
667
  showScript: () => showScript,
654
668
  skillsFor: () => skillsFor,
669
+ slice: () => slice,
655
670
  start: () => start,
656
671
  threads: () => threads,
657
672
  vars: () => vars,
658
673
  wrote: () => wrote
659
674
  });
660
675
  //#endregion
661
- export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, displayPath, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, judgeDiffShape, kept, mark, matches, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, seamGate, sectionBodies, sections, showScript, skillsFor, start, flows_exports as t, threads, vars, wrote };
676
+ export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, displayPath, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, judgeDiffShape, kept, mark, matches, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, seamGate, sectionBodies, sections, showScript, skillsFor, slice, start, flows_exports as t, threads, vars, wrote };
@@ -21724,6 +21724,20 @@ const scope = async (scope, fn) => {
21724
21724
  context.popScope();
21725
21725
  }
21726
21726
  };
21727
+ /**
21728
+ * Mark the span `fn` covers as one named slice of the process. Lands no
21729
+ * commit, prefixes no step name, opens no memory scope.
21730
+ */
21731
+ const slice = async (name, fn) => {
21732
+ const context = ctx();
21733
+ if (typeof context.enterSlice !== "function") return fn();
21734
+ context.enterSlice(name);
21735
+ try {
21736
+ return await fn();
21737
+ } finally {
21738
+ context.leaveSlice?.();
21739
+ }
21740
+ };
21727
21741
  const read = (path) => ctx().read(path);
21728
21742
  /** Every path in the tree matching `pattern` (`*` stays within a segment, `**` crosses them). */
21729
21743
  const glob = (pattern) => ctx().glob(pattern);
@@ -22290,6 +22304,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
22290
22304
  sections: () => sections,
22291
22305
  showScript: () => showScript,
22292
22306
  skillsFor: () => skillsFor,
22307
+ slice: () => slice,
22293
22308
  start: () => start,
22294
22309
  threads: () => threads,
22295
22310
  vars: () => vars,
@@ -37637,6 +37652,22 @@ var init_Glob = __esmMin((() => {
37637
37652
  globMatches = (path, glob) => compile(glob).test(path);
37638
37653
  }));
37639
37654
  //#endregion
37655
+ //#region src/wire/Slices.ts
37656
+ var slicesDocument, renderSlicesJson;
37657
+ var init_Slices = __esmMin((() => {
37658
+ init_constants();
37659
+ slicesDocument = (slices) => ({
37660
+ schema: 1,
37661
+ slices: slices.map(({ name, base, tip, closed }) => ({
37662
+ name,
37663
+ base,
37664
+ tip,
37665
+ closed
37666
+ }))
37667
+ });
37668
+ renderSlicesJson = (document) => JSON.stringify(document) + "\n";
37669
+ }));
37670
+ //#endregion
37640
37671
  //#region src/wire/index.ts
37641
37672
  var init_wire = __esmMin((() => {
37642
37673
  init_constants();
@@ -37651,6 +37682,7 @@ var init_wire = __esmMin((() => {
37651
37682
  init_Select();
37652
37683
  init_Sessions();
37653
37684
  init_Glob();
37685
+ init_Slices();
37654
37686
  }));
37655
37687
  //#endregion
37656
37688
  //#region src/replay/RefName.ts
@@ -37670,6 +37702,32 @@ const refNameFault = (name) => {
37670
37702
  }
37671
37703
  };
37672
37704
  //#endregion
37705
+ //#region src/replay/Slices.ts
37706
+ const IMPLICIT_SLICE = "main";
37707
+ /** Same shape as mark()'s name rule; kept local so @pmelab/gtd/flows publishes no regex. */
37708
+ const SLICE_NAME = /^[a-z][a-z0-9-]*$/;
37709
+ /**
37710
+ * Folds boundaries into ranges: each kept span's base is the previous kept
37711
+ * span's `leftAt`, so a gap before a span belongs to it, and the last span
37712
+ * runs to the episode tip. A closed span that landed nothing is dropped.
37713
+ */
37714
+ const sliceRanges = (spans, episode) => {
37715
+ if (spans.some((s) => s.enteredAt === "" || s.leftAt === "")) throw new Error("sliceRanges: a pending turn's \"\" hash is not a commit");
37716
+ const kept = spans.filter((s) => s.leftAt === void 0 || s.enteredAt !== s.leftAt);
37717
+ if (kept.length === 0) return [{
37718
+ name: IMPLICIT_SLICE,
37719
+ base: episode.base,
37720
+ tip: episode.tip,
37721
+ open: true
37722
+ }];
37723
+ return kept.map((s, i) => ({
37724
+ name: s.name,
37725
+ base: i === 0 ? episode.base : kept[i - 1].leftAt ?? episode.base,
37726
+ tip: i === kept.length - 1 ? episode.tip : s.leftAt ?? episode.tip,
37727
+ open: i === kept.length - 1
37728
+ }));
37729
+ };
37730
+ //#endregion
37673
37731
  //#region src/replay/Tree.ts
37674
37732
  const treeFromRecord = (files) => ({
37675
37733
  paths: () => Object.keys(files).sort(),
@@ -37826,7 +37884,7 @@ const TRAILER_READERS = {
37826
37884
  if (value !== "") into.branch ??= value;
37827
37885
  },
37828
37886
  "Gtd-Version": (value, into) => {
37829
- if (value !== "") into.version ??= value;
37887
+ if (value !== "" && !/[\u0000-\u001f\u007f-\u009f]/.test(value)) into.version ??= value;
37830
37888
  },
37831
37889
  "Gtd-Kept": (value, into) => {
37832
37890
  if (HASH_RE.test(value)) into.kept ??= value;
@@ -38017,6 +38075,7 @@ const replay = async (input) => {
38017
38075
  const keptTips = /* @__PURE__ */ new Map();
38018
38076
  let landed;
38019
38077
  const marks = [];
38078
+ const slices = [];
38020
38079
  let outcome;
38021
38080
  let signal;
38022
38081
  const settled = new Promise((resolve) => {
@@ -38223,7 +38282,8 @@ const replay = async (input) => {
38223
38282
  rest: step,
38224
38283
  trace,
38225
38284
  landed,
38226
- marks
38285
+ marks,
38286
+ slices: slices.map((s) => ({ ...s }))
38227
38287
  });
38228
38288
  return landPending(step, input.pending);
38229
38289
  };
@@ -38262,7 +38322,8 @@ const replay = async (input) => {
38262
38322
  via,
38263
38323
  trace,
38264
38324
  landed,
38265
- marks
38325
+ marks,
38326
+ slices: slices.map((s) => ({ ...s }))
38266
38327
  };
38267
38328
  const changesSince = (hash) => {
38268
38329
  const tree = treeAt(hash);
@@ -38287,6 +38348,31 @@ const replay = async (input) => {
38287
38348
  scopes.pop();
38288
38349
  scopeCallIds.pop();
38289
38350
  },
38351
+ enterSlice: (name) => {
38352
+ if (outcome !== void 0) return;
38353
+ const fail = (message) => {
38354
+ finish({
38355
+ kind: "failed",
38356
+ message
38357
+ });
38358
+ throw new Stop();
38359
+ };
38360
+ if (typeof name !== "string" || !SLICE_NAME.test(name)) fail(`gtd: slice "${name}": name must match ${SLICE_NAME}`);
38361
+ if (name === "main") fail(`gtd: slice "${IMPLICIT_SLICE}" is reserved for the implicit slice`);
38362
+ const open = slices.find((s) => s.leftAt === void 0);
38363
+ if (open !== void 0) fail(`gtd: slice "${name}" is entered inside slice "${open.name}" — slices do not nest`);
38364
+ if (slices.some((s) => s.name === name)) fail(`gtd: slice "${name}" is entered twice in this episode — a loop must sit inside its slice`);
38365
+ slices.push({
38366
+ name,
38367
+ enteredAt: position.hash,
38368
+ leftAt: void 0
38369
+ });
38370
+ },
38371
+ leaveSlice: () => {
38372
+ if (outcome !== void 0) return;
38373
+ const open = slices.find((s) => s.leftAt === void 0);
38374
+ if (open !== void 0) open.leftAt = position.hash;
38375
+ },
38290
38376
  read: (path) => position.tree.read(path),
38291
38377
  glob: (pattern) => position.tree.paths().filter((path) => globMatches(path, pattern)),
38292
38378
  changes: () => changesBetween(previousPosition.tree, position.tree),
@@ -39678,22 +39764,14 @@ const runAtTip = (git, def, tip) => gen(function* () {
39678
39764
  if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
39679
39765
  return yield* runOf([...before, ...closing], { ...location }, tip.hash);
39680
39766
  });
39681
- /** The process HEAD closes or sits inside — `gtd summary`'s run. */
39682
- const summaryRun = gen(function* () {
39683
- const git = yield* GitService;
39684
- const def = (yield* (yield* ConfigService).load).workflow;
39767
+ const runAtHead = (git, def) => gen(function* () {
39685
39768
  return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
39686
39769
  });
39687
- /** The process whose finishing commit is `tip` — reads from `tip`'s own ancestry, so it works when no branch reaches it. */
39688
- const closedRunAt = (tip) => gen(function* () {
39770
+ /** The process HEAD closes or sits inside — `gtd summary`'s run. */
39771
+ const summaryRun = gen(function* () {
39689
39772
  const git = yield* GitService;
39690
39773
  const def = (yield* (yield* ConfigService).load).workflow;
39691
- const hash = yield* git.resolveRef(tip);
39692
- const subject = yield* git.lastCommitSubject(hash);
39693
- return yield* runAtTip(git, def, {
39694
- hash,
39695
- subject
39696
- });
39774
+ return yield* runAtHead(git, def);
39697
39775
  });
39698
39776
  const commitTree = (workspace, hash) => {
39699
39777
  let entries;
@@ -39976,6 +40054,10 @@ const pendingChanges = (git, withIndex) => gen(function* () {
39976
40054
  const seen = new Set(worktree.map((e) => e.path));
39977
40055
  return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))];
39978
40056
  });
40057
+ const sliceRangesOf = (slices, run) => sliceRanges(slices, {
40058
+ base: run.startParentHash,
40059
+ tip: run.trace.at(-1)?.hash ?? run.startParentHash
40060
+ });
39979
40061
  /** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
39980
40062
  const restAt = (ref, startOverrides = {}) => gen(function* () {
39981
40063
  const git = yield* GitService;
@@ -40023,6 +40105,49 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
40023
40105
  });
40024
40106
  const currentRest = restAt(void 0);
40025
40107
  /**
40108
+ * The slices of the process HEAD closes or sits inside, current episode only,
40109
+ * stack order. Reads like `historyTrail`, never `restAt`: it narrates and
40110
+ * writes nothing, and returns a version warning for a closed run.
40111
+ */
40112
+ const processSlices = gen(function* () {
40113
+ const git = yield* GitService;
40114
+ const config = yield* (yield* ConfigService).load;
40115
+ const workspace = yield* Workspace;
40116
+ const host = yield* Host;
40117
+ const run = yield* runAtHead(git, config.workflow);
40118
+ if (run.trace.length === 0) return yield* fail$7(/* @__PURE__ */ new Error("gtd slices: refused — HEAD neither closes nor sits inside a process"));
40119
+ const closing = run.closingHash;
40120
+ let outcome;
40121
+ let versionWarning;
40122
+ if (closing !== void 0) {
40123
+ const replayed = yield* replayFinishedProcess(config, run, closing, {
40124
+ command: "gtd slices",
40125
+ configReason: "HEAD's process replays under today's config; revert or stash the edit to list its slices",
40126
+ notEnded: (o) => slicesDivergence(run, o)
40127
+ });
40128
+ outcome = replayed.outcome;
40129
+ versionWarning = replayed.versionWarning;
40130
+ } else {
40131
+ const { setup } = yield* setupFor(config, run, workspace, host);
40132
+ outcome = yield* promise(() => replayFor(setup));
40133
+ if (outcome.kind !== "rest") return yield* fail$7(slicesDivergence(run, outcome));
40134
+ }
40135
+ return {
40136
+ slices: sliceRangesOf(outcome.slices, run).map(({ name, base, tip, open }) => ({
40137
+ name,
40138
+ base,
40139
+ tip,
40140
+ closed: closing !== void 0 || !open
40141
+ })),
40142
+ ...versionWarning !== void 0 ? { versionWarning } : {}
40143
+ };
40144
+ });
40145
+ /** Every `gtd slices` refusal reads `gtd slices: refused — <reason>`; replay's own `gtd: ` lead is dropped. */
40146
+ const slicesDivergence = (run, outcome) => {
40147
+ const reason = outcome.kind === "rest" && run.closingHash !== void 0 ? `${run.closingHash.slice(0, 7)} closes a process that does not end under today's config` : replayError(outcome)?.message.replace(/^gtd: /, "") ?? "replay failed";
40148
+ return /* @__PURE__ */ new Error(`gtd slices: refused — ${reason}`);
40149
+ };
40150
+ /**
40026
40151
  * Whether `gtd --workflow <name>` can start a process here: the workflow must
40027
40152
  * reach a step from the tree the opening commit would capture. That step is
40028
40153
  * what the opening commit's subject names.
@@ -40256,7 +40381,7 @@ const inRepoConfigPaths = (root, files, recorded) => {
40256
40381
  for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
40257
40382
  return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
40258
40383
  };
40259
- 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`);
40384
+ const configRefusal = (command, tip, paths, reason = "A history ref replays under today's config, so it is unreadable after the config changes") => /* @__PURE__ */ new Error(`${command}: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. ${reason}`);
40260
40385
  const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
40261
40386
  const differing = [];
40262
40387
  for (const path of inRepoConfigPaths(root, Object.keys(config.fingerprint), run.pinnedConfig?.files ?? {})) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
@@ -40287,22 +40412,19 @@ const versionWarningFor = (recorded) => recorded === GTD_VERSION ? void 0 : `gtd
40287
40412
  const historyTrail = (tip) => gen(function* () {
40288
40413
  const git = yield* GitService;
40289
40414
  const config = yield* (yield* ConfigService).load;
40290
- const workspace = yield* Workspace;
40291
- const host = yield* Host;
40292
40415
  const hash = yield* git.resolveRef(tip);
40293
- const run = yield* closedRunAt(hash);
40294
- const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
40295
- if (differing.length > 0) return yield* fail$7(configRefusal(hash, differing));
40296
- const { setup } = yield* setupFor(config, run, workspace, host);
40297
- yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
40298
- const outcome = yield* promise(() => replayFor(setup));
40299
- if (outcome.kind !== "ended") return yield* fail$7(notClosedError(hash, outcome));
40300
- const steps = historySteps(run);
40301
- const recorded = parseCommitMessage(yield* git.lastCommitMessage(hash)).version;
40302
- const versionWarning = versionWarningFor(recorded);
40416
+ const subject = yield* git.lastCommitSubject(hash);
40417
+ const run = yield* runAtTip(git, config.workflow, {
40418
+ hash,
40419
+ subject
40420
+ });
40421
+ const { outcome, recorded, versionWarning } = yield* replayFinishedProcess(config, run, hash, {
40422
+ command: "gtd history",
40423
+ notEnded: (o) => notClosedError(hash, o)
40424
+ });
40303
40425
  return {
40304
40426
  tip: hash,
40305
- steps,
40427
+ steps: historySteps(run),
40306
40428
  ended: outcome.via,
40307
40429
  version: {
40308
40430
  recorded,
@@ -40311,6 +40433,24 @@ const historyTrail = (tip) => gen(function* () {
40311
40433
  ...versionWarning !== void 0 ? { versionWarning } : {}
40312
40434
  };
40313
40435
  });
40436
+ /** Refuses on config drift before replaying, so a stale config never runs the flow. */
40437
+ const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
40438
+ const git = yield* GitService;
40439
+ const workspace = yield* Workspace;
40440
+ const host = yield* Host;
40441
+ const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
40442
+ if (differing.length > 0) return yield* fail$7(configRefusal(how.command, hash, differing, how.configReason));
40443
+ const { setup } = yield* setupFor(config, run, workspace, host);
40444
+ const outcome = yield* promise(() => replayFor(setup));
40445
+ if (outcome.kind !== "ended") return yield* fail$7(how.notEnded(outcome));
40446
+ const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
40447
+ const versionWarning = versionWarningFor(recorded);
40448
+ return {
40449
+ outcome,
40450
+ recorded,
40451
+ ...versionWarning !== void 0 ? { versionWarning } : {}
40452
+ };
40453
+ });
40314
40454
  //#endregion
40315
40455
  //#region src/emit/GitScript.ts
40316
40456
  const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
@@ -41183,6 +41323,17 @@ const runDoorsCommand = (json, out) => gen(function* () {
41183
41323
  if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
41184
41324
  else yield* writeSelection(out, document, json.path);
41185
41325
  });
41326
+ const runSlicesCommand = (json, out) => gen(function* () {
41327
+ const { slices, versionWarning } = yield* processSlices;
41328
+ if (versionWarning !== void 0) yield* (yield* Narrator).warn(versionWarning);
41329
+ if (json.kind === "off") {
41330
+ out.write(slices.map((s) => `${s.name} ${s.base}..${s.tip}\n`).join(""));
41331
+ return;
41332
+ }
41333
+ const document = slicesDocument(slices);
41334
+ if (json.kind === "document") out.write(renderSlicesJson(document));
41335
+ else yield* writeSelection(out, document, json.path);
41336
+ });
41186
41337
  /**
41187
41338
  * Ends the process underway without completing it. Nothing is discarded: a
41188
41339
  * `git reset --mixed` to the process's start commit leaves every turn
@@ -41518,6 +41669,7 @@ const dispatchVoidCommand = (command, json, out) => {
41518
41669
  case "start": return runStartCommand(command.actor, command.workflow, command.vars, out, command.label);
41519
41670
  case "door": return runDoorCommand(command.name, command.args, out);
41520
41671
  case "doors": return runDoorsCommand(json, out);
41672
+ case "slices": return runSlicesCommand(json, out);
41521
41673
  case "abandon": return runAbandonCommand(out);
41522
41674
  case "restore": return runRestoreCommand(out);
41523
41675
  case "next": return runNextCommand(json, out);
@@ -41576,12 +41728,12 @@ const FLAGS = [
41576
41728
  name: "--json",
41577
41729
  arity: "optional",
41578
41730
  repeatable: false,
41579
- scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "historyList" || kind === "historyShow" || kind === "strays",
41731
+ scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "slices" || kind === "historyList" || kind === "historyShow" || kind === "strays",
41580
41732
  decode: ([raw]) => right(raw ?? ""),
41581
- scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd history list`/`gtd history show`/`gtd strays` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
41733
+ scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd slices`/`gtd history list`/`gtd history show`/`gtd strays` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
41582
41734
  valueHint: "<path>",
41583
41735
  help: [
41584
- "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
41736
+ "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
41585
41737
  "list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
41586
41738
  "--json=<path> (a dotted key path into that document, e.g.",
41587
41739
  "kind, content, session.id) prints just that value: a",
@@ -42051,6 +42203,25 @@ const COMMAND_ROWS = [
42051
42203
  "value off it. Writes nothing"
42052
42204
  ]
42053
42205
  },
42206
+ {
42207
+ token: "slices",
42208
+ kind: "slices",
42209
+ arity: "none",
42210
+ details: [
42211
+ "List the slices of the process HEAD closes or sits inside,",
42212
+ "first slice first: one `name base..tip` line each (full",
42213
+ "hashes; plain output carries no promise). --json prints",
42214
+ "{ schema: 1, slices: [{ name, base, tip, closed }] };",
42215
+ "--json=<path> reads one value off it. Writes nothing: no",
42216
+ "git, no state transition, no session identity. When HEAD",
42217
+ "closes a process, every slice is listed as closed; a gtd",
42218
+ "version mismatch only warns on stderr. Refuses (exit 1)",
42219
+ "when HEAD neither closes nor sits inside a process, or when",
42220
+ "today's config disagrees with history about the process:",
42221
+ "HEAD closes it and its config files differ from the working",
42222
+ "tree, or the replay diverged"
42223
+ ]
42224
+ },
42054
42225
  {
42055
42226
  token: "judge",
42056
42227
  kind: "judge",