@pmelab/gtd 27.8.0 → 28.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +59 -30
  3. package/claude/hooks/doors.ts +12 -0
  4. package/claude/hooks/register.tsx +2 -4
  5. package/claude/hooks/ship.ts +115 -20
  6. package/dist/chunks/{reserved-UdSuHDUJ.mjs → reserved-BIsngMGL.mjs} +168 -27
  7. package/dist/chunks/{tail-fj60rQ3y.mjs → tail-DHEaOHyp.mjs} +1 -1
  8. package/dist/chunks/{wire-C12dahrH.mjs → wire-D3Rc5r22.mjs} +14 -1
  9. package/dist/gtd.bundle.mjs +174 -48
  10. package/dist/testkit.mjs +158 -49
  11. package/dist/wire.mjs +2 -2
  12. package/dist/workflows/cleanup.mjs +3 -4
  13. package/dist/workflows/feature.mjs +2 -2
  14. package/dist/workflows/fix.mjs +2 -2
  15. package/dist/workflows/review.mjs +2 -2
  16. package/dist/workflows/rollout.mjs +19 -4
  17. package/dist/workflows.mjs +186 -127
  18. package/package.json +1 -1
  19. package/skills/authoring/SKILL.md +4 -3
  20. package/src/cli/Cli.test.ts +39 -3
  21. package/src/cli/Cli.ts +27 -6
  22. package/src/cli/Program.test.ts +3 -1
  23. package/src/cli/Program.ts +22 -0
  24. package/src/edge/Edge.test.ts +209 -54
  25. package/src/edge/Edge.ts +121 -42
  26. package/src/edge/index.ts +1 -0
  27. package/src/install/ConfigScaffold.ts +4 -1
  28. package/src/install/Install.test.ts +10 -0
  29. package/src/install/Install.ts +5 -2
  30. package/src/replay/Replay.test.ts +32 -0
  31. package/src/replay/Replay.ts +3 -1
  32. package/src/replay/Trailers.test.ts +11 -0
  33. package/src/replay/Trailers.ts +3 -1
  34. package/src/replay/index.ts +1 -1
  35. package/src/wire/Slices.ts +25 -0
  36. package/src/wire/WireSchema.test.ts +18 -0
  37. package/src/wire/golden/1/slices.json +11 -0
  38. package/src/wire/index.ts +1 -0
  39. package/src/workflow/load.test.ts +82 -5
  40. package/src/workflow/load.ts +40 -16
  41. package/src/workflows/cleanup/cleanup.feature +21 -0
  42. package/src/workflows/cleanup/cleanup.ts +1 -1
  43. package/src/workflows/index.ts +2 -1
  44. package/src/workflows/rollout/rollout.feature +282 -2
  45. package/src/workflows/rollout/rollout.test.ts +96 -2
  46. package/src/workflows/rollout/rollout.ts +41 -3
  47. package/src/workflows/shared/options.test.ts +25 -2
  48. package/src/workflows/shared/options.ts +7 -4
  49. package/src/workflows/shared/registry.test.ts +41 -1
  50. package/src/workflows/shared/registry.ts +26 -1
  51. package/src/workflows/shared/text.test.ts +252 -2
  52. package/src/workflows/shared/text.ts +44 -1
  53. package/src/workflows/shared/vars.ts +2 -0
  54. package/dist/chunks/registry-CP3YuV9j.mjs +0 -101
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";
@@ -6647,8 +6647,11 @@ hands off to \`gtd-build\`, which halts at the blocked gate and prints it,
6647
6647
  rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
6648
6648
  straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
6649
6649
  \`gtd doors\` lists the doors a repository offers (those of the workflows its
6650
- config exports — the bundled \`fix\` and \`review [base]\` — plus any it
6651
- declares). Refuses with
6650
+ config exports — the bundled \`fix\` and \`review [base]\`, plus the opt-in
6651
+ \`rollout <flag>\` (turn a release flag default-on) and \`cleanup <flag>\`
6652
+ (remove it), which a config adds by exporting \`rollout\` from
6653
+ \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
6654
+ \`@pmelab/gtd/workflows/cleanup\` — plus any it declares). Refuses with
6652
6655
  \`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
6653
6656
  missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
6654
6657
 
@@ -6758,7 +6761,10 @@ const renderBriefing = () => HEADER() + BEAT_PROTOCOL + JSON_FIELD_REFERENCE + D
6758
6761
  const CONFIG_SCAFFOLD = `// The workflows gtd runs here: gtd's shipped feature, review and fix.
6759
6762
  // Every exported function is startable with \`gtd --workflow <name>\`; the
6760
6763
  // default export is what a change in the tree starts. Each workflow brings
6761
- // its own doors: \`gtd door review [base]\` and \`gtd door fix\`.
6764
+ // its own doors: \`gtd door review [base]\` and \`gtd door fix\`. Export \`rollout\`
6765
+ // from \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
6766
+ // \`@pmelab/gtd/workflows/cleanup\` to add \`gtd door rollout <flag>\` and
6767
+ // \`gtd door cleanup <flag>\`.
6762
6768
  export {
6763
6769
  feature as default,
6764
6770
  feature,
@@ -6999,7 +7005,7 @@ const TRAILER_READERS = {
6999
7005
  if (value !== "") into.branch ??= value;
7000
7006
  },
7001
7007
  "Gtd-Version": (value, into) => {
7002
- if (value !== "") into.version ??= value;
7008
+ if (value !== "" && !/[\u0000-\u001f\u007f-\u009f]/.test(value)) into.version ??= value;
7003
7009
  },
7004
7010
  "Gtd-Kept": (value, into) => {
7005
7011
  if (HASH_RE.test(value)) into.kept ??= value;
@@ -7437,7 +7443,8 @@ const replay = async (input) => {
7437
7443
  via,
7438
7444
  trace,
7439
7445
  landed,
7440
- marks
7446
+ marks,
7447
+ slices: slices.map((s) => ({ ...s }))
7441
7448
  };
7442
7449
  const changesSince = (hash) => {
7443
7450
  const tree = treeAt(hash);
@@ -8103,10 +8110,10 @@ const doorsFrom = (exports) => {
8103
8110
  };
8104
8111
  /**
8105
8112
  * Each exported workflow's own doors, onto the name it is exported under. The
8106
- * `doors` export adds to them and settles a name two workflows both declare.
8113
+ * `doors` export adds to them and declares a name several workflows clash on.
8107
8114
  */
8108
8115
  const derivedDoors = (catalogue, declared) => {
8109
- const doors = /* @__PURE__ */ new Map();
8116
+ const builders = /* @__PURE__ */ new Map();
8110
8117
  for (const [workflow, loaded] of catalogue) for (const [name, spec] of Object.entries(loaded.doors ?? {})) {
8111
8118
  const door = {
8112
8119
  ...spec,
@@ -8114,12 +8121,22 @@ const derivedDoors = (catalogue, declared) => {
8114
8121
  };
8115
8122
  const problem = doorProblem(name, door);
8116
8123
  if (problem !== void 0) throw new Error(`workflow "${workflow}" builds door "${name}", which ${problem}`);
8117
- const other = doors.get(name);
8118
- if (other !== void 0 && !declared.has(name)) throw new Error(`door "${name}" is built by workflows "${other.workflow}" and "${workflow}" — declare it in the "doors" export to choose one (a declared door replaces the derived one, so restate its args and vars)`);
8119
- doors.set(name, door);
8124
+ builders.set(name, [...builders.get(name) ?? [], door]);
8125
+ }
8126
+ const doors = /* @__PURE__ */ new Map();
8127
+ const clashes = /* @__PURE__ */ new Map();
8128
+ for (const [name, built] of builders) {
8129
+ const owner = ownerOf(name, built);
8130
+ if (owner !== void 0) doors.set(name, owner);
8131
+ else if (!declared.has(name)) clashes.set(name, built.map((door) => door.workflow));
8120
8132
  }
8121
- return new Map([...doors, ...declared]);
8133
+ return {
8134
+ doors: new Map([...doors, ...declared]),
8135
+ clashes
8136
+ };
8122
8137
  };
8138
+ /** The export named after the door owns it, else the sole builder; a clash of several builds no door. */
8139
+ const ownerOf = (name, built) => built.find((door) => door.workflow === name) ?? (built.length === 1 ? built[0] : void 0);
8123
8140
  /** Sort the doors and check each names a startable workflow. */
8124
8141
  const checkDoors = (declared, catalogue) => {
8125
8142
  const doors = new Map([...declared].sort(([a], [b]) => a.localeCompare(b)));
@@ -8150,16 +8167,28 @@ const loadWorkflows = (module, shipped) => {
8150
8167
  return workflow === void 0 ? [] : [[name, workflow]];
8151
8168
  }));
8152
8169
  const defaultWorkflow = built.get(value);
8170
+ const derived = rejections.length > 0 ? {
8171
+ doors: /* @__PURE__ */ new Map(),
8172
+ clashes: /* @__PURE__ */ new Map()
8173
+ } : derivedDoors(catalogue, doorsFrom(exports));
8153
8174
  return {
8154
8175
  file,
8155
8176
  catalogue,
8156
- doors: rejections.length > 0 ? /* @__PURE__ */ new Map() : checkDoors(derivedDoors(catalogue, doorsFrom(exports)), catalogue),
8177
+ doors: checkDoors(derived.doors, catalogue),
8157
8178
  default: defaultWorkflow && {
8158
8179
  ...defaultWorkflow,
8159
8180
  name: defaultName
8160
8181
  },
8161
8182
  config: compileConfig(exports["config"], module.filepath),
8162
- diagnostics: nameRejections(rejections)
8183
+ diagnostics: [...nameRejections(rejections), ...[...derived.clashes].map(([name, builders]) => {
8184
+ const names = [...builders].sort().map((b) => JSON.stringify(b));
8185
+ return {
8186
+ severity: "warning",
8187
+ message: `door "${name}" is built by workflows ${names.slice(0, -1).join(", ")} and ${names.at(-1)} and none is exported under that name — declare it in the "doors" export`,
8188
+ path: ["doors", name],
8189
+ origin: file.origin
8190
+ };
8191
+ })]
8163
8192
  };
8164
8193
  };
8165
8194
  const evaluateFromDisk = (module, shipped) => {
@@ -8867,22 +8896,14 @@ const runAtTip = (git, def, tip) => gen(function* () {
8867
8896
  if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
8868
8897
  return yield* runOf([...before, ...closing], { ...location }, tip.hash);
8869
8898
  });
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;
8899
+ const runAtHead = (git, def) => gen(function* () {
8874
8900
  return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
8875
8901
  });
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* () {
8902
+ /** The process HEAD closes or sits inside — `gtd summary`'s run. */
8903
+ const summaryRun = gen(function* () {
8878
8904
  const git = yield* GitService;
8879
8905
  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
- });
8906
+ return yield* runAtHead(git, def);
8886
8907
  });
8887
8908
  const commitTree = (workspace, hash) => {
8888
8909
  let entries;
@@ -9211,12 +9232,54 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
9211
9232
  total: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
9212
9233
  byModel: costByModel(run.costEntries)
9213
9234
  },
9214
- setup,
9215
- slices: sliceRangesOf(outcome.slices, run)
9235
+ setup
9216
9236
  };
9217
9237
  });
9218
9238
  const currentRest = restAt(void 0);
9219
9239
  /**
9240
+ * The slices of the process HEAD closes or sits inside, current episode only,
9241
+ * stack order. Reads like `historyTrail`, never `restAt`: it narrates and
9242
+ * writes nothing, and returns a version warning for a closed run.
9243
+ */
9244
+ const processSlices = gen(function* () {
9245
+ const git = yield* GitService;
9246
+ const config = yield* (yield* ConfigService).load;
9247
+ const workspace = yield* Workspace;
9248
+ const host = yield* Host;
9249
+ const run = yield* runAtHead(git, config.workflow);
9250
+ if (run.trace.length === 0) return yield* fail$5(/* @__PURE__ */ new Error("gtd slices: refused — HEAD neither closes nor sits inside a process"));
9251
+ const closing = run.closingHash;
9252
+ let outcome;
9253
+ let versionWarning;
9254
+ if (closing !== void 0) {
9255
+ const replayed = yield* replayFinishedProcess(config, run, closing, {
9256
+ command: "gtd slices",
9257
+ configReason: "HEAD's process replays under today's config; revert or stash the edit to list its slices",
9258
+ notEnded: (o) => slicesDivergence(run, o)
9259
+ });
9260
+ outcome = replayed.outcome;
9261
+ versionWarning = replayed.versionWarning;
9262
+ } else {
9263
+ const { setup } = yield* setupFor(config, run, workspace, host);
9264
+ outcome = yield* promise(() => replayFor(setup));
9265
+ if (outcome.kind !== "rest") return yield* fail$5(slicesDivergence(run, outcome));
9266
+ }
9267
+ return {
9268
+ slices: sliceRangesOf(outcome.slices, run).map(({ name, base, tip, open }) => ({
9269
+ name,
9270
+ base,
9271
+ tip,
9272
+ closed: closing !== void 0 || !open
9273
+ })),
9274
+ ...versionWarning !== void 0 ? { versionWarning } : {}
9275
+ };
9276
+ });
9277
+ /** Every `gtd slices` refusal reads `gtd slices: refused — <reason>`; replay's own `gtd: ` lead is dropped. */
9278
+ const slicesDivergence = (run, outcome) => {
9279
+ 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";
9280
+ return /* @__PURE__ */ new Error(`gtd slices: refused — ${reason}`);
9281
+ };
9282
+ /**
9220
9283
  * Whether `gtd --workflow <name>` can start a process here: the workflow must
9221
9284
  * reach a step from the tree the opening commit would capture. That step is
9222
9285
  * what the opening commit's subject names.
@@ -9450,7 +9513,7 @@ const inRepoConfigPaths = (root, files, recorded) => {
9450
9513
  for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
9451
9514
  return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
9452
9515
  };
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`);
9516
+ 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
9517
  const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
9455
9518
  const differing = [];
9456
9519
  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 +9544,19 @@ const versionWarningFor = (recorded) => recorded === GTD_VERSION ? void 0 : `gtd
9481
9544
  const historyTrail = (tip) => gen(function* () {
9482
9545
  const git = yield* GitService;
9483
9546
  const config = yield* (yield* ConfigService).load;
9484
- const workspace = yield* Workspace;
9485
- const host = yield* Host;
9486
9547
  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);
9548
+ const subject = yield* git.lastCommitSubject(hash);
9549
+ const run = yield* runAtTip(git, config.workflow, {
9550
+ hash,
9551
+ subject
9552
+ });
9553
+ const { outcome, recorded, versionWarning } = yield* replayFinishedProcess(config, run, hash, {
9554
+ command: "gtd history",
9555
+ notEnded: (o) => notClosedError(hash, o)
9556
+ });
9497
9557
  return {
9498
9558
  tip: hash,
9499
- steps,
9559
+ steps: historySteps(run),
9500
9560
  ended: outcome.via,
9501
9561
  version: {
9502
9562
  recorded,
@@ -9505,6 +9565,24 @@ const historyTrail = (tip) => gen(function* () {
9505
9565
  ...versionWarning !== void 0 ? { versionWarning } : {}
9506
9566
  };
9507
9567
  });
9568
+ /** Refuses on config drift before replaying, so a stale config never runs the flow. */
9569
+ const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
9570
+ const git = yield* GitService;
9571
+ const workspace = yield* Workspace;
9572
+ const host = yield* Host;
9573
+ const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
9574
+ if (differing.length > 0) return yield* fail$5(configRefusal(how.command, hash, differing, how.configReason));
9575
+ const { setup } = yield* setupFor(config, run, workspace, host);
9576
+ const outcome = yield* promise(() => replayFor(setup));
9577
+ if (outcome.kind !== "ended") return yield* fail$5(how.notEnded(outcome));
9578
+ const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
9579
+ const versionWarning = versionWarningFor(recorded);
9580
+ return {
9581
+ outcome,
9582
+ recorded,
9583
+ ...versionWarning !== void 0 ? { versionWarning } : {}
9584
+ };
9585
+ });
9508
9586
  //#endregion
9509
9587
  //#region src/emit/GitScript.ts
9510
9588
  const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
@@ -10373,6 +10451,17 @@ const runDoorsCommand = (json, out) => gen(function* () {
10373
10451
  if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
10374
10452
  else yield* writeSelection(out, document, json.path);
10375
10453
  });
10454
+ const runSlicesCommand = (json, out) => gen(function* () {
10455
+ const { slices, versionWarning } = yield* processSlices;
10456
+ if (versionWarning !== void 0) yield* (yield* Narrator).warn(versionWarning);
10457
+ if (json.kind === "off") {
10458
+ out.write(slices.map((s) => `${s.name} ${s.base}..${s.tip}\n`).join(""));
10459
+ return;
10460
+ }
10461
+ const document = slicesDocument(slices);
10462
+ if (json.kind === "document") out.write(renderSlicesJson(document));
10463
+ else yield* writeSelection(out, document, json.path);
10464
+ });
10376
10465
  /**
10377
10466
  * Ends the process underway without completing it. Nothing is discarded: a
10378
10467
  * `git reset --mixed` to the process's start commit leaves every turn
@@ -10708,6 +10797,7 @@ const dispatchVoidCommand = (command, json, out) => {
10708
10797
  case "start": return runStartCommand(command.actor, command.workflow, command.vars, out, command.label);
10709
10798
  case "door": return runDoorCommand(command.name, command.args, out);
10710
10799
  case "doors": return runDoorsCommand(json, out);
10800
+ case "slices": return runSlicesCommand(json, out);
10711
10801
  case "abandon": return runAbandonCommand(out);
10712
10802
  case "restore": return runRestoreCommand(out);
10713
10803
  case "next": return runNextCommand(json, out);
@@ -10764,12 +10854,12 @@ const FLAGS = [
10764
10854
  name: "--json",
10765
10855
  arity: "optional",
10766
10856
  repeatable: false,
10767
- scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "historyList" || kind === "historyShow" || kind === "strays",
10857
+ scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "slices" || kind === "historyList" || kind === "historyShow" || kind === "strays",
10768
10858
  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",
10859
+ 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
10860
  valueHint: "<path>",
10771
10861
  help: [
10772
- "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
10862
+ "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
10773
10863
  "list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
10774
10864
  "--json=<path> (a dotted key path into that document, e.g.",
10775
10865
  "kind, content, session.id) prints just that value: a",
@@ -11222,10 +11312,10 @@ const COMMAND_ROWS = [
11222
11312
  "positional args to that workflow's process settings — the",
11223
11313
  "same start `gtd --workflow` makes, printed the same way.",
11224
11314
  "Each workflow the config exports brings its own doors (gtd's",
11225
- "`fix` brings `fix`, `review` brings `review [base]`), plus",
11226
- "the config's `doors` export. An unknown door, or too few or",
11227
- "too many args, is a usage error (exit 2) naming the doors or",
11228
- "the door's synopsis"
11315
+ "`fix`, `review [base]`, `rollout <flag>` and",
11316
+ "`cleanup <flag>`), plus the config's `doors` export. An",
11317
+ "unknown door, or too few or too many args, is a usage error",
11318
+ "(exit 2) naming the doors or the door's synopsis"
11229
11319
  ]
11230
11320
  },
11231
11321
  {
@@ -11239,6 +11329,25 @@ const COMMAND_ROWS = [
11239
11329
  "value off it. Writes nothing"
11240
11330
  ]
11241
11331
  },
11332
+ {
11333
+ token: "slices",
11334
+ kind: "slices",
11335
+ arity: "none",
11336
+ details: [
11337
+ "List the slices of the process HEAD closes or sits inside,",
11338
+ "first slice first: one `name base..tip` line each (full",
11339
+ "hashes; plain output carries no promise). --json prints",
11340
+ "{ schema: 1, slices: [{ name, base, tip, closed }] };",
11341
+ "--json=<path> reads one value off it. Writes nothing: no",
11342
+ "git, no state transition, no session identity. When HEAD",
11343
+ "closes a process, every slice is listed as closed; a gtd",
11344
+ "version mismatch only warns on stderr. Refuses (exit 1)",
11345
+ "when HEAD neither closes nor sits inside a process, or when",
11346
+ "today's config disagrees with history about the process:",
11347
+ "HEAD closes it and its config files differ from the working",
11348
+ "tree, or the replay diverged"
11349
+ ]
11350
+ },
11242
11351
  {
11243
11352
  token: "judge",
11244
11353
  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 };
@@ -1,7 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { a as flagShipped, b as coder, c as envDefaults, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-UdSuHDUJ.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-fj60rQ3y.mjs";
4
- import { i as requireFlag, r as registryEntry, t as flagReferences } from "../chunks/registry-CP3YuV9j.mjs";
2
+ import { a as flagShipped, ct as defaults, dt as flagReferences, i as skills, lt as envDefaults, mt as requireFlag, n as summary, pt as registryEntry, r as access, t as steering, v as coder } from "../chunks/reserved-BIsngMGL.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DHEaOHyp.mjs";
5
4
  //#region src/workflows/cleanup/cleanup.ts
6
5
  const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
7
6
  const cleanupFlow = async () => {
@@ -29,7 +28,7 @@ const cleanupFlow = async () => {
29
28
  });
30
29
  await handOffTail(false, start(), guard);
31
30
  };
32
- const cleanup = flagShipped(cleanupFlow);
31
+ const cleanup = flagShipped(cleanupFlow, "cleanup");
33
32
  //#endregion
34
33
  //#region src/workflows/cleanup/index.ts
35
34
  var cleanup_default = cleanup;
@@ -1,7 +1,7 @@
1
1
  import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, seamGate, start, vars } from "../flows.mjs";
2
2
  import { review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as refactorFixPrompt, A as architectureAuthorPrompt, B as designGateAnswerMessage, C as fixRisks, D as reviewing, F as buildReviewCollectingPrompt, H as designTriagePrompt, I as buildReviewFixNitsPrompt, J as implementFixPrompt, K as idleMessage, L as buildReviewTriageMessage, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewAwaitAfterDeferMessage, Q as refactorBuildingPrompt, R as builderSystem, T as reviewMissing, V as designSystem, X as packagesItemBuildingPrompt, Y as naivePassPrompt, Z as packagesItemFixSuitePrompt, _ as REQUIREMENTS, at as seamByHandMessage, b as coder, c as envDefaults, ct as unwindFailedMessage, d as gate, et as reviewEditNotesCapture, h as FEEDBACK, i as skills, it as reviewerSystem, j as architectureDecomposePrompt, k as architectSystem, lt as unwindFailure, m as ARCHITECTURE, n as summary, o as shipped, ot as seamSteerMessage, p as healthy, q as implementBuildingPrompt, r as access, rt as reviewNotesCapture, s as defaults, st as startGateBlockedMessage, t as steering, tt as reviewEditsCapture, v as REVIEW, w as planner, x as fixCheck, y as awaitReview, z as deferNotesScript } from "../chunks/reserved-UdSuHDUJ.mjs";
4
- import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-fj60rQ3y.mjs";
3
+ import { $ as reviewEditsCapture, A as architectureGateAnswerMessage, B as designTriagePrompt, C as reviewMissing, D as architectSystem, E as agentWithSkills, F as buildReviewTriageMessage, G as implementBuildingPrompt, I as builderSystem, J as packagesItemBuildingPrompt, K as implementFixPrompt, L as deferNotesScript, M as buildReviewAwaitAfterDeferMessage, N as buildReviewCollectingPrompt, O as architectureAuthorPrompt, P as buildReviewFixNitsPrompt, Q as reviewEditNotesCapture, R as designGateAnswerMessage, S as planner, T as reviewing, W as idleMessage, X as refactorBuildingPrompt, Y as packagesItemFixSuitePrompt, Z as refactorFixPrompt, _ as awaitReview, at as startGateBlockedMessage, ct as defaults, d as healthy, f as ARCHITECTURE, g as REVIEW, h as REQUIREMENTS, i as skills, it as seamSteerMessage, j as buildReviewAnswerQuestionsPrompt, k as architectureDecomposePrompt, l as gate, lt as envDefaults, n as summary, nt as reviewerSystem, o as shipped, ot as unwindFailedMessage, p as FEEDBACK, q as naivePassPrompt, r as access, rt as seamByHandMessage, st as unwindFailure, t as steering, tt as reviewNotesCapture, v as coder, x as fixRisks, y as fixCheck, z as designSystem } from "../chunks/reserved-BIsngMGL.mjs";
4
+ import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-DHEaOHyp.mjs";
5
5
  //#region src/workflows/feature/tier.ts
6
6
  /** GTD_FLAG_NAIVE_PASS, GTD_FLAG_RISK_TIER: both pinned at process start, so a mid-process flip never moves a process. */
7
7
  const riskTierOn = () => vars.flag_naive_pass === "1" && vars.flag_risk_tier === "1";
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
3
- import { O as agentWithSkills, U as finisherSystem, W as fixReviewNotesPrompt, b as coder, c as envDefaults, i as skills, l as baseline, n as summary, o as shipped, r as access, s as defaults, t as steering, v as REVIEW } from "../chunks/reserved-UdSuHDUJ.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-fj60rQ3y.mjs";
3
+ import { E as agentWithSkills, H as fixReviewNotesPrompt, V as finisherSystem, ct as defaults, g as REVIEW, i as skills, lt as envDefaults, n as summary, o as shipped, r as access, s as baseline, t as steering, v as coder } from "../chunks/reserved-BIsngMGL.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-DHEaOHyp.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,6 +1,6 @@
1
1
  import { start } from "../flows.mjs";
2
- import { c as envDefaults, d as gate, i as skills, n as summary, nt as reviewGateBlockedMessage, o as shipped, r as access, s as defaults, t as steering } from "../chunks/reserved-UdSuHDUJ.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-fj60rQ3y.mjs";
2
+ import { ct as defaults, et as reviewGateBlockedMessage, i as skills, l as gate, lt as envDefaults, n as summary, o as shipped, r as access, t as steering } from "../chunks/reserved-BIsngMGL.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DHEaOHyp.mjs";
4
4
  //#region src/workflows/review/review.ts
5
5
  /**
6
6
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -1,9 +1,23 @@
1
- import { agent, changesSince, head, read, refuse, scope, vars } from "../flows.mjs";
2
- import { a as flagShipped, b as coder, c as envDefaults, d as gate, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-UdSuHDUJ.mjs";
3
- import { i as requireFlag, n as flipProblems } from "../chunks/registry-CP3YuV9j.mjs";
1
+ import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
+ import { a as flagShipped, ct as defaults, ft as flipProblems, i as skills, l as gate, lt as envDefaults, mt as requireFlag, n as summary, p as FEEDBACK, r as access, t as steering, ut as flagMentions, v as coder } from "../chunks/reserved-BIsngMGL.mjs";
4
3
  //#region src/workflows/rollout/rollout.ts
5
4
  const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
6
5
  const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
6
+ const MARKED_MENTIONS = 50;
7
+ const listMentions = async (registry, name, guard) => {
8
+ await run("mentions", removeScript([FEEDBACK]), { label: "Listing flag mentions" });
9
+ guard();
10
+ const mentions = flagMentions(glob("**").filter((path) => path !== registry && path !== ".gtd" && !path.startsWith(".gtd/")).sort().map((path) => ({
11
+ path,
12
+ text: read(path) ?? ""
13
+ })), name);
14
+ mark("flag-mentions", String(mentions.length));
15
+ for (const m of mentions.slice(0, MARKED_MENTIONS)) try {
16
+ mark("flag-mention", `${m.path}:${m.line}`);
17
+ } catch {
18
+ continue;
19
+ }
20
+ };
7
21
  const rolloutFlow = async () => {
8
22
  const registry = vars.flagRegistry ?? "";
9
23
  const name = vars.flag ?? "";
@@ -23,9 +37,10 @@ const rolloutFlow = async () => {
23
37
  });
24
38
  guard();
25
39
  await gate("suite", suiteBlockedMessage(), guard);
40
+ await listMentions(registry, name, guard);
26
41
  });
27
42
  };
28
- const rollout = flagShipped(rolloutFlow);
43
+ const rollout = flagShipped(rolloutFlow, "rollout");
29
44
  //#endregion
30
45
  //#region src/workflows/rollout/index.ts
31
46
  var rollout_default = rollout;