@pmelab/gtd 27.8.0 → 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.8.0",
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
@@ -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 };
@@ -37652,6 +37652,22 @@ var init_Glob = __esmMin((() => {
37652
37652
  globMatches = (path, glob) => compile(glob).test(path);
37653
37653
  }));
37654
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
37655
37671
  //#region src/wire/index.ts
37656
37672
  var init_wire = __esmMin((() => {
37657
37673
  init_constants();
@@ -37666,6 +37682,7 @@ var init_wire = __esmMin((() => {
37666
37682
  init_Select();
37667
37683
  init_Sessions();
37668
37684
  init_Glob();
37685
+ init_Slices();
37669
37686
  }));
37670
37687
  //#endregion
37671
37688
  //#region src/replay/RefName.ts
@@ -37867,7 +37884,7 @@ const TRAILER_READERS = {
37867
37884
  if (value !== "") into.branch ??= value;
37868
37885
  },
37869
37886
  "Gtd-Version": (value, into) => {
37870
- if (value !== "") into.version ??= value;
37887
+ if (value !== "" && !/[\u0000-\u001f\u007f-\u009f]/.test(value)) into.version ??= value;
37871
37888
  },
37872
37889
  "Gtd-Kept": (value, into) => {
37873
37890
  if (HASH_RE.test(value)) into.kept ??= value;
@@ -38305,7 +38322,8 @@ const replay = async (input) => {
38305
38322
  via,
38306
38323
  trace,
38307
38324
  landed,
38308
- marks
38325
+ marks,
38326
+ slices: slices.map((s) => ({ ...s }))
38309
38327
  };
38310
38328
  const changesSince = (hash) => {
38311
38329
  const tree = treeAt(hash);
@@ -39746,22 +39764,14 @@ const runAtTip = (git, def, tip) => gen(function* () {
39746
39764
  if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
39747
39765
  return yield* runOf([...before, ...closing], { ...location }, tip.hash);
39748
39766
  });
39767
+ const runAtHead = (git, def) => gen(function* () {
39768
+ return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
39769
+ });
39749
39770
  /** The process HEAD closes or sits inside — `gtd summary`'s run. */
39750
39771
  const summaryRun = gen(function* () {
39751
39772
  const git = yield* GitService;
39752
39773
  const def = (yield* (yield* ConfigService).load).workflow;
39753
- return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
39754
- });
39755
- /** The process whose finishing commit is `tip` — reads from `tip`'s own ancestry, so it works when no branch reaches it. */
39756
- const closedRunAt = (tip) => gen(function* () {
39757
- const git = yield* GitService;
39758
- const def = (yield* (yield* ConfigService).load).workflow;
39759
- const hash = yield* git.resolveRef(tip);
39760
- const subject = yield* git.lastCommitSubject(hash);
39761
- return yield* runAtTip(git, def, {
39762
- hash,
39763
- subject
39764
- });
39774
+ return yield* runAtHead(git, def);
39765
39775
  });
39766
39776
  const commitTree = (workspace, hash) => {
39767
39777
  let entries;
@@ -40090,12 +40100,54 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
40090
40100
  total: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
40091
40101
  byModel: costByModel(run.costEntries)
40092
40102
  },
40093
- setup,
40094
- slices: sliceRangesOf(outcome.slices, run)
40103
+ setup
40095
40104
  };
40096
40105
  });
40097
40106
  const currentRest = restAt(void 0);
40098
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
+ /**
40099
40151
  * Whether `gtd --workflow <name>` can start a process here: the workflow must
40100
40152
  * reach a step from the tree the opening commit would capture. That step is
40101
40153
  * what the opening commit's subject names.
@@ -40329,7 +40381,7 @@ const inRepoConfigPaths = (root, files, recorded) => {
40329
40381
  for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
40330
40382
  return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
40331
40383
  };
40332
- 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}`);
40333
40385
  const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
40334
40386
  const differing = [];
40335
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);
@@ -40360,22 +40412,19 @@ const versionWarningFor = (recorded) => recorded === GTD_VERSION ? void 0 : `gtd
40360
40412
  const historyTrail = (tip) => gen(function* () {
40361
40413
  const git = yield* GitService;
40362
40414
  const config = yield* (yield* ConfigService).load;
40363
- const workspace = yield* Workspace;
40364
- const host = yield* Host;
40365
40415
  const hash = yield* git.resolveRef(tip);
40366
- const run = yield* closedRunAt(hash);
40367
- const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
40368
- if (differing.length > 0) return yield* fail$7(configRefusal(hash, differing));
40369
- const { setup } = yield* setupFor(config, run, workspace, host);
40370
- yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
40371
- const outcome = yield* promise(() => replayFor(setup));
40372
- if (outcome.kind !== "ended") return yield* fail$7(notClosedError(hash, outcome));
40373
- const steps = historySteps(run);
40374
- const recorded = parseCommitMessage(yield* git.lastCommitMessage(hash)).version;
40375
- 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
+ });
40376
40425
  return {
40377
40426
  tip: hash,
40378
- steps,
40427
+ steps: historySteps(run),
40379
40428
  ended: outcome.via,
40380
40429
  version: {
40381
40430
  recorded,
@@ -40384,6 +40433,24 @@ const historyTrail = (tip) => gen(function* () {
40384
40433
  ...versionWarning !== void 0 ? { versionWarning } : {}
40385
40434
  };
40386
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
+ });
40387
40454
  //#endregion
40388
40455
  //#region src/emit/GitScript.ts
40389
40456
  const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
@@ -41256,6 +41323,17 @@ const runDoorsCommand = (json, out) => gen(function* () {
41256
41323
  if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
41257
41324
  else yield* writeSelection(out, document, json.path);
41258
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
+ });
41259
41337
  /**
41260
41338
  * Ends the process underway without completing it. Nothing is discarded: a
41261
41339
  * `git reset --mixed` to the process's start commit leaves every turn
@@ -41591,6 +41669,7 @@ const dispatchVoidCommand = (command, json, out) => {
41591
41669
  case "start": return runStartCommand(command.actor, command.workflow, command.vars, out, command.label);
41592
41670
  case "door": return runDoorCommand(command.name, command.args, out);
41593
41671
  case "doors": return runDoorsCommand(json, out);
41672
+ case "slices": return runSlicesCommand(json, out);
41594
41673
  case "abandon": return runAbandonCommand(out);
41595
41674
  case "restore": return runRestoreCommand(out);
41596
41675
  case "next": return runNextCommand(json, out);
@@ -41649,12 +41728,12 @@ const FLAGS = [
41649
41728
  name: "--json",
41650
41729
  arity: "optional",
41651
41730
  repeatable: false,
41652
- 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",
41653
41732
  decode: ([raw]) => right(raw ?? ""),
41654
- 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",
41655
41734
  valueHint: "<path>",
41656
41735
  help: [
41657
- "(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",
41658
41737
  "list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
41659
41738
  "--json=<path> (a dotted key path into that document, e.g.",
41660
41739
  "kind, content, session.id) prints just that value: a",
@@ -42124,6 +42203,25 @@ const COMMAND_ROWS = [
42124
42203
  "value off it. Writes nothing"
42125
42204
  ]
42126
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
+ },
42127
42225
  {
42128
42226
  token: "judge",
42129
42227
  kind: "judge",
package/dist/testkit.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports } from "./flows.mjs";
2
- import { E as ACCESS_REFUSAL, _ as demandOf, a as landFields, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, i as judgeJson, k as renderFormat, l as renderLandPlain, m as statusOf, n as resolveSession, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches } from "./chunks/wire-C12dahrH.mjs";
2
+ import { O as ACCESS_REFUSAL, a as selectPath, d as renderLandPlain, g as statusOf, h as renderBeatJson, i as resolveSession, j as renderFormat, l as noopText, m as beatDocument, n as slicesDocument, o as judgeJson, p as renderBeatPlain, r as globMatches, s as landFields, t as renderSlicesJson, u as renderLandJson, v as beatKindOf, y as demandOf } from "./chunks/wire-D3Rc5r22.mjs";
3
3
  import { C as headingSections, S as headingSectionBodies, a as unansweredQuestions, b as parseThreadsWithFindings, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, w as formatFinding } from "./chunks/steering-DVILanVf.mjs";
4
4
  import { $ as logWarning, $i as NodeInspectSymbol, $n as map$5, $r as make$19, $t as provide$1, A as catchIf, Ai as add, An as scopeMake, Ar as flatMap$3, At as suspend$5, B as flatMap$4, Bi as getOrElse, Bn as get$3, Br as sync$1, Bt as unsafeMakeLatch, C as all$1, Ci as toArray, Cn as isInterrupted, Cr as exitFail, Ct as sandbox, D as async, Di as GenericTag, Dn as squash, Dr as exitZipRight, Dt as scopedWith$1, E as asVoid$1, Ei as unsafeHead, En as reduceWithContext, Er as exitVoid, Et as scoped$4, F as ensuring$2, Fi as fromIterable, Fn as join$3, Fr as map$3, Ft as tryMap, G as forkIn, Gi as none, Gn as TypeId$10, Gr as whenEffect, Gt as zipLeft, H as fnUntraced, Hi as isSome, Hn as modify$1, Hr as uninterruptible, Ht as when, I as exit, Ii as splitAt, In as sequential, Ir as onInterrupt, It as tryPromise, J as ignore, Ji as left, Jn as all, Jr as DeferredTypeId$1, Jt as spaced$1, K as forkScoped, Ki as some, Kn as Class, Kr as withFiberRuntime, Kt as zipRight$3, L as fail$5, Li as filter$1, Ln as bind, Lr as scopeClose, Lt as try_, M as die$2, Mi as getOption, Mn as _await$2, Mr as interruptAsFiber, Mt as tap, N as dieMessage, Ni as omit, Nn as inheritAll$1, Nr as interruptFiber, Nt as tapErrorCause, O as catchAll$1, Oi as Reference, On as ensuring$3, Or as fiberId, Ot as serviceOption, P as either, Pi as empty$5, Pn as interruptAllAs$1, Pr as interruptible$1, Pt as timeout, Q as isEffect, Qi as BaseProto, Qn as isFailure, Qr as ids, Qt as mergeAll$2, R as failCause$4, Ri as flatMap$5, Rn as bindTo, Rr as succeed$4, Rt as uninterruptible$1, S as addFinalizer$1, Si as takeRight, Sn as isDieType, Sr as exitAs, St as runtime, T as as, Ti as unsafeGet, Tn as map$6, Tr as exitSucceed, Tt as scopeWith, U as forEach$2, Ui as map$8, Un as set$1, Ur as uninterruptibleMask$1, Ut as withFiberRuntime$1, V as flatten$4, Vi as isNone, Vn as make$16, Vr as tap$1, Vt as unsafeMakeSemaphore$1, W as forkDaemon, Wi as match$4, Wn as update$1, Wr as void_$2, Wt as zip$1, X as interruptible, Xi as right, Xn as fail$4, Xr as combine, Xt as tagged, Y as interrupt$4, Yi as match$2, Yn as die, Yr as empty$6, Yt as TaggedError$1, Z as intoDeferred, Zi as pipeArguments, Zn as failCause$3, Zr as combineAll, Zt as fromEffect$5, _ as decodeSync, _i as of, _n as fail$3, _r as deferredIsDone, _t as raceWith, a as Literal, aa as constTrue, ai as size$2, an as updateContext$1, ar as zipRight$2, at as matchCause, b as acquireRelease, bi as reverse, bn as interrupt$3, br as deferredUnsafeDone, bt as runPromise, c as String$, ca as dual, ci as append$1, cn as Scope, cr as asyncInterrupt, ct as negate, d as Union, di as empty$4, dn as close, dr as deferredAwait, dt as orElse, ea as format, ei as none$1, en as provideMerge$1, er as mapBoth, et as makeSemaphore, f as between, fi as filter, fn as extend, fr as deferredComplete, ft as orElseSucceed, gi as map$7, gn as die$1, gr as deferredInterruptWith, gt as race, hi as isEmpty$2, hn as NoSuchElementException, hr as deferredInterrupt, ht as provideService, i as Defect, ia as constFalse, ii as has, in as unsafeRunPromiseExit, ir as zip$2, it as match$5, j as catchTag, ji as get$5, jn as scopeTag, jr as interrupt$2, jt as sync$2, k as catchAllCause, ki as Tag, kn as forEachConcurrentDiscard, kr as fiberRefLocally, kt as succeed$6, l as Struct, la as identity, li as appendAll, ln as addFinalizer, lr as catchAll, lt as onError, m as optional$1, mi as isChunk, mn as make$18, mr as deferredFailCause, mt as provide$2, n as Boolean$, na as equals, ni as make$17, nn as succeed$3, nr as succeed$5, nt as mapBoth$1, o as Never, oa as constUndefined, oi as decode, on as forkIn$1, or as as$1, ot as matchCauseEffect, p as decodeUnknown, pi as fromIterable$1, pn as fork, pr as deferredFail, pt as promise, q as gen, qi as isLeft, qn as spanTag, qr as zipRight$1, qt as zipWith, r as Cause, ra as hasProperty, ri as set$2, rn as unsafeFork, rr as void_$3, rt as mapError, s as Number$, sa as constVoid, si as isFinite, sn as unsafeMakeSemaphore, sr as asVoid, st as matchEffect, t as Array$, ta as toJSON, ti as get$4, tn as scoped$3, tr as match$3, tt as map$4, u as TaggedError, ua as pipe, ui as drop, un as addFinalizerExit, ur as currentContext$1, ut as orDie, v as encodeSync, vi as prepend, vn as failureOrCause, vr as deferredMake, vt as repeat, w as annotateLogs, wi as unsafeFromArray, wn as isInterruptedOnly, wr as exitFailCause, wt as scope, x as acquireUseRelease$2, xi as take$2, xn as interruptors, xr as deferredUnsafeMake, xt as runPromiseExit$1, y as _void, yi as reduce, yn as flipCauseOption, yr as deferredSucceed, yt as retry, z as fiberIdWith, zi as fromNullable, zn as sleep, zr as suspend$4, zt as uninterruptibleMask } from "./chunks/Schema-DO5ttJaI.mjs";
5
5
  import { createRequire } from "node:module";
@@ -6999,7 +6999,7 @@ const TRAILER_READERS = {
6999
6999
  if (value !== "") into.branch ??= value;
7000
7000
  },
7001
7001
  "Gtd-Version": (value, into) => {
7002
- if (value !== "") into.version ??= value;
7002
+ if (value !== "" && !/[\u0000-\u001f\u007f-\u009f]/.test(value)) into.version ??= value;
7003
7003
  },
7004
7004
  "Gtd-Kept": (value, into) => {
7005
7005
  if (HASH_RE.test(value)) into.kept ??= value;
@@ -7437,7 +7437,8 @@ const replay = async (input) => {
7437
7437
  via,
7438
7438
  trace,
7439
7439
  landed,
7440
- marks
7440
+ marks,
7441
+ slices: slices.map((s) => ({ ...s }))
7441
7442
  };
7442
7443
  const changesSince = (hash) => {
7443
7444
  const tree = treeAt(hash);
@@ -8867,22 +8868,14 @@ const runAtTip = (git, def, tip) => gen(function* () {
8867
8868
  if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
8868
8869
  return yield* runOf([...before, ...closing], { ...location }, tip.hash);
8869
8870
  });
8870
- /** The process HEAD closes or sits inside — `gtd summary`'s run. */
8871
- const summaryRun = gen(function* () {
8872
- const git = yield* GitService;
8873
- const def = (yield* (yield* ConfigService).load).workflow;
8871
+ const runAtHead = (git, def) => gen(function* () {
8874
8872
  return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
8875
8873
  });
8876
- /** The process whose finishing commit is `tip` — reads from `tip`'s own ancestry, so it works when no branch reaches it. */
8877
- const closedRunAt = (tip) => gen(function* () {
8874
+ /** The process HEAD closes or sits inside — `gtd summary`'s run. */
8875
+ const summaryRun = gen(function* () {
8878
8876
  const git = yield* GitService;
8879
8877
  const def = (yield* (yield* ConfigService).load).workflow;
8880
- const hash = yield* git.resolveRef(tip);
8881
- const subject = yield* git.lastCommitSubject(hash);
8882
- return yield* runAtTip(git, def, {
8883
- hash,
8884
- subject
8885
- });
8878
+ return yield* runAtHead(git, def);
8886
8879
  });
8887
8880
  const commitTree = (workspace, hash) => {
8888
8881
  let entries;
@@ -9211,12 +9204,54 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
9211
9204
  total: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
9212
9205
  byModel: costByModel(run.costEntries)
9213
9206
  },
9214
- setup,
9215
- slices: sliceRangesOf(outcome.slices, run)
9207
+ setup
9216
9208
  };
9217
9209
  });
9218
9210
  const currentRest = restAt(void 0);
9219
9211
  /**
9212
+ * The slices of the process HEAD closes or sits inside, current episode only,
9213
+ * stack order. Reads like `historyTrail`, never `restAt`: it narrates and
9214
+ * writes nothing, and returns a version warning for a closed run.
9215
+ */
9216
+ const processSlices = gen(function* () {
9217
+ const git = yield* GitService;
9218
+ const config = yield* (yield* ConfigService).load;
9219
+ const workspace = yield* Workspace;
9220
+ const host = yield* Host;
9221
+ const run = yield* runAtHead(git, config.workflow);
9222
+ if (run.trace.length === 0) return yield* fail$5(/* @__PURE__ */ new Error("gtd slices: refused — HEAD neither closes nor sits inside a process"));
9223
+ const closing = run.closingHash;
9224
+ let outcome;
9225
+ let versionWarning;
9226
+ if (closing !== void 0) {
9227
+ const replayed = yield* replayFinishedProcess(config, run, closing, {
9228
+ command: "gtd slices",
9229
+ configReason: "HEAD's process replays under today's config; revert or stash the edit to list its slices",
9230
+ notEnded: (o) => slicesDivergence(run, o)
9231
+ });
9232
+ outcome = replayed.outcome;
9233
+ versionWarning = replayed.versionWarning;
9234
+ } else {
9235
+ const { setup } = yield* setupFor(config, run, workspace, host);
9236
+ outcome = yield* promise(() => replayFor(setup));
9237
+ if (outcome.kind !== "rest") return yield* fail$5(slicesDivergence(run, outcome));
9238
+ }
9239
+ return {
9240
+ slices: sliceRangesOf(outcome.slices, run).map(({ name, base, tip, open }) => ({
9241
+ name,
9242
+ base,
9243
+ tip,
9244
+ closed: closing !== void 0 || !open
9245
+ })),
9246
+ ...versionWarning !== void 0 ? { versionWarning } : {}
9247
+ };
9248
+ });
9249
+ /** Every `gtd slices` refusal reads `gtd slices: refused — <reason>`; replay's own `gtd: ` lead is dropped. */
9250
+ const slicesDivergence = (run, outcome) => {
9251
+ 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";
9252
+ return /* @__PURE__ */ new Error(`gtd slices: refused — ${reason}`);
9253
+ };
9254
+ /**
9220
9255
  * Whether `gtd --workflow <name>` can start a process here: the workflow must
9221
9256
  * reach a step from the tree the opening commit would capture. That step is
9222
9257
  * what the opening commit's subject names.
@@ -9450,7 +9485,7 @@ const inRepoConfigPaths = (root, files, recorded) => {
9450
9485
  for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
9451
9486
  return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
9452
9487
  };
9453
- 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`);
9488
+ 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}`);
9454
9489
  const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
9455
9490
  const differing = [];
9456
9491
  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);
@@ -9481,22 +9516,19 @@ const versionWarningFor = (recorded) => recorded === GTD_VERSION ? void 0 : `gtd
9481
9516
  const historyTrail = (tip) => gen(function* () {
9482
9517
  const git = yield* GitService;
9483
9518
  const config = yield* (yield* ConfigService).load;
9484
- const workspace = yield* Workspace;
9485
- const host = yield* Host;
9486
9519
  const hash = yield* git.resolveRef(tip);
9487
- const run = yield* closedRunAt(hash);
9488
- const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
9489
- if (differing.length > 0) return yield* fail$5(configRefusal(hash, differing));
9490
- const { setup } = yield* setupFor(config, run, workspace, host);
9491
- yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
9492
- const outcome = yield* promise(() => replayFor(setup));
9493
- if (outcome.kind !== "ended") return yield* fail$5(notClosedError(hash, outcome));
9494
- const steps = historySteps(run);
9495
- const recorded = parseCommitMessage(yield* git.lastCommitMessage(hash)).version;
9496
- const versionWarning = versionWarningFor(recorded);
9520
+ const subject = yield* git.lastCommitSubject(hash);
9521
+ const run = yield* runAtTip(git, config.workflow, {
9522
+ hash,
9523
+ subject
9524
+ });
9525
+ const { outcome, recorded, versionWarning } = yield* replayFinishedProcess(config, run, hash, {
9526
+ command: "gtd history",
9527
+ notEnded: (o) => notClosedError(hash, o)
9528
+ });
9497
9529
  return {
9498
9530
  tip: hash,
9499
- steps,
9531
+ steps: historySteps(run),
9500
9532
  ended: outcome.via,
9501
9533
  version: {
9502
9534
  recorded,
@@ -9505,6 +9537,24 @@ const historyTrail = (tip) => gen(function* () {
9505
9537
  ...versionWarning !== void 0 ? { versionWarning } : {}
9506
9538
  };
9507
9539
  });
9540
+ /** Refuses on config drift before replaying, so a stale config never runs the flow. */
9541
+ const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
9542
+ const git = yield* GitService;
9543
+ const workspace = yield* Workspace;
9544
+ const host = yield* Host;
9545
+ const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
9546
+ if (differing.length > 0) return yield* fail$5(configRefusal(how.command, hash, differing, how.configReason));
9547
+ const { setup } = yield* setupFor(config, run, workspace, host);
9548
+ const outcome = yield* promise(() => replayFor(setup));
9549
+ if (outcome.kind !== "ended") return yield* fail$5(how.notEnded(outcome));
9550
+ const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
9551
+ const versionWarning = versionWarningFor(recorded);
9552
+ return {
9553
+ outcome,
9554
+ recorded,
9555
+ ...versionWarning !== void 0 ? { versionWarning } : {}
9556
+ };
9557
+ });
9508
9558
  //#endregion
9509
9559
  //#region src/emit/GitScript.ts
9510
9560
  const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
@@ -10373,6 +10423,17 @@ const runDoorsCommand = (json, out) => gen(function* () {
10373
10423
  if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
10374
10424
  else yield* writeSelection(out, document, json.path);
10375
10425
  });
10426
+ const runSlicesCommand = (json, out) => gen(function* () {
10427
+ const { slices, versionWarning } = yield* processSlices;
10428
+ if (versionWarning !== void 0) yield* (yield* Narrator).warn(versionWarning);
10429
+ if (json.kind === "off") {
10430
+ out.write(slices.map((s) => `${s.name} ${s.base}..${s.tip}\n`).join(""));
10431
+ return;
10432
+ }
10433
+ const document = slicesDocument(slices);
10434
+ if (json.kind === "document") out.write(renderSlicesJson(document));
10435
+ else yield* writeSelection(out, document, json.path);
10436
+ });
10376
10437
  /**
10377
10438
  * Ends the process underway without completing it. Nothing is discarded: a
10378
10439
  * `git reset --mixed` to the process's start commit leaves every turn
@@ -10708,6 +10769,7 @@ const dispatchVoidCommand = (command, json, out) => {
10708
10769
  case "start": return runStartCommand(command.actor, command.workflow, command.vars, out, command.label);
10709
10770
  case "door": return runDoorCommand(command.name, command.args, out);
10710
10771
  case "doors": return runDoorsCommand(json, out);
10772
+ case "slices": return runSlicesCommand(json, out);
10711
10773
  case "abandon": return runAbandonCommand(out);
10712
10774
  case "restore": return runRestoreCommand(out);
10713
10775
  case "next": return runNextCommand(json, out);
@@ -10764,12 +10826,12 @@ const FLAGS = [
10764
10826
  name: "--json",
10765
10827
  arity: "optional",
10766
10828
  repeatable: false,
10767
- scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "historyList" || kind === "historyShow" || kind === "strays",
10829
+ scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "slices" || kind === "historyList" || kind === "historyShow" || kind === "strays",
10768
10830
  decode: ([raw]) => right(raw ?? ""),
10769
- 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",
10831
+ 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",
10770
10832
  valueHint: "<path>",
10771
10833
  help: [
10772
- "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
10834
+ "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
10773
10835
  "list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
10774
10836
  "--json=<path> (a dotted key path into that document, e.g.",
10775
10837
  "kind, content, session.id) prints just that value: a",
@@ -11239,6 +11301,25 @@ const COMMAND_ROWS = [
11239
11301
  "value off it. Writes nothing"
11240
11302
  ]
11241
11303
  },
11304
+ {
11305
+ token: "slices",
11306
+ kind: "slices",
11307
+ arity: "none",
11308
+ details: [
11309
+ "List the slices of the process HEAD closes or sits inside,",
11310
+ "first slice first: one `name base..tip` line each (full",
11311
+ "hashes; plain output carries no promise). --json prints",
11312
+ "{ schema: 1, slices: [{ name, base, tip, closed }] };",
11313
+ "--json=<path> reads one value off it. Writes nothing: no",
11314
+ "git, no state transition, no session identity. When HEAD",
11315
+ "closes a process, every slice is listed as closed; a gtd",
11316
+ "version mismatch only warns on stderr. Refuses (exit 1)",
11317
+ "when HEAD neither closes nor sits inside a process, or when",
11318
+ "today's config disagrees with history about the process:",
11319
+ "HEAD closes it and its config files differ from the working",
11320
+ "tree, or the replay diverged"
11321
+ ]
11322
+ },
11242
11323
  {
11243
11324
  token: "judge",
11244
11325
  kind: "judge",
package/dist/wire.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { C as EXIT_SIGTERM, D as UNATTRIBUTED_MODEL, E as ACCESS_REFUSAL, O as WIRE_SCHEMA, S as EXIT_SIGINT, T as EXIT_USAGE_ERROR, _ as demandOf, a as landFields, b as EXIT_OK, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, h as BEAT_KINDS, i as judgeJson, k as renderFormat, l as renderLandPlain, m as statusOf, n as resolveSession, o as landProseText, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches, u as DEMAND_BRIEFING, v as stallDiagnosis, w as EXIT_STRAYS_FOUND, x as EXIT_RUNTIME_ERROR, y as EXIT_CODES } from "./chunks/wire-C12dahrH.mjs";
2
- export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_STRAYS_FOUND, EXIT_USAGE_ERROR, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, resolveSession, selectPath, stallDiagnosis, statusOf };
1
+ import { A as WIRE_SCHEMA, C as EXIT_RUNTIME_ERROR, D as EXIT_USAGE_ERROR, E as EXIT_STRAYS_FOUND, O as ACCESS_REFUSAL, S as EXIT_OK, T as EXIT_SIGTERM, _ as BEAT_KINDS, a as selectPath, b as stallDiagnosis, c as landProseText, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as resolveSession, j as renderFormat, k as UNATTRIBUTED_MODEL, l as noopText, m as beatDocument, n as slicesDocument, o as judgeJson, p as renderBeatPlain, r as globMatches, s as landFields, t as renderSlicesJson, u as renderLandJson, v as beatKindOf, w as EXIT_SIGINT, x as EXIT_CODES, y as demandOf } from "./chunks/wire-D3Rc5r22.mjs";
2
+ export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_STRAYS_FOUND, EXIT_USAGE_ERROR, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, renderSlicesJson, resolveSession, selectPath, slicesDocument, stallDiagnosis, statusOf };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "27.8.0",
3
+ "version": "27.9.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {