@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.
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";
@@ -6817,6 +6817,32 @@ const refNameFault = (name) => {
6817
6817
  }
6818
6818
  };
6819
6819
  //#endregion
6820
+ //#region src/replay/Slices.ts
6821
+ const IMPLICIT_SLICE = "main";
6822
+ /** Same shape as mark()'s name rule; kept local so @pmelab/gtd/flows publishes no regex. */
6823
+ const SLICE_NAME = /^[a-z][a-z0-9-]*$/;
6824
+ /**
6825
+ * Folds boundaries into ranges: each kept span's base is the previous kept
6826
+ * span's `leftAt`, so a gap before a span belongs to it, and the last span
6827
+ * runs to the episode tip. A closed span that landed nothing is dropped.
6828
+ */
6829
+ const sliceRanges = (spans, episode) => {
6830
+ if (spans.some((s) => s.enteredAt === "" || s.leftAt === "")) throw new Error("sliceRanges: a pending turn's \"\" hash is not a commit");
6831
+ const kept = spans.filter((s) => s.leftAt === void 0 || s.enteredAt !== s.leftAt);
6832
+ if (kept.length === 0) return [{
6833
+ name: IMPLICIT_SLICE,
6834
+ base: episode.base,
6835
+ tip: episode.tip,
6836
+ open: true
6837
+ }];
6838
+ return kept.map((s, i) => ({
6839
+ name: s.name,
6840
+ base: i === 0 ? episode.base : kept[i - 1].leftAt ?? episode.base,
6841
+ tip: i === kept.length - 1 ? episode.tip : s.leftAt ?? episode.tip,
6842
+ open: i === kept.length - 1
6843
+ }));
6844
+ };
6845
+ //#endregion
6820
6846
  //#region src/replay/Tree.ts
6821
6847
  const treeFromRecord = (files) => ({
6822
6848
  paths: () => Object.keys(files).sort(),
@@ -6973,7 +6999,7 @@ const TRAILER_READERS = {
6973
6999
  if (value !== "") into.branch ??= value;
6974
7000
  },
6975
7001
  "Gtd-Version": (value, into) => {
6976
- if (value !== "") into.version ??= value;
7002
+ if (value !== "" && !/[\u0000-\u001f\u007f-\u009f]/.test(value)) into.version ??= value;
6977
7003
  },
6978
7004
  "Gtd-Kept": (value, into) => {
6979
7005
  if (HASH_RE.test(value)) into.kept ??= value;
@@ -7164,6 +7190,7 @@ const replay = async (input) => {
7164
7190
  const keptTips = /* @__PURE__ */ new Map();
7165
7191
  let landed;
7166
7192
  const marks = [];
7193
+ const slices = [];
7167
7194
  let outcome;
7168
7195
  let signal;
7169
7196
  const settled = new Promise((resolve) => {
@@ -7370,7 +7397,8 @@ const replay = async (input) => {
7370
7397
  rest: step,
7371
7398
  trace,
7372
7399
  landed,
7373
- marks
7400
+ marks,
7401
+ slices: slices.map((s) => ({ ...s }))
7374
7402
  });
7375
7403
  return landPending(step, input.pending);
7376
7404
  };
@@ -7409,7 +7437,8 @@ const replay = async (input) => {
7409
7437
  via,
7410
7438
  trace,
7411
7439
  landed,
7412
- marks
7440
+ marks,
7441
+ slices: slices.map((s) => ({ ...s }))
7413
7442
  };
7414
7443
  const changesSince = (hash) => {
7415
7444
  const tree = treeAt(hash);
@@ -7434,6 +7463,31 @@ const replay = async (input) => {
7434
7463
  scopes.pop();
7435
7464
  scopeCallIds.pop();
7436
7465
  },
7466
+ enterSlice: (name) => {
7467
+ if (outcome !== void 0) return;
7468
+ const fail = (message) => {
7469
+ finish({
7470
+ kind: "failed",
7471
+ message
7472
+ });
7473
+ throw new Stop();
7474
+ };
7475
+ if (typeof name !== "string" || !SLICE_NAME.test(name)) fail(`gtd: slice "${name}": name must match ${SLICE_NAME}`);
7476
+ if (name === "main") fail(`gtd: slice "${IMPLICIT_SLICE}" is reserved for the implicit slice`);
7477
+ const open = slices.find((s) => s.leftAt === void 0);
7478
+ if (open !== void 0) fail(`gtd: slice "${name}" is entered inside slice "${open.name}" — slices do not nest`);
7479
+ if (slices.some((s) => s.name === name)) fail(`gtd: slice "${name}" is entered twice in this episode — a loop must sit inside its slice`);
7480
+ slices.push({
7481
+ name,
7482
+ enteredAt: position.hash,
7483
+ leftAt: void 0
7484
+ });
7485
+ },
7486
+ leaveSlice: () => {
7487
+ if (outcome !== void 0) return;
7488
+ const open = slices.find((s) => s.leftAt === void 0);
7489
+ if (open !== void 0) open.leftAt = position.hash;
7490
+ },
7437
7491
  read: (path) => position.tree.read(path),
7438
7492
  glob: (pattern) => position.tree.paths().filter((path) => globMatches(path, pattern)),
7439
7493
  changes: () => changesBetween(previousPosition.tree, position.tree),
@@ -8814,22 +8868,14 @@ const runAtTip = (git, def, tip) => gen(function* () {
8814
8868
  if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
8815
8869
  return yield* runOf([...before, ...closing], { ...location }, tip.hash);
8816
8870
  });
8871
+ const runAtHead = (git, def) => gen(function* () {
8872
+ return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
8873
+ });
8817
8874
  /** The process HEAD closes or sits inside — `gtd summary`'s run. */
8818
8875
  const summaryRun = gen(function* () {
8819
8876
  const git = yield* GitService;
8820
8877
  const def = (yield* (yield* ConfigService).load).workflow;
8821
- return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
8822
- });
8823
- /** The process whose finishing commit is `tip` — reads from `tip`'s own ancestry, so it works when no branch reaches it. */
8824
- const closedRunAt = (tip) => gen(function* () {
8825
- const git = yield* GitService;
8826
- const def = (yield* (yield* ConfigService).load).workflow;
8827
- const hash = yield* git.resolveRef(tip);
8828
- const subject = yield* git.lastCommitSubject(hash);
8829
- return yield* runAtTip(git, def, {
8830
- hash,
8831
- subject
8832
- });
8878
+ return yield* runAtHead(git, def);
8833
8879
  });
8834
8880
  const commitTree = (workspace, hash) => {
8835
8881
  let entries;
@@ -9112,6 +9158,10 @@ const pendingChanges = (git, withIndex) => gen(function* () {
9112
9158
  const seen = new Set(worktree.map((e) => e.path));
9113
9159
  return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))];
9114
9160
  });
9161
+ const sliceRangesOf = (slices, run) => sliceRanges(slices, {
9162
+ base: run.startParentHash,
9163
+ tip: run.trace.at(-1)?.hash ?? run.startParentHash
9164
+ });
9115
9165
  /** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
9116
9166
  const restAt = (ref, startOverrides = {}) => gen(function* () {
9117
9167
  const git = yield* GitService;
@@ -9159,6 +9209,49 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
9159
9209
  });
9160
9210
  const currentRest = restAt(void 0);
9161
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
+ /**
9162
9255
  * Whether `gtd --workflow <name>` can start a process here: the workflow must
9163
9256
  * reach a step from the tree the opening commit would capture. That step is
9164
9257
  * what the opening commit's subject names.
@@ -9392,7 +9485,7 @@ const inRepoConfigPaths = (root, files, recorded) => {
9392
9485
  for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
9393
9486
  return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
9394
9487
  };
9395
- 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}`);
9396
9489
  const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
9397
9490
  const differing = [];
9398
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);
@@ -9423,22 +9516,19 @@ const versionWarningFor = (recorded) => recorded === GTD_VERSION ? void 0 : `gtd
9423
9516
  const historyTrail = (tip) => gen(function* () {
9424
9517
  const git = yield* GitService;
9425
9518
  const config = yield* (yield* ConfigService).load;
9426
- const workspace = yield* Workspace;
9427
- const host = yield* Host;
9428
9519
  const hash = yield* git.resolveRef(tip);
9429
- const run = yield* closedRunAt(hash);
9430
- const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
9431
- if (differing.length > 0) return yield* fail$5(configRefusal(hash, differing));
9432
- const { setup } = yield* setupFor(config, run, workspace, host);
9433
- yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
9434
- const outcome = yield* promise(() => replayFor(setup));
9435
- if (outcome.kind !== "ended") return yield* fail$5(notClosedError(hash, outcome));
9436
- const steps = historySteps(run);
9437
- const recorded = parseCommitMessage(yield* git.lastCommitMessage(hash)).version;
9438
- 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
+ });
9439
9529
  return {
9440
9530
  tip: hash,
9441
- steps,
9531
+ steps: historySteps(run),
9442
9532
  ended: outcome.via,
9443
9533
  version: {
9444
9534
  recorded,
@@ -9447,6 +9537,24 @@ const historyTrail = (tip) => gen(function* () {
9447
9537
  ...versionWarning !== void 0 ? { versionWarning } : {}
9448
9538
  };
9449
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
+ });
9450
9558
  //#endregion
9451
9559
  //#region src/emit/GitScript.ts
9452
9560
  const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
@@ -10315,6 +10423,17 @@ const runDoorsCommand = (json, out) => gen(function* () {
10315
10423
  if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
10316
10424
  else yield* writeSelection(out, document, json.path);
10317
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
+ });
10318
10437
  /**
10319
10438
  * Ends the process underway without completing it. Nothing is discarded: a
10320
10439
  * `git reset --mixed` to the process's start commit leaves every turn
@@ -10650,6 +10769,7 @@ const dispatchVoidCommand = (command, json, out) => {
10650
10769
  case "start": return runStartCommand(command.actor, command.workflow, command.vars, out, command.label);
10651
10770
  case "door": return runDoorCommand(command.name, command.args, out);
10652
10771
  case "doors": return runDoorsCommand(json, out);
10772
+ case "slices": return runSlicesCommand(json, out);
10653
10773
  case "abandon": return runAbandonCommand(out);
10654
10774
  case "restore": return runRestoreCommand(out);
10655
10775
  case "next": return runNextCommand(json, out);
@@ -10706,12 +10826,12 @@ const FLAGS = [
10706
10826
  name: "--json",
10707
10827
  arity: "optional",
10708
10828
  repeatable: false,
10709
- 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",
10710
10830
  decode: ([raw]) => right(raw ?? ""),
10711
- 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",
10712
10832
  valueHint: "<path>",
10713
10833
  help: [
10714
- "(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",
10715
10835
  "list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
10716
10836
  "--json=<path> (a dotted key path into that document, e.g.",
10717
10837
  "kind, content, session.id) prints just that value: a",
@@ -11181,6 +11301,25 @@ const COMMAND_ROWS = [
11181
11301
  "value off it. Writes nothing"
11182
11302
  ]
11183
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
+ },
11184
11323
  {
11185
11324
  token: "judge",
11186
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.7.1",
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": {
@@ -112,6 +112,7 @@ describe("parseArgv — scope", () => {
112
112
  ["judge", "--json"],
113
113
  ["judge", "answer", "--json"],
114
114
  ["doors", "--json"],
115
+ ["slices", "--json"],
115
116
  ]) {
116
117
  const ok = parseArgv(["node", "gtd.js", ...args])
117
118
  expect(ok.kind).toBe("command")
@@ -927,6 +928,40 @@ describe("parseArgv — doors", () => {
927
928
  })
928
929
  })
929
930
 
931
+ describe("parseArgv — slices", () => {
932
+ const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
933
+
934
+ it("`gtd slices` parses with every --json shape", () => {
935
+ expect(parse("slices")).toMatchObject({
936
+ kind: "command",
937
+ command: { kind: "slices" },
938
+ json: { kind: "off" },
939
+ })
940
+ expect(parse("slices", "--json")).toMatchObject({ json: { kind: "document" } })
941
+ expect(parse("slices", "--json=slices.0.name")).toMatchObject({
942
+ json: { kind: "select", path: "slices.0.name" },
943
+ })
944
+ })
945
+
946
+ it("`gtd slices` takes no argument", () => {
947
+ const plan = parse("slices", "extra")
948
+ expect(plan.kind).toBe("usage")
949
+ if (plan.kind === "usage") expect(plan.message).toContain("too many arguments")
950
+ })
951
+
952
+ it("help lists the command, and --json names it", () => {
953
+ const help = renderHelp()
954
+ expect(help).toMatch(/^ {2}slices\b/m)
955
+ expect(help).toContain("gtd slices")
956
+ })
957
+
958
+ it("--json's scope error names gtd slices", () => {
959
+ const plan = parse("lsp", "--json")
960
+ expect(plan.kind).toBe("usage")
961
+ if (plan.kind === "usage") expect(plan.message).toContain("`gtd slices`")
962
+ })
963
+ })
964
+
930
965
  describe("parseArgv — gtd history", () => {
931
966
  const parse = (...args: string[]) => parseArgv(["node", "gtd.js", ...args])
932
967
  const usage = (plan: CliPlan): string => (plan.kind === "usage" ? plan.message : "")
@@ -1057,6 +1092,7 @@ describe("standaloneKinds / needsOf", () => {
1057
1092
  "judgeAnswer",
1058
1093
  "door",
1059
1094
  "doors",
1095
+ "slices",
1060
1096
  "historyList",
1061
1097
  "historyShow",
1062
1098
  "historyFetch",
@@ -1136,9 +1172,9 @@ describe("renderHelp", () => {
1136
1172
  expect(match, file).not.toBeNull()
1137
1173
  return new Set([...match![1]!.matchAll(/`([a-z]+(?: [a-z]+)?)`/g)].map(([, name]) => name!))
1138
1174
  }
1139
- expect(listed("../../README.md", /The `gtd` bin's commands — ([\s\S]*?) — are listed/)).toEqual(
1140
- commands,
1141
- )
1175
+ expect(
1176
+ listed("../../README.md", /The `gtd` bin's commands — ([\s\S]*?) — are\s+listed/),
1177
+ ).toEqual(commands)
1142
1178
  expect(listed("../../COMPATIBILITY.md", /every command \(([\s\S]*?)\), option/)).toEqual(
1143
1179
  commands,
1144
1180
  )
package/src/cli/Cli.ts CHANGED
@@ -101,16 +101,17 @@ const FLAGS: readonly FlagRow[] = [
101
101
  kind === "judge" ||
102
102
  kind === "judgeAnswer" ||
103
103
  kind === "doors" ||
104
+ kind === "slices" ||
104
105
  kind === "historyList" ||
105
106
  kind === "historyShow" ||
106
107
  kind === "strays",
107
108
  decode: ([raw]) => Either.right(raw ?? ""),
108
109
  scopeError:
109
- "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` — " +
110
+ "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` — " +
110
111
  "every other command prints plain text; see `gtd install` for the driver protocol briefing",
111
112
  valueHint: "<path>",
112
113
  help: [
113
- "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
114
+ "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
114
115
  "list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
115
116
  "--json=<path> (a dotted key path into that document, e.g.",
116
117
  "kind, content, session.id) prints just that value: a",
@@ -636,6 +637,25 @@ const COMMAND_ROWS: readonly CommandRow[] = [
636
637
  "value off it. Writes nothing",
637
638
  ],
638
639
  },
640
+ {
641
+ token: "slices",
642
+ kind: "slices",
643
+ arity: "none",
644
+ details: [
645
+ "List the slices of the process HEAD closes or sits inside,",
646
+ "first slice first: one `name base..tip` line each (full",
647
+ "hashes; plain output carries no promise). --json prints",
648
+ "{ schema: 1, slices: [{ name, base, tip, closed }] };",
649
+ "--json=<path> reads one value off it. Writes nothing: no",
650
+ "git, no state transition, no session identity. When HEAD",
651
+ "closes a process, every slice is listed as closed; a gtd",
652
+ "version mismatch only warns on stderr. Refuses (exit 1)",
653
+ "when HEAD neither closes nor sits inside a process, or when",
654
+ "today's config disagrees with history about the process:",
655
+ "HEAD closes it and its config files differ from the working",
656
+ "tree, or the replay diverged",
657
+ ],
658
+ },
639
659
  {
640
660
  token: "judge",
641
661
  kind: "judge",
@@ -1304,6 +1324,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
1304
1324
  | "install"
1305
1325
  | "loop"
1306
1326
  | "doors"
1327
+ | "slices"
1307
1328
  | "summary"
1308
1329
  | "base"
1309
1330
  | "judge"
@@ -1677,6 +1677,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
1677
1677
  start: { kind: "start", actor: "human", workflow: "feature", vars: {}, label: "" },
1678
1678
  door: { kind: "door", name: "fix", args: [] },
1679
1679
  doors: { kind: "doors" },
1680
+ slices: { kind: "slices" },
1680
1681
  abandon: { kind: "abandon" },
1681
1682
  restore: { kind: "restore" },
1682
1683
  next: { kind: "next" },
@@ -1703,13 +1704,14 @@ describe("runCommand — refuses in a repository with no commits", () => {
1703
1704
  const NO_COMMITS_MESSAGE =
1704
1705
  "gtd requires a repository with at least one commit — make an initial commit, then run gtd again"
1705
1706
 
1706
- it("derives exactly the sixteen non-standalone kinds — a canary for the table-driven cases below", () => {
1707
+ it("derives exactly the seventeen non-standalone kinds — a canary for the table-driven cases below", () => {
1707
1708
  expect(stateKinds.sort()).toEqual(
1708
1709
  [
1709
1710
  "abandon",
1710
1711
  "base",
1711
1712
  "door",
1712
1713
  "doors",
1714
+ "slices",
1713
1715
  "start",
1714
1716
  "land",
1715
1717
  "next",
@@ -43,6 +43,8 @@ import {
43
43
  type StatusChange,
44
44
  selectPath,
45
45
  EXIT_OK,
46
+ renderSlicesJson,
47
+ slicesDocument,
46
48
  } from "../wire/index.js"
47
49
  import {
48
50
  currentRest,
@@ -51,6 +53,7 @@ import {
51
53
  renderRest,
52
54
  restAt,
53
55
  previewLanding,
56
+ processSlices,
54
57
  noProcessUnderway,
55
58
  restIsIdle,
56
59
  reviewBaseFor,
@@ -143,6 +146,7 @@ export type Command =
143
146
  }
144
147
  | { readonly kind: "door"; readonly name: string; readonly args: readonly string[] }
145
148
  | { readonly kind: "doors" }
149
+ | { readonly kind: "slices" }
146
150
  | { readonly kind: "abandon" }
147
151
  | { readonly kind: "restore" }
148
152
  | { readonly kind: "next" }
@@ -659,6 +663,22 @@ const runDoorsCommand = (
659
663
  else yield* writeSelection(out, document, json.path)
660
664
  })
661
665
 
666
+ const runSlicesCommand = (
667
+ json: JsonMode,
668
+ out: ArtifactOut,
669
+ ): Effect.Effect<void, Error, CommandRequirements> =>
670
+ Effect.gen(function* () {
671
+ const { slices, versionWarning } = yield* processSlices
672
+ if (versionWarning !== undefined) yield* (yield* Narrator).warn(versionWarning)
673
+ if (json.kind === "off") {
674
+ out.write(slices.map((s) => `${s.name} ${s.base}..${s.tip}\n`).join(""))
675
+ return
676
+ }
677
+ const document = slicesDocument(slices)
678
+ if (json.kind === "document") out.write(renderSlicesJson(document))
679
+ else yield* writeSelection(out, document, json.path)
680
+ })
681
+
662
682
  /**
663
683
  * Ends the process underway without completing it. Nothing is discarded: a
664
684
  * `git reset --mixed` to the process's start commit leaves every turn
@@ -1223,6 +1243,8 @@ const dispatchVoidCommand = (
1223
1243
  return runDoorCommand(command.name, command.args, out)
1224
1244
  case "doors":
1225
1245
  return runDoorsCommand(json, out)
1246
+ case "slices":
1247
+ return runSlicesCommand(json, out)
1226
1248
  case "abandon":
1227
1249
  return runAbandonCommand(out)
1228
1250
  case "restore":