@pmelab/gtd 27.9.0 → 28.1.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 (43) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +53 -27
  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-DeBS4Ymh.mjs} +178 -32
  7. package/dist/chunks/{tail-fj60rQ3y.mjs → tail-B2SER2Ih.mjs} +1 -1
  8. package/dist/gtd.bundle.mjs +209 -72
  9. package/dist/testkit.mjs +209 -72
  10. package/dist/workflows/cleanup.mjs +3 -4
  11. package/dist/workflows/feature.mjs +2 -2
  12. package/dist/workflows/fix.mjs +2 -2
  13. package/dist/workflows/review.mjs +2 -2
  14. package/dist/workflows/rollout.mjs +19 -4
  15. package/dist/workflows.mjs +196 -132
  16. package/package.json +1 -1
  17. package/skills/authoring/SKILL.md +4 -3
  18. package/src/cli/Cli.test.ts +39 -0
  19. package/src/cli/Cli.ts +41 -9
  20. package/src/cli/Program.test.ts +31 -0
  21. package/src/cli/Program.ts +14 -7
  22. package/src/edge/Edge.test.ts +259 -3
  23. package/src/edge/Edge.ts +175 -58
  24. package/src/flows/runtime.ts +20 -1
  25. package/src/install/ConfigScaffold.ts +4 -1
  26. package/src/install/Install.test.ts +10 -0
  27. package/src/install/Install.ts +5 -2
  28. package/src/workflow/load.test.ts +82 -5
  29. package/src/workflow/load.ts +40 -16
  30. package/src/workflows/cleanup/cleanup.feature +21 -0
  31. package/src/workflows/cleanup/cleanup.ts +1 -1
  32. package/src/workflows/index.ts +2 -1
  33. package/src/workflows/rollout/rollout.feature +282 -2
  34. package/src/workflows/rollout/rollout.test.ts +96 -2
  35. package/src/workflows/rollout/rollout.ts +41 -3
  36. package/src/workflows/shared/options.test.ts +25 -2
  37. package/src/workflows/shared/options.ts +7 -4
  38. package/src/workflows/shared/registry.test.ts +41 -1
  39. package/src/workflows/shared/registry.ts +26 -1
  40. package/src/workflows/shared/text.test.ts +377 -2
  41. package/src/workflows/shared/text.ts +65 -7
  42. package/src/workflows/shared/vars.ts +2 -0
  43. package/dist/chunks/registry-CP3YuV9j.mjs +0 -101
package/dist/testkit.mjs CHANGED
@@ -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,
@@ -8104,10 +8110,10 @@ const doorsFrom = (exports) => {
8104
8110
  };
8105
8111
  /**
8106
8112
  * Each exported workflow's own doors, onto the name it is exported under. The
8107
- * `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.
8108
8114
  */
8109
8115
  const derivedDoors = (catalogue, declared) => {
8110
- const doors = /* @__PURE__ */ new Map();
8116
+ const builders = /* @__PURE__ */ new Map();
8111
8117
  for (const [workflow, loaded] of catalogue) for (const [name, spec] of Object.entries(loaded.doors ?? {})) {
8112
8118
  const door = {
8113
8119
  ...spec,
@@ -8115,12 +8121,22 @@ const derivedDoors = (catalogue, declared) => {
8115
8121
  };
8116
8122
  const problem = doorProblem(name, door);
8117
8123
  if (problem !== void 0) throw new Error(`workflow "${workflow}" builds door "${name}", which ${problem}`);
8118
- const other = doors.get(name);
8119
- 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)`);
8120
- 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));
8121
8132
  }
8122
- return new Map([...doors, ...declared]);
8133
+ return {
8134
+ doors: new Map([...doors, ...declared]),
8135
+ clashes
8136
+ };
8123
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);
8124
8140
  /** Sort the doors and check each names a startable workflow. */
8125
8141
  const checkDoors = (declared, catalogue) => {
8126
8142
  const doors = new Map([...declared].sort(([a], [b]) => a.localeCompare(b)));
@@ -8151,16 +8167,28 @@ const loadWorkflows = (module, shipped) => {
8151
8167
  return workflow === void 0 ? [] : [[name, workflow]];
8152
8168
  }));
8153
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));
8154
8174
  return {
8155
8175
  file,
8156
8176
  catalogue,
8157
- doors: rejections.length > 0 ? /* @__PURE__ */ new Map() : checkDoors(derivedDoors(catalogue, doorsFrom(exports)), catalogue),
8177
+ doors: checkDoors(derived.doors, catalogue),
8158
8178
  default: defaultWorkflow && {
8159
8179
  ...defaultWorkflow,
8160
8180
  name: defaultName
8161
8181
  },
8162
8182
  config: compileConfig(exports["config"], module.filepath),
8163
- 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
+ })]
8164
8192
  };
8165
8193
  };
8166
8194
  const evaluateFromDisk = (module, shipped) => {
@@ -8698,9 +8726,10 @@ const traceEntryOf = (hash, message) => ({
8698
8726
  hash,
8699
8727
  actor: message.parsed?.actor ?? ""
8700
8728
  });
8701
- const costEntriesOf = (message) => message.cost.map((c) => ({
8729
+ const costEntriesOf = (hash, message) => message.cost.map((c) => ({
8702
8730
  cost: c.cost,
8703
- model: c.model ?? "unspecified"
8731
+ model: c.model ?? "unspecified",
8732
+ hash
8704
8733
  }));
8705
8734
  const hashAt = (history, index) => history[index]?.hash;
8706
8735
  const historyFormatFault = (history, location) => formatFault(history.slice(location.baseIndex + 1).concat(history[location.baseIndex]?.message.startsWith("gtd") ? [history[location.baseIndex]] : []).map((c) => ({
@@ -8740,7 +8769,7 @@ const buildRun = (history, location, closingHash) => {
8740
8769
  startParentHash,
8741
8770
  diffBase: first?.reviewBase ?? startParentHash,
8742
8771
  trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
8743
- costEntries: parsed.flatMap(costEntriesOf),
8772
+ costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
8744
8773
  judgeVerdicts: parsed.flatMap((m) => m.judge),
8745
8774
  marks: parsed.flatMap((m) => m.marks),
8746
8775
  approvals: processCommits.flatMap((c, i) => approvalEntryOf(c.hash, parsed[i])),
@@ -9097,6 +9126,10 @@ const costByModel = (entries) => {
9097
9126
  cost
9098
9127
  })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
9099
9128
  };
9129
+ const costTotals = (entries) => ({
9130
+ total: entries.reduce((sum, entry) => sum + entry.cost, 0),
9131
+ byModel: costByModel(entries)
9132
+ });
9100
9133
  const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(function* () {
9101
9134
  const resolved = yield* workflowOf(config, run);
9102
9135
  const def = resolved.def;
@@ -9200,57 +9233,69 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
9200
9233
  memory: memory.key,
9201
9234
  memoryResumed: memory.resumed,
9202
9235
  hints: hintsOf(stepDef),
9203
- cost: {
9204
- total: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
9205
- byModel: costByModel(run.costEntries)
9206
- },
9236
+ cost: costTotals(run.costEntries),
9207
9237
  setup
9208
9238
  };
9209
9239
  });
9210
9240
  const currentRest = restAt(void 0);
9241
+ /** Every slice refusal reads `<command>: refused — <reason>`; replay's own `gtd: ` lead is dropped. */
9242
+ const slicesDivergence = (command, run, outcome) => {
9243
+ 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";
9244
+ return /* @__PURE__ */ new Error(`${command}: refused — ${reason}`);
9245
+ };
9211
9246
  /**
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.
9247
+ * The replay `gtd slices` and `gtd summary --slice` share: the slice ranges of
9248
+ * `run`, plus whether HEAD closes it. Reads like `historyTrail`, never
9249
+ * `restAt`: it narrates and writes nothing.
9215
9250
  */
9216
- const processSlices = gen(function* () {
9217
- const git = yield* GitService;
9218
- const config = yield* (yield* ConfigService).load;
9251
+ const replayedSlices = (config, run, how) => gen(function* () {
9219
9252
  const workspace = yield* Workspace;
9220
9253
  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"));
9254
+ if (run.trace.length === 0) return yield* fail$5(/* @__PURE__ */ new Error(`${how.command}: refused — HEAD neither closes nor sits inside a process`));
9223
9255
  const closing = run.closingHash;
9224
9256
  let outcome;
9225
9257
  let versionWarning;
9226
9258
  if (closing !== void 0) {
9227
9259
  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)
9260
+ command: how.command,
9261
+ configReason: how.configReason,
9262
+ notEnded: (o) => slicesDivergence(how.command, run, o)
9231
9263
  });
9232
9264
  outcome = replayed.outcome;
9233
9265
  versionWarning = replayed.versionWarning;
9234
9266
  } else {
9235
9267
  const { setup } = yield* setupFor(config, run, workspace, host);
9236
9268
  outcome = yield* promise(() => replayFor(setup));
9237
- if (outcome.kind !== "rest") return yield* fail$5(slicesDivergence(run, outcome));
9269
+ if (outcome.kind !== "rest") return yield* fail$5(slicesDivergence(how.command, run, outcome));
9238
9270
  }
9239
9271
  return {
9240
- slices: sliceRangesOf(outcome.slices, run).map(({ name, base, tip, open }) => ({
9272
+ ranges: sliceRangesOf(outcome.slices, run),
9273
+ closed: closing !== void 0,
9274
+ ...versionWarning !== void 0 ? { versionWarning } : {}
9275
+ };
9276
+ });
9277
+ /**
9278
+ * The slices of the process HEAD closes or sits inside, current episode only,
9279
+ * stack order. Returns a version warning for a closed run.
9280
+ */
9281
+ const processSlices = gen(function* () {
9282
+ const git = yield* GitService;
9283
+ const config = yield* (yield* ConfigService).load;
9284
+ const run = yield* runAtHead(git, config.workflow);
9285
+ const { ranges, closed, versionWarning } = yield* replayedSlices(config, run, {
9286
+ command: "gtd slices",
9287
+ configReason: "HEAD's process replays under today's config; revert or stash the edit to list its slices"
9288
+ });
9289
+ return {
9290
+ slices: ranges.map(({ name, base, tip, open }) => ({
9241
9291
  name,
9242
9292
  base,
9243
9293
  tip,
9244
- closed: closing !== void 0 || !open
9294
+ closed: closed || !open
9245
9295
  })),
9246
9296
  ...versionWarning !== void 0 ? { versionWarning } : {}
9247
9297
  };
9248
9298
  });
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
9299
  /**
9255
9300
  * Whether `gtd --workflow <name>` can start a process here: the workflow must
9256
9301
  * reach a step from the tree the opening commit would capture. That step is
@@ -9457,27 +9502,85 @@ const snapshotFromRest = (rest, verdicts) => gen(function* () {
9457
9502
  landing
9458
9503
  };
9459
9504
  });
9460
- /** `gtd summary`'s prompt for `run`, or `undefined` when the workflow has none or there is nothing to summarize. */
9461
- const summaryFor = (run) => gen(function* () {
9462
- const config = yield* (yield* ConfigService).load;
9463
- const host = yield* Host;
9464
- const resolved = yield* workflowOf(config, run);
9465
- const summary = resolved.def.summary;
9466
- if (summary === void 0 || run.trace.length === 0) return void 0;
9505
+ const narrowToSlice = (run, slice) => {
9506
+ if (slice === void 0) return {
9507
+ trace: run.trace,
9508
+ costEntries: run.costEntries
9509
+ };
9510
+ const from = slice.base === run.startParentHash ? -1 : run.trace.findIndex((e) => e.hash === slice.base);
9511
+ const to = run.trace.findIndex((e) => e.hash === slice.tip);
9512
+ const trace = run.trace.slice(from + 1, to + 1);
9513
+ const hashes = new Set(trace.map((e) => e.hash));
9514
+ return {
9515
+ trace,
9516
+ costEntries: run.costEntries.filter((c) => hashes.has(c.hash))
9517
+ };
9518
+ };
9519
+ const humanCommitsOf = (trace, entryCommit) => trace.filter((entry) => entry.actor === "human" && entry.hash !== entryCommit).map((entry) => ({
9520
+ hash: entry.hash,
9521
+ state: entry.state
9522
+ }));
9523
+ const summaryInput = (run, slice, settings) => {
9467
9524
  const entryCommit = run.trace[0].hash;
9468
- return summary({
9525
+ const { trace, costEntries } = narrowToSlice(run, slice);
9526
+ const { total, byModel } = costTotals(costEntries);
9527
+ return {
9469
9528
  entryCommit,
9470
- processBase: run.startParentHash,
9471
- processTip: run.trace[run.trace.length - 1].hash,
9472
- humanCommits: run.trace.filter((entry) => entry.actor === "human" && entry.hash !== entryCommit).map((entry) => ({
9473
- hash: entry.hash,
9474
- state: entry.state
9475
- })),
9476
- processCost: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
9477
- processCostByModel: costByModel(run.costEntries),
9529
+ processBase: slice?.base ?? run.startParentHash,
9530
+ processTip: slice?.tip ?? run.trace[run.trace.length - 1].hash,
9531
+ humanCommits: humanCommitsOf(trace, entryCommit),
9532
+ processCost: total,
9533
+ processCostByModel: byModel,
9478
9534
  marks: run.marks,
9479
- ...settingsFor(resolved, run.pinnedVars, host.env, {}, {})
9535
+ ...settings,
9536
+ ...slice !== void 0 ? { slice } : {}
9537
+ };
9538
+ };
9539
+ const locateSlice = (config, run, sliceName) => gen(function* () {
9540
+ if (sliceName === void 0) return {};
9541
+ const { ranges, versionWarning } = yield* replayedSlices(config, run, {
9542
+ command: "gtd summary",
9543
+ configReason: "HEAD's process replays under today's config; revert or stash the edit to summarize its slices"
9480
9544
  });
9545
+ const warn = versionWarning !== void 0 ? { versionWarning } : {};
9546
+ const at = ranges.findIndex((r) => r.name === sliceName);
9547
+ if (at < 0) return {
9548
+ unknownSlice: `gtd summary: refused — no slice named "${sliceName}"; the slices, in stack order: ${ranges.map((r) => r.name).join(", ")}`,
9549
+ ...warn
9550
+ };
9551
+ const { base, tip } = ranges[at];
9552
+ return {
9553
+ slice: {
9554
+ name: sliceName,
9555
+ base,
9556
+ tip,
9557
+ index: at + 1,
9558
+ count: ranges.length
9559
+ },
9560
+ ...warn
9561
+ };
9562
+ });
9563
+ /**
9564
+ * A plain call never replays; under a slice, `entryCommit` and `marks` stay
9565
+ * whole-process.
9566
+ */
9567
+ const summaryFor = (run, sliceName) => gen(function* () {
9568
+ const config = yield* (yield* ConfigService).load;
9569
+ const host = yield* Host;
9570
+ const resolved = yield* workflowOf(config, run);
9571
+ const summary = resolved.def.summary;
9572
+ if (summary === void 0) return { prompt: void 0 };
9573
+ if (sliceName === void 0 && run.trace.length === 0) return { prompt: void 0 };
9574
+ const located = yield* locateSlice(config, run, sliceName);
9575
+ if ("unknownSlice" in located) return {
9576
+ prompt: void 0,
9577
+ ...located
9578
+ };
9579
+ const { slice, versionWarning } = located;
9580
+ return {
9581
+ prompt: summary(summaryInput(run, slice, settingsFor(resolved, run.pinnedVars, host.env, {}, {}))),
9582
+ ...versionWarning !== void 0 ? { versionWarning } : {}
9583
+ };
9481
9584
  });
9482
9585
  /** Repository-relative paths of the config files today's config and the process's own record name; one outside the repository cannot be read at a ref, so it is left out. */
9483
9586
  const inRepoConfigPaths = (root, files, recorded) => {
@@ -9542,13 +9645,14 @@ const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
9542
9645
  const git = yield* GitService;
9543
9646
  const workspace = yield* Workspace;
9544
9647
  const host = yield* Host;
9648
+ const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
9649
+ const versionWarning = versionWarningFor(recorded);
9650
+ const withWarning = (err) => versionWarning === void 0 ? err : /* @__PURE__ */ new Error(`${err.message}\n${versionWarning}`);
9545
9651
  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));
9652
+ if (differing.length > 0) return yield* fail$5(withWarning(configRefusal(how.command, hash, differing, how.configReason)));
9547
9653
  const { setup } = yield* setupFor(config, run, workspace, host);
9548
9654
  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);
9655
+ if (outcome.kind !== "ended") return yield* fail$5(withWarning(how.notEnded(outcome)));
9552
9656
  return {
9553
9657
  outcome,
9554
9658
  recorded,
@@ -10237,12 +10341,16 @@ const planLanding = (opts = {}, current) => gen(function* () {
10237
10341
  * closes or sits inside its own closing message. Writes nothing — no git, no
10238
10342
  * state transition, no file. Refuses (throws, mapped to the runtime-error
10239
10343
  * exit code) when the workflow declares no `summary` prompt, or the resolved
10240
- * run has no commits to name.
10344
+ * run has no commits to name. The edge never narrates: the CLI prints the
10345
+ * version warning, before the unknown-slice refusal; a replay refusal carries
10346
+ * it in its message.
10241
10347
  */
10242
- const runSummaryCommand = (out) => gen(function* () {
10243
- const rendered = yield* summaryFor(yield* summaryRun);
10244
- if (rendered === void 0) return yield* fail$5(/* @__PURE__ */ new Error("gtd summary: refused — either this workflow declares no \"summary:\" template, or there is no finished/in-flight process to summarize at HEAD"));
10245
- out.write(rendered.endsWith("\n") ? rendered : rendered + "\n");
10348
+ const runSummaryCommand = (out, sliceName) => gen(function* () {
10349
+ const { prompt, versionWarning, unknownSlice } = yield* summaryFor(yield* summaryRun, sliceName);
10350
+ if (versionWarning !== void 0) yield* (yield* Narrator).warn(versionWarning);
10351
+ if (unknownSlice !== void 0) return yield* fail$5(new Error(unknownSlice));
10352
+ if (prompt === void 0) return yield* fail$5(/* @__PURE__ */ new Error("gtd summary: refused — either this workflow declares no \"summary:\" template, or there is no finished/in-flight process to summarize at HEAD"));
10353
+ out.write(prompt.endsWith("\n") ? prompt : prompt + "\n");
10246
10354
  });
10247
10355
  /** Prints the review anchor hash bare, so an external tool can be pointed at the range under review. Nothing is written. Refuses when the run has no commits, where the hash would name a range with no review. */
10248
10356
  const runBaseCommand = (out) => gen(function* () {
@@ -10780,7 +10888,7 @@ const dispatchVoidCommand = (command, json, out) => {
10780
10888
  case "uncheck": return runUncheckCommand(command.file);
10781
10889
  case "install": return runInstallCommand(out);
10782
10890
  case "loop": return runLoopCommand();
10783
- case "summary": return runSummaryCommand(out);
10891
+ case "summary": return runSummaryCommand(out, command.slice);
10784
10892
  case "base": return runBaseCommand(out);
10785
10893
  case "judge": return runJudgeCommand(json, out);
10786
10894
  case "judgeAnswer": return runJudgeAnswerCommand(json, out);
@@ -10948,6 +11056,21 @@ const FLAGS = [
10948
11056
  "GTD_JUDGE_MODEL, then judge.json, then sonnet"
10949
11057
  ]
10950
11058
  },
11059
+ {
11060
+ name: "--slice",
11061
+ arity: 1,
11062
+ repeatable: false,
11063
+ scope: (kind) => kind === "summary",
11064
+ decode: ([raw]) => right(raw),
11065
+ scopeError: "gtd: --slice is only valid for `gtd summary`",
11066
+ valueHint: "<name>",
11067
+ help: [
11068
+ "(gtd summary only) the closing-message prompt for one",
11069
+ "slice, named as `gtd slices` lists it: its own range, human",
11070
+ "commits and cost. An unknown name is refused (exit 1) with",
11071
+ "every valid slice listed"
11072
+ ]
11073
+ },
10951
11074
  {
10952
11075
  name: "--workflow",
10953
11076
  arity: 1,
@@ -11252,10 +11375,15 @@ const COMMAND_ROWS = [
11252
11375
  "no state transition, no file, no session identity — the",
11253
11376
  "driver pipes the output to a cold agent and does what it",
11254
11377
  "wants with the result (a squash, an amend, a PR body).",
11255
- "Refuses (exit 1) when the workflow declares no summary:",
11256
- "template, or when the resolved run has no commits to name",
11257
- "— runnable any time before the next thing lands on the",
11258
- "branch"
11378
+ "--slice <name> prints it for one slice instead: its own",
11379
+ "range, human commits and cost (see `gtd slices`); it also",
11380
+ "refuses an unknown slice name, and a HEAD-closing process",
11381
+ "whose config files differ from the working tree or whose",
11382
+ "replay diverged; an open last slice is provisional. Refuses",
11383
+ "(exit 1) when the workflow declares no summary: template,",
11384
+ "or when the resolved run has no commits to name — plain",
11385
+ "gtd summary is runnable any time before the next thing",
11386
+ "lands on the branch"
11259
11387
  ]
11260
11388
  },
11261
11389
  {
@@ -11284,10 +11412,10 @@ const COMMAND_ROWS = [
11284
11412
  "positional args to that workflow's process settings — the",
11285
11413
  "same start `gtd --workflow` makes, printed the same way.",
11286
11414
  "Each workflow the config exports brings its own doors (gtd's",
11287
- "`fix` brings `fix`, `review` brings `review [base]`), plus",
11288
- "the config's `doors` export. An unknown door, or too few or",
11289
- "too many args, is a usage error (exit 2) naming the doors or",
11290
- "the door's synopsis"
11415
+ "`fix`, `review [base]`, `rollout <flag>` and",
11416
+ "`cleanup <flag>`), plus the config's `doors` export. An",
11417
+ "unknown door, or too few or too many args, is a usage error",
11418
+ "(exit 2) naming the doors or the door's synopsis"
11291
11419
  ]
11292
11420
  },
11293
11421
  {
@@ -11712,6 +11840,15 @@ const parseArgv = (argv) => {
11712
11840
  verbose
11713
11841
  };
11714
11842
  }
11843
+ if (kind === "summary") return {
11844
+ kind: "command",
11845
+ command: {
11846
+ kind: "summary",
11847
+ ...bag["--slice"] !== void 0 ? { slice: bag["--slice"] } : {}
11848
+ },
11849
+ json,
11850
+ verbose
11851
+ };
11715
11852
  if (kind === "historyShow") return {
11716
11853
  kind: "command",
11717
11854
  command: {
@@ -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-DeBS4Ymh.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-B2SER2Ih.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-DeBS4Ymh.mjs";
4
+ import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-B2SER2Ih.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-DeBS4Ymh.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-B2SER2Ih.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-DeBS4Ymh.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-B2SER2Ih.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-DeBS4Ymh.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;