@pmelab/gtd 52.0.0 → 53.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 (108) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +96 -131
  3. package/claude/hooks/drive.ts +0 -3
  4. package/claude/hooks/register.tsx +3 -68
  5. package/claude/hooks/ship.ts +85 -1595
  6. package/dist/chunks/{reserved-D0MKVFFa.mjs → reserved-CQ7amcKG.mjs} +87 -133
  7. package/dist/chunks/{review-ZzUQH1KZ.mjs → review-CNgN_V8T.mjs} +2 -2
  8. package/dist/chunks/{tail-I1-vVq0P.mjs → tail-JZ9PAOwp.mjs} +2 -14
  9. package/dist/chunks/{wire-BJuZWR3C.mjs → wire-C12dahrH.mjs} +2 -130
  10. package/dist/flows.mjs +1 -16
  11. package/dist/gtd.bundle.mjs +476 -982
  12. package/dist/testkit.mjs +58 -417
  13. package/dist/wire.mjs +2 -2
  14. package/dist/workflows/cleanup.mjs +2 -2
  15. package/dist/workflows/feature.mjs +159 -363
  16. package/dist/workflows/fix.mjs +2 -2
  17. package/dist/workflows/review.mjs +3 -3
  18. package/dist/workflows/rollout.mjs +1 -1
  19. package/dist/workflows.mjs +400 -675
  20. package/package.json +5 -4
  21. package/src/cli/Cli.test.ts +1 -86
  22. package/src/cli/Cli.ts +13 -133
  23. package/src/cli/Metrics.test.ts +21 -31
  24. package/src/cli/Metrics.ts +25 -36
  25. package/src/cli/Program.test.ts +1 -35
  26. package/src/cli/Program.ts +5 -64
  27. package/src/cli/ToolCommands.ts +0 -3
  28. package/src/edge/Edge.test.ts +20 -322
  29. package/src/edge/Edge.ts +10 -181
  30. package/src/edge/index.ts +0 -1
  31. package/src/flows/runtime.test.ts +0 -27
  32. package/src/flows/runtime.ts +1 -49
  33. package/src/install/Install.ts +15 -24
  34. package/src/replay/Replay.test.ts +0 -169
  35. package/src/replay/Replay.ts +4 -93
  36. package/src/replay/index.ts +0 -1
  37. package/src/testkit/Layers.ts +0 -1
  38. package/src/testkit/steps.ts +13 -0
  39. package/src/wire/BeatDocument.ts +2 -12
  40. package/src/wire/BeatStatus.ts +0 -3
  41. package/src/wire/WireSchema.test.ts +0 -18
  42. package/src/wire/index.ts +0 -2
  43. package/src/wire/types.ts +0 -7
  44. package/src/workflow/Workflow.ts +1 -9
  45. package/src/workflows/feature/acceptance-criteria.feature +159 -3
  46. package/src/workflows/feature/criteria.test.ts +23 -15
  47. package/src/workflows/feature/criteria.ts +72 -39
  48. package/src/workflows/feature/fast-path.feature +4 -0
  49. package/src/workflows/feature/feature.test.ts +6 -5
  50. package/src/workflows/feature/feature.ts +34 -118
  51. package/src/workflows/feature/flag-mode.feature +4 -0
  52. package/src/workflows/feature/hands-on-skip.feature +37 -65
  53. package/src/workflows/feature/index.ts +0 -1
  54. package/src/workflows/feature/lens-models.feature +4 -0
  55. package/src/workflows/feature/lens-review.feature +4 -0
  56. package/src/workflows/feature/naive-acceptance.feature +4 -0
  57. package/src/workflows/feature/one-step-land.feature +4 -4
  58. package/src/workflows/feature/planning.ts +2 -1
  59. package/src/workflows/feature/prototype-rounds.feature +5 -1
  60. package/src/workflows/feature/prototype.feature +11 -3
  61. package/src/workflows/feature/prototype.ts +18 -29
  62. package/src/workflows/feature/refactor-absorbs.feature +7 -8
  63. package/src/workflows/feature/refactor-first.feature +4 -5
  64. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  65. package/src/workflows/feature/refactor-rebuild.feature +4 -6
  66. package/src/workflows/feature/refactor-review.feature +31 -11
  67. package/src/workflows/feature/refactor.ts +30 -64
  68. package/src/workflows/feature/review-answers.feature +4 -0
  69. package/src/workflows/feature/review-gate.feature +13 -1
  70. package/src/workflows/feature/reviewer-walkthrough.feature +4 -0
  71. package/src/workflows/feature/risk-tier.feature +32 -0
  72. package/src/workflows/feature/seam-rejection.feature +8 -4
  73. package/src/workflows/feature/seam.feature +8 -0
  74. package/src/workflows/feature/seam.test.ts +11 -28
  75. package/src/workflows/feature/seam.ts +52 -165
  76. package/src/workflows/feature/severity-budget.feature +4 -0
  77. package/src/workflows/feature/severity-sorted-review.feature +4 -0
  78. package/src/workflows/feature/standards.feature +4 -0
  79. package/src/workflows/feature/tier-pin.feature +4 -0
  80. package/src/workflows/index.ts +0 -1
  81. package/src/workflows/shared/lenses.ts +0 -13
  82. package/src/workflows/shared/options.test.ts +2 -11
  83. package/src/workflows/shared/options.ts +4 -21
  84. package/src/workflows/shared/review.test.ts +3 -3
  85. package/src/workflows/shared/steps.ts +0 -2
  86. package/src/workflows/shared/text.test.ts +41 -71
  87. package/src/workflows/shared/text.ts +82 -212
  88. package/src/workflows/shared/vars.ts +2 -8
  89. package/src/workflows/shared/workflows.steps.ts +0 -13
  90. package/claude/hooks/context.ts +0 -66
  91. package/src/replay/Slices.test.ts +0 -73
  92. package/src/replay/Slices.ts +0 -45
  93. package/src/wire/ContextLimit.fixture.ts +0 -30
  94. package/src/wire/ContextLimit.test.ts +0 -96
  95. package/src/wire/ContextLimit.ts +0 -169
  96. package/src/wire/Slices.ts +0 -25
  97. package/src/wire/golden/1/slices.json +0 -11
  98. package/src/workflows/feature/context-limit.feature +0 -37
  99. package/src/workflows/feature/continuation-cap.feature +0 -65
  100. package/src/workflows/feature/continuation-rebuild.feature +0 -88
  101. package/src/workflows/feature/continuation-slices.feature +0 -64
  102. package/src/workflows/feature/continuation.feature +0 -205
  103. package/src/workflows/feature/slice-findings.feature +0 -252
  104. package/src/workflows/feature/slicing-cap.feature +0 -23
  105. package/src/workflows/feature/slicing.feature +0 -265
  106. package/src/workflows/feature/slicing.test.ts +0 -49
  107. package/src/workflows/feature/slicing.ts +0 -105
  108. package/src/workflows/shared/continuation.ts +0 -92
@@ -1,8 +1,8 @@
1
- import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
1
+ import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, start, vars, wrote } from "../flows.mjs";
2
2
  import { lensEntriesOf } from "../workflow-kit.mjs";
3
- import { $ as normaliseCriteriaScript, At as unwindFailure, C as coder, Ct as seamSteerMessage, E as fixNits, Et as testerSystem, Ft as envDefaults, H as designGateAnswerMessage, I as builderSystem, It as testFileGlobs, J as idleMessage, N as agentWithSkills, P as buildReviewCollectingPrompt, Pt as defaults, Q as naivePassPrompt, R as clearCriteriaMessage, St as seamByHandMessage, Tt as startGateBlockedMessage, U as designSystem, V as criterionLine, W as designTriagePrompt, X as implementFixPrompt, Y as implementBuildingPrompt, Z as naiveApplyFailure, _ as REFACTOR_NOTE, a as flagModeOn, at as prototypeTestingPrompt, b as SLICES, d as healthy, dt as refactorDroppedRefusal, et as observableMessage, f as FEEDBACK, ft as refactorFixPrompt, g as REFACTOR, i as skills, it as prototypeOpenMessage, k as planner, kt as unwindFailedMessage, l as gate, m as PROTOTYPE, mt as refactorRules, n as summary, nt as oversizedMessage, ot as prototypeUnguidedMessage, p as HANDOFF, pt as refactorNotePrompt, r as access, rt as prototypeFixingPrompt, s as shipped, t as steering, tt as observableQuestion, ut as refactorBuildingPrompt, v as REQUIREMENTS, w as collecting, wt as slicingPrompt, x as answerReviewQuestions, xt as reviewerSystem, z as clearCriteriaQuestion } from "../chunks/reserved-D0MKVFFa.mjs";
4
- import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-I1-vVq0P.mjs";
5
- import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-ZzUQH1KZ.mjs";
3
+ import { $ as observableMessage, B as designGateAnswerMessage, Ct as testerSystem, D as planner, Dt as unwindFailure, Et as unwindFailedMessage, H as designSystem, I as clearCriteriaMessage, J as implementBuildingPrompt, L as clearCriteriaQuestion, M as buildReviewCollectingPrompt, Mt as envDefaults, Nt as testFileGlobs, P as builderSystem, Q as normaliseCriteriaScript, S as collecting, St as startGateBlockedMessage, U as designTriagePrompt, V as designImpactPrompt, X as naiveApplyFailure, Y as implementFixPrompt, Z as naivePassPrompt, _ as REQUIREMENTS, a as flagModeOn, bt as seamByHandMessage, ct as refactorBuildingPrompt, d as healthy, dt as refactorNotePrompt, et as observableQuestion, f as FEEDBACK, ft as refactorRules, g as REFACTOR_NOTE, h as REFACTOR, i as skills, it as prototypeUnguidedMessage, j as agentWithSkills, jt as defaults, l as gate, lt as refactorDroppedRefusal, n as summary, nt as prototypeOpenMessage, p as PROTOTYPE, q as idleMessage, r as access, rt as prototypeTestingPrompt, s as shipped, t as steering, tt as prototypeFixingPrompt, ut as refactorFixPrompt, w as fixNits, x as coder, xt as seamSteerMessage, y as answerReviewQuestions, yt as reviewerSystem, z as criterionLine } from "../chunks/reserved-CQ7amcKG.mjs";
4
+ import { a as refusePendingHandOff, o as tail } from "../chunks/tail-JZ9PAOwp.mjs";
5
+ import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-CNgN_V8T.mjs";
6
6
  import { posix } from "node:path";
7
7
  //#region src/workflows/shared/fences.ts
8
8
  const markFenced = (text) => {
@@ -26,7 +26,7 @@ const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
26
26
  //#region src/workflows/feature/criteria.ts
27
27
  /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
28
28
  const CLEAR_MIN_P = .8;
29
- const ID$1 = /^(AC-\d+)(\s*\(no hands-on\))?:\s*(.*)$/;
29
+ const ID = /^(AC-\d+)(\s*\(no hands-on\))?:\s*(.*)$/;
30
30
  const criterion = (id, as = id[1]) => ({
31
31
  id: as,
32
32
  text: id[3],
@@ -59,24 +59,22 @@ const listUnder = (text, title, level) => {
59
59
  };
60
60
  /** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
61
61
  const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
62
- const id = ID$1.exec(item);
62
+ const id = ID.exec(item);
63
63
  return id === null ? {
64
64
  id: "",
65
65
  text: item
66
66
  } : criterion(id);
67
67
  });
68
- /** The ID'd criteria of `text`, only `slice`'s when given. */
69
- const criterionIds = (text, slice) => (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "" && (slice?.only === void 0 || slice.only.includes(id)) && !(slice?.except ?? []).includes(id));
70
- /** The ID'd criteria of `text` (`slice`'s when given) a person can try by hand: none tagged `(no hands-on)`. */
71
- const handsOnIds = (text, slice) => {
68
+ /** The ID'd criteria of `text`. */
69
+ const criterionIds = (text) => (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "");
70
+ /** The ID'd criteria of `text` a person can try by hand: none tagged `(no hands-on)`. */
71
+ const handsOnIds = (text) => {
72
72
  const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id);
73
- return criterionIds(text, slice).filter((id) => !tagged.includes(id));
73
+ return criterionIds(text).filter((id) => !tagged.includes(id));
74
74
  };
75
- /** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
76
- const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
77
75
  /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
78
76
  const numbered = (items) => {
79
- const ids = items.map((item) => ID$1.exec(item));
77
+ const ids = items.map((item) => ID.exec(item));
80
78
  const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
81
79
  return items.map((item, i) => {
82
80
  const id = ids[i];
@@ -121,6 +119,38 @@ const requireCriteria = (file) => {
121
119
  if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
122
120
  refuse(`gtd land: acceptance-criteria: ${file} has ${criteria.length === 0 ? `no "## Acceptance Criteria" list` : unnamed.length > 0 ? `${unnamed.length} criterion without an ID:\n${unnamed.map((c) => ` - ${c.text}`).join("\n")}` : `duplicate IDs: ${[...new Set(twice)].join(", ")}`} — list every criterion under "## Acceptance Criteria" as "- AC-<n>: <checkable criterion>", each ID unique and never reused`);
123
121
  };
122
+ const isImpactHeading = (h) => h !== null && h[1].length === 2 && /^impact$/i.test(h[2].trim());
123
+ /** Whether `text` has an unfenced, level-2 `## Impact` heading. */
124
+ const hasImpact = (text) => markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)));
125
+ /** `text` without its `## Impact` section, whitespace collapsed. */
126
+ const withoutImpact = (text) => {
127
+ let inImpact = false;
128
+ return markFenced(text).filter((l) => {
129
+ const h = l.fenced ? null : HEADING.exec(l.line);
130
+ if (h !== null && h[1].length <= 2) inImpact = isImpactHeading(h);
131
+ return !inImpact;
132
+ }).map((l) => l.line).join(" ").replace(/\s+/g, " ").trim();
133
+ };
134
+ /**
135
+ * Refuse a turn that leaves `file` without a `## Impact` section. Only the
136
+ * pending turn (`head()` is `""`): a triage lap recorded before Impact existed
137
+ * replays without one, and refusing there would fail every in-flight process.
138
+ * A human's edit at a later gate is not checked — the human owns that file then.
139
+ */
140
+ const requireImpact = (file) => {
141
+ if (head() !== "") return;
142
+ const text = read(file);
143
+ if (text === void 0) return;
144
+ if (hasImpact(text)) return;
145
+ refuse(`gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`);
146
+ };
147
+ /** Refuse a turn that changed `file` beyond its `## Impact` section. */
148
+ const requireImpactOnly = (file) => {
149
+ const change = changes().get(file);
150
+ if (change === void 0) return;
151
+ if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return;
152
+ refuse(`gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`);
153
+ };
124
154
  const idNumber = (id) => Number(id.slice(3));
125
155
  /** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
126
156
  const unstableIds = (listed, ids) => {
@@ -154,6 +184,19 @@ const clear = async (stated) => {
154
184
  });
155
185
  return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
156
186
  };
187
+ /** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
188
+ const impact = async () => {
189
+ await agentWithSkills("impact", designImpactPrompt(), {
190
+ label: "Mapping the change's impact",
191
+ file: REQUIREMENTS,
192
+ model: planner(),
193
+ system: designSystem()
194
+ });
195
+ requireProgress(REQUIREMENTS);
196
+ requireCriteriaFrozen(REQUIREMENTS);
197
+ requireImpact(REQUIREMENTS);
198
+ requireImpactOnly(REQUIREMENTS);
199
+ };
157
200
  /**
158
201
  * The skip path: when the sketch stated clear, checkable criteria, write them
159
202
  * to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
@@ -165,15 +208,9 @@ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scop
165
208
  label: "Recording the stated acceptance criteria",
166
209
  file: REQUIREMENTS
167
210
  });
211
+ await impact();
168
212
  return true;
169
213
  });
170
- /** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
171
- const requireCriteriaIfEdited = () => {
172
- const change = changes().get(REQUIREMENTS);
173
- if (change === void 0) return;
174
- if (change.status === "deleted") refuse(`gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`);
175
- requireCriteria(REQUIREMENTS);
176
- };
177
214
  //#endregion
178
215
  //#region src/workflows/feature/tier.ts
179
216
  /** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
@@ -261,6 +298,7 @@ const design = (base, guard) => scope("design", async () => {
261
298
  requireProgress(REQUIREMENTS);
262
299
  requireReplies(REQUIREMENTS);
263
300
  requireCriteria(REQUIREMENTS);
301
+ requireImpact(REQUIREMENTS);
264
302
  guard?.();
265
303
  } while (await noteGate(REQUIREMENTS, answerProductQuestions));
266
304
  });
@@ -338,12 +376,12 @@ const requireReport = (ids, retest) => {
338
376
  };
339
377
  };
340
378
  /**
341
- * The slice's criteria a person can observe by using the change, by the
342
- * judge: those tagged `(no hands-on)` are never asked about.
379
+ * The criteria a person can observe by using the change, by the judge: those
380
+ * tagged `(no hands-on)` are never asked about.
343
381
  */
344
- const observable = async (slice) => {
382
+ const observable = async () => {
345
383
  const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
346
- const own = handsOnIds(requirements, slice);
384
+ const own = handsOnIds(requirements);
347
385
  const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
348
386
  if (criteria.length === 0) return [];
349
387
  const { answers } = await judge("observable", {
@@ -355,10 +393,9 @@ const observable = async (slice) => {
355
393
  return own.filter((id) => !answered(answers[id], "no", UNOBSERVABLE_MIN_P));
356
394
  };
357
395
  const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
358
- const testing = async (slice, ids, retest) => {
359
- const skipped = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice).filter((id) => !ids.includes(id));
360
- await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), skipped, retest), {
361
- label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
396
+ const testing = async (ids, retest) => {
397
+ await agentWithSkills("testing", prototypeTestingPrompt(criterionIds(read(".gtd/REQUIREMENTS.md") ?? "").filter((id) => !ids.includes(id)), retest), {
398
+ label: retest ? "Re-testing the change by hand" : "Testing the change by hand",
362
399
  file: PROTOTYPE,
363
400
  model: coder(),
364
401
  system: testerSystem(),
@@ -366,8 +403,8 @@ const testing = async (slice, ids, retest) => {
366
403
  });
367
404
  return requireReport(ids, retest);
368
405
  };
369
- const fixing = async (slice, guard) => {
370
- await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
406
+ const fixing = async (guard) => {
407
+ await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(), {
371
408
  label: "Fixing what the hands-on test found",
372
409
  file: PROTOTYPE,
373
410
  model: coder(),
@@ -391,29 +428,29 @@ const askPerson = async (name, message, label) => {
391
428
  return true;
392
429
  };
393
430
  /**
394
- * Use the slice by hand: a testing turn reports on every criterion a person
395
- * can observe, a fix turn takes each failure, and the slice is tested again —
431
+ * Use the change by hand: a testing turn reports on every criterion a person
432
+ * can observe, a fix turn takes each failure, and the change is tested again —
396
433
  * until the report is clean, or `prototypeRounds` fixes are spent and a person
397
- * decides. A slice with no such criterion is not tested by hand. `guard` holds
398
- * every fix turn, as it holds the implementation's.
434
+ * decides. A change with no such criterion is not tested by hand. `guard`
435
+ * holds every fix turn, as it holds the implementation's.
399
436
  */
400
- const prototypeTest = async (slice, guard) => {
437
+ const prototypeTest = async (guard) => {
401
438
  const cap = prototypeRounds();
402
439
  if (cap === 0) return;
403
440
  await scope("prototype", async () => {
404
- const ids = await observable(slice);
441
+ const ids = await observable();
405
442
  if (ids.length === 0) return;
406
443
  let fixes = 0;
407
444
  let retest = false;
408
445
  for (;;) {
409
- const { unguided, open } = await testing(slice, ids, retest);
446
+ const { unguided, open } = await testing(ids, retest);
410
447
  retest = !unguided;
411
448
  if (unguided) {
412
449
  if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
413
450
  } else if (open.length === 0) return;
414
451
  else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
415
452
  fixes++;
416
- await fixing(slice, guard);
453
+ await fixing(guard);
417
454
  } else {
418
455
  if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
419
456
  fixes = 0;
@@ -423,53 +460,6 @@ const prototypeTest = async (slice, guard) => {
423
460
  });
424
461
  };
425
462
  //#endregion
426
- //#region src/workflows/shared/continuation.ts
427
- const phase = (name, steered) => ({
428
- name,
429
- spent: false,
430
- count: 0,
431
- stalled: 0,
432
- steered
433
- });
434
- const own = (fn) => fn();
435
- const continued = (fn) => scope("continue", fn);
436
- const afresh = (fn) => scope("fresh", fn);
437
- const setting = (name, least) => {
438
- const raw = vars[name] ?? defaults[name];
439
- const n = Number(raw);
440
- if (raw.trim() === "" || !Number.isInteger(n) || n < least) refuse(`gtd: ${name} must be a whole number of at least ${least}, got ${JSON.stringify(raw)}`);
441
- return n;
442
- };
443
- /**
444
- * Run `turn` until one ends without a handoff note: each stopped turn is picked
445
- * up by a continuation in a fresh conversation. Past `continuations` of them in
446
- * the phase, or after `continuationStall` in a row that close nothing still
447
- * open, the phase rests at `oversized` for a person; landing there grants a new round.
448
- */
449
- const continuing = async (phase, turn) => {
450
- const cap = setting("continuations", 0);
451
- const stall = setting("continuationStall", 1);
452
- let last = await turn(void 0, phase.spent ? afresh : own);
453
- while (last.handoff !== void 0) {
454
- phase.spent = true;
455
- const from = last.handoff;
456
- if (phase.count >= cap || phase.stalled >= stall) {
457
- await human("oversized", {
458
- message: oversizedMessage(phase.name, from, phase.count >= cap ? { cap } : { stall }, phase.steered !== void 0),
459
- label: "Too big for its context",
460
- acceptClean: true
461
- });
462
- phase.steered?.();
463
- phase.count = 0;
464
- phase.stalled = 0;
465
- }
466
- last = await turn(from, continued);
467
- phase.count++;
468
- phase.stalled = from.open.length === 0 || from.open.some((item) => !(last.handoff?.open ?? []).includes(item)) ? 0 : phase.stalled + 1;
469
- }
470
- return last.result;
471
- };
472
- //#endregion
473
463
  //#region src/workflows/feature/refactor.ts
474
464
  const REFACTOR_PACKAGE = REFACTOR;
475
465
  const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
@@ -615,7 +605,7 @@ const guardOf = ({ pattern, strays }, entry) => () => {
615
605
  requireNoNewFlags(pattern, entry);
616
606
  };
617
607
  /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
618
- const refactorLap = async (p, guard, refactor, feedback) => {
608
+ const refactorLap = async (p, guard, feedback) => {
619
609
  const { pattern, strays, reference, note } = p;
620
610
  await run("deriving", deriveScript([
621
611
  ...packageLines(strays, pattern, reference),
@@ -635,48 +625,32 @@ const refactorLap = async (p, guard, refactor, feedback) => {
635
625
  label: "Deriving the refactor package",
636
626
  file: REFACTOR_PACKAGE
637
627
  });
638
- const reached = /* @__PURE__ */ new Set();
639
- await continuing(refactor, async (from, fresh) => {
640
- await fresh(() => agentWithSkills("building", refactorBuildingPrompt({
641
- pkg: REFACTOR_PACKAGE,
642
- strays,
643
- pattern,
644
- reference,
645
- feedback: feedback !== void 0,
646
- note: note !== void 0,
647
- from
648
- }), {
649
- label: from === void 0 ? "Refactoring" : "Continuing the refactor",
628
+ await agentWithSkills("building", refactorBuildingPrompt({
629
+ pkg: REFACTOR_PACKAGE,
630
+ strays,
631
+ pattern,
632
+ reference,
633
+ feedback: feedback !== void 0,
634
+ note: note !== void 0
635
+ }), {
636
+ label: "Refactoring",
637
+ model: coder(),
638
+ system: builderSystem()
639
+ });
640
+ guard();
641
+ await healthy(async () => {
642
+ await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
643
+ label: "Fixing the check",
644
+ file: FEEDBACK,
650
645
  model: coder(),
651
646
  system: builderSystem()
652
- }));
653
- const left = read(HANDOFF);
654
- guard();
655
- const snapshot = head();
656
- for (const c of changes()) reached.add(c.path);
657
- await healthy(async () => {
658
- await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
659
- label: "Fixing the check",
660
- file: FEEDBACK,
661
- model: coder(),
662
- system: builderSystem()
663
- });
664
- guard();
665
- }, {
666
- describeGuard: guard,
667
- rules: refactorRules(strays, pattern)
668
647
  });
669
- if (left === void 0) return { result: void 0 };
670
- return {
671
- result: void 0,
672
- handoff: {
673
- snapshot,
674
- note: left.trim(),
675
- open: strays.filter((path) => !reached.has(path))
676
- }
677
- };
648
+ guard();
649
+ }, {
650
+ describeGuard: guard,
651
+ rules: refactorRules(strays, pattern)
678
652
  });
679
- await run("closing", removeScript([REFACTOR_PACKAGE, HANDOFF]), { label: "Closing out the refactor" });
653
+ await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
680
654
  };
681
655
  const strayAccess = (strays) => ({ write: [
682
656
  ...strays,
@@ -690,7 +664,7 @@ const refactorFirst = async (shape, options = {}) => {
690
664
  name: "refactor",
691
665
  access: strayAccess(p.strays)
692
666
  }, async () => {
693
- await refactorLap(p, guardOf(p, head()), phase("refactor"));
667
+ await refactorLap(p, guardOf(p, head()));
694
668
  return true;
695
669
  });
696
670
  };
@@ -715,10 +689,9 @@ const reviewedRefactor = async (shape, options = {}) => {
715
689
  unmatched: "gtd land: refactor-feedback: the review feedback changed files but wrote no .gtd/REFACTOR.md — a refactor cannot act on it; revert the change, or `gtd abandon` and restart",
716
690
  dropped: refactorDroppedRefusal
717
691
  };
718
- const refactor = phase("refactor");
719
692
  let collected;
720
693
  for (;;) {
721
- await refactorLap(p, guard, refactor, collected);
694
+ await refactorLap(p, guard, collected);
722
695
  const outcome = await buildTail(entry, {
723
696
  green: true,
724
697
  guard,
@@ -748,32 +721,12 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
748
721
  allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
749
722
  exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
750
723
  });
751
- const naivePass = (retry, slice, from) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice), from), {
752
- label: from !== void 0 ? "Continuing the naive pass" : retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
724
+ const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
725
+ label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
753
726
  keep: true,
754
727
  model: coder(),
755
728
  system: builderSystem()
756
729
  });
757
- const handoffOf = (tip, slice) => {
758
- const note = tip.changes.find((c) => c.path === ".gtd/HANDOFF.md" && c.after !== void 0);
759
- if (note === void 0) return void 0;
760
- return {
761
- snapshot: tip.hash,
762
- note: note.after.trim(),
763
- open: unnamed(idsOf(REQUIREMENTS, slice), acceptanceTests(tip.changes))
764
- };
765
- };
766
- /** A naive pass, continued until a pass ends without a handoff note: the snapshot is its kept tip. */
767
- const naiveContinued = (retry, slice, naive) => continuing(naive, async (from, fresh) => {
768
- const tip = await fresh(async () => {
769
- await naivePass(retry, slice, from);
770
- return kept("naive");
771
- });
772
- return {
773
- result: tip,
774
- handoff: handoffOf(tip, slice)
775
- };
776
- });
777
730
  const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
778
731
  const EXTENSION = /\.[\w-]+$/;
779
732
  const ALIAS = /^(?:[@~]\/|#)/;
@@ -809,7 +762,7 @@ const addedLines = (change) => {
809
762
  return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
810
763
  };
811
764
  const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
812
- const idsOf = (file, slice) => criterionIds(read(file) ?? "", slice);
765
+ const idsOf = (file) => criterionIds(read(file) ?? "");
813
766
  const unnamed = (ids, tests) => {
814
767
  const lines = tests.map(addedLines);
815
768
  return ids.filter((id) => !lines.some((text) => named(id, text)));
@@ -819,9 +772,9 @@ const unnamed = (ids, tests) => {
819
772
  * test line of the pass names, or a test importing a file the pass added —
820
773
  * naive structure a reset and refactor replaces, which the tests must survive.
821
774
  */
822
- const acceptanceFault = (file, tip, slice) => {
775
+ const acceptanceFault = (file, tip) => {
823
776
  const tests = acceptanceTests(tip.changes);
824
- const untested = unnamed(idsOf(file, slice), tests);
777
+ const untested = unnamed(idsOf(file), tests);
825
778
  if (untested.length > 0) return `acceptance-tests: no test of the naive pass names ${untested.join(", ")} — write a behaviour-level test for each criterion with its ID in the test or scenario name, e.g. "[${untested[0]}] …"`;
826
779
  const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
827
780
  const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
@@ -831,8 +784,8 @@ const acceptanceFault = (file, tip, slice) => {
831
784
  if (reaching.length > 0) return `acceptance-internals: these tests import files the naive pass added — test through code that existed before it (a public entry point, an existing module's exports):\n${reaching.join("\n")}`;
832
785
  };
833
786
  /** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
834
- const naiveAcceptance = (tip, slice) => {
835
- const ids = idsOf(REQUIREMENTS, slice);
787
+ const naiveAcceptance = (tip) => {
788
+ const ids = idsOf(REQUIREMENTS);
836
789
  const perTest = acceptanceTests(tip.changes).map((c) => {
837
790
  const lines = addedLines(c);
838
791
  return {
@@ -902,31 +855,19 @@ const applied = async (tip) => {
902
855
  return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
903
856
  };
904
857
  /** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
905
- const settled = (carried, slice) => {
906
- const listed = idsOf(REQUIREMENTS, slice);
907
- if (carried === void 0) return listed;
858
+ const settled = (carried) => {
859
+ const listed = idsOf(REQUIREMENTS);
908
860
  return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id));
909
861
  };
910
- const build = (options) => continuing(phase("build", requireCriteriaIfEdited), async (from, fresh) => {
911
- await fresh(() => agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice), from), {
912
- label: from === void 0 ? "Implementing" : "Continuing the implementation",
862
+ const build = async (options) => {
863
+ await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild), {
864
+ label: "Implementing",
913
865
  model: coder(),
914
866
  system: builderSystem(),
915
867
  ...options.afterRefactor === true ? { allowEmpty: true } : {}
916
- }));
868
+ });
917
869
  requireCriteriaFrozen(REQUIREMENTS);
918
- const note = read(HANDOFF);
919
- if (note === void 0) return { result: void 0 };
920
- const ids = settled(options.rebuild?.acceptance?.ids, options.slice);
921
- return {
922
- result: void 0,
923
- handoff: {
924
- snapshot: head(),
925
- note: note.trim(),
926
- open: unnamed(ids, acceptanceTests(changesSince(start())))
927
- }
928
- };
929
- });
870
+ };
930
871
  async function implement(first, tier, amend) {
931
872
  await implementing(typeof first === "object" && first !== null ? first : {
932
873
  reference: first,
@@ -943,30 +884,19 @@ const implementing = (options) => scope("implement", async () => {
943
884
  else requireCriteriaFrozen(REQUIREMENTS);
944
885
  if (built && acceptance !== void 0) requireAcceptance({
945
886
  ...acceptance,
946
- ids: settled(acceptance.ids, options.slice)
887
+ ids: settled(acceptance.ids)
947
888
  }, since);
948
889
  const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
949
890
  await green(keepsTests);
950
- await prototypeTest(options.slice, keepsTests);
891
+ await prototypeTest(keepsTests);
951
892
  const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
952
- await closing(moreToBuild(options.more));
893
+ await run("closing", removeScript([
894
+ REQUIREMENTS,
895
+ REFACTOR_NOTE,
896
+ PROTOTYPE
897
+ ]), { label: "Closing out the implementation" });
953
898
  return ids;
954
899
  });
955
- const moreToBuild = (more) => typeof more === "function" ? more() : more === true;
956
- /** The last slice's closing, for a last slice a steer left with no criterion to build. */
957
- const closeOut = () => scope("implement", () => closing(false));
958
- /** A slice with `more` after it keeps the criteria and the slicing for the next one. */
959
- const closing = (more) => run("closing", removeScript(more ? [
960
- REFACTOR_NOTE,
961
- HANDOFF,
962
- PROTOTYPE
963
- ] : [
964
- REQUIREMENTS,
965
- REFACTOR_NOTE,
966
- SLICES,
967
- HANDOFF,
968
- PROTOTYPE
969
- ]), { label: "Closing out the implementation" });
970
900
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
971
901
  const steer = async (retry) => {
972
902
  await human("steer", {
@@ -997,8 +927,8 @@ const budget = () => {
997
927
  };
998
928
  };
999
929
  /** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
1000
- const vetted = async (tip, vetting, slice) => {
1001
- const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip, slice);
930
+ const vetted = async (tip, vetting) => {
931
+ const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
1002
932
  if (fault !== void 0) {
1003
933
  const verdict = await vetting.again({
1004
934
  fault,
@@ -1032,16 +962,16 @@ const ruled = async (tip, again) => {
1032
962
  tip
1033
963
  } : next;
1034
964
  };
1035
- const decide = (slice) => scope("seam", async () => {
965
+ const decide = () => scope("seam", async () => {
1036
966
  const vetting = {
1037
967
  again: budget(),
1038
968
  waived: false
1039
969
  };
1040
- const naive = phase("naive pass", requireCriteriaIfEdited);
1041
970
  let retry;
1042
971
  for (;;) {
1043
- const tip = await naiveContinued(retry, slice, naive);
1044
- const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
972
+ await naivePass(retry);
973
+ const tip = kept("naive");
974
+ const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
1045
975
  reference: tip.hash,
1046
976
  tip
1047
977
  });
@@ -1062,8 +992,8 @@ const refactorNote = async (reference, strays) => {
1062
992
  if (others.length > 0 || note.trim() === "") refuse(`gtd land: refactor-note: write ${REFACTOR_NOTE} and change nothing else${others.length > 0 ? ` — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}` : ""}`);
1063
993
  return note;
1064
994
  };
1065
- const seamBuild = async (base, slice$1 = {}) => {
1066
- const { reference, tip, shape } = await decide(slice$1.criteria);
995
+ const seamBuild = async (base) => {
996
+ const { reference, tip, shape } = await decide();
1067
997
  if (shape === void 0) return {
1068
998
  reference,
1069
999
  base,
@@ -1078,107 +1008,31 @@ const seamBuild = async (base, slice$1 = {}) => {
1078
1008
  return {
1079
1009
  reference,
1080
1010
  base,
1081
- rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
1011
+ rebuild: { acceptance: naiveAcceptance(tip) }
1082
1012
  };
1083
1013
  }
1084
1014
  const strays = refactorStrays(shape);
1085
1015
  if (strays.length === 0) return {
1086
1016
  reference,
1087
1017
  base,
1088
- rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
1018
+ rebuild: { acceptance: naiveAcceptance(tip) }
1089
1019
  };
1090
1020
  const note = await scope("seam", () => refactorNote(reference, strays));
1091
- const refactored = await slice(slice$1.refactor ?? "refactor", () => reviewedRefactor(shape, {
1021
+ const refactored = await reviewedRefactor(shape, {
1092
1022
  reference,
1093
1023
  note
1094
- }));
1024
+ });
1095
1025
  return {
1096
1026
  reference,
1097
1027
  base: head(),
1098
1028
  refactored,
1099
1029
  rebuild: {
1100
1030
  note,
1101
- acceptance: naiveAcceptance(tip, slice$1.criteria)
1031
+ acceptance: naiveAcceptance(tip)
1102
1032
  }
1103
1033
  };
1104
1034
  };
1105
1035
  //#endregion
1106
- //#region src/workflows/feature/slicing.ts
1107
- const ID = /^(AC-\d+)(?![\w-])/;
1108
- /** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
1109
- const sliceCap = () => {
1110
- const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria;
1111
- if (!/^[1-9]\d*$/.test(value)) refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`);
1112
- return Number(value);
1113
- };
1114
- /** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
1115
- const slicesOf = (text) => {
1116
- const slices = [];
1117
- let open = false;
1118
- for (const { line, fenced } of markFenced(text)) {
1119
- if (fenced) continue;
1120
- const heading = HEADING.exec(line);
1121
- if (heading !== null) {
1122
- open = heading[1] === "##";
1123
- if (open) slices.push({
1124
- title: heading[2],
1125
- ids: []
1126
- });
1127
- continue;
1128
- }
1129
- const item = ITEM.exec(line);
1130
- if (item !== null && open) slices.at(-1).ids.push(ID.exec(item[1])?.[1] ?? "");
1131
- }
1132
- return slices;
1133
- };
1134
- const twice = (list) => [...new Set(list.filter((id, i) => list.indexOf(id) !== i))];
1135
- /** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
1136
- const sliceFault = (slices, ids, cap) => {
1137
- const listed = slices.flatMap((s) => s.ids);
1138
- if (slices.length === 0) return `no "## " slice`;
1139
- const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title);
1140
- if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`;
1141
- if (listed.includes("")) return "a list item not led by a criterion ID";
1142
- const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title);
1143
- if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`;
1144
- const repeated = twice(listed);
1145
- if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`;
1146
- const unknown = listed.filter((id) => !ids.includes(id));
1147
- if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`;
1148
- const missing = ids.filter((id) => !listed.includes(id));
1149
- if (missing.length > 0) return `no slice for: ${missing.join(", ")}`;
1150
- };
1151
- /** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
1152
- const requireSlices = (ids, cap) => {
1153
- const slices = slicesOf(read(".gtd/SLICES.md") ?? "");
1154
- const why = sliceFault(slices, ids, cap);
1155
- if (why !== void 0) refuse(`gtd land: slices: ${SLICES} has ${why} — list each slice as a "## <title>" heading over "- AC-<n>" items, every criterion of ${REQUIREMENTS} in exactly one slice, at most ${cap} per slice`);
1156
- return slices;
1157
- };
1158
- /**
1159
- * The slices to build, in order. Criteria within `sliceMaxCriteria` are one
1160
- * slice and no turn; above it `design.slicing` groups them.
1161
- */
1162
- const slicing = async () => {
1163
- const all = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
1164
- const cap = sliceCap();
1165
- if (all.length <= cap) return [{
1166
- title: "",
1167
- ids: all
1168
- }];
1169
- return scope("design", async () => {
1170
- await agentWithSkills("slicing", slicingPrompt(cap), {
1171
- label: "Slicing the change",
1172
- file: SLICES,
1173
- model: planner(),
1174
- system: designSystem()
1175
- });
1176
- return requireSlices(all, cap);
1177
- });
1178
- };
1179
- /** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
1180
- const sliceCriteria = (slices, i) => slices.length === 1 ? void 0 : i < slices.length - 1 ? { only: slices[i].ids } : { except: slices.slice(0, -1).flatMap((s) => s.ids) };
1181
- //#endregion
1182
1036
  //#region src/workflows/feature/feature.ts
1183
1037
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
1184
1038
  const unwind = () => {
@@ -1186,94 +1040,37 @@ const unwind = () => {
1186
1040
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
1187
1041
  };
1188
1042
  /**
1189
- * The slices `design.slicing` grouped the criteria into, each built in turn
1190
- * from where the previous one landed; design must have run since `firstBase`.
1043
+ * A naive pass (judged only with `flagMode` on), then implement and the
1044
+ * quality lap, then the review; design must have run since `firstBase`.
1191
1045
  */
1192
1046
  const buildFeature = async (firstBase) => {
1193
1047
  const tier = riskTierOn() ? recordRisk() : void 0;
1194
- const slices = await slicing();
1195
1048
  const handedOut = new Set(listedIds());
1196
- const one = slices.length === 1;
1197
- const built = (i) => sliceIds(sliceCriteria(slices, i))?.length !== 0;
1198
- const reports = [];
1199
- for (const i of slices.keys()) {
1200
- if (!built(i)) continue;
1201
- const n = one ? "" : `-${i + 1}`;
1202
- await buildSlice(i === 0 ? firstBase : head(), {
1203
- number: `${i + 1}`,
1204
- tier,
1205
- criteria: sliceCriteria(slices, i),
1206
- refactor: `refactor${n}`,
1207
- feature: `feature${n}`,
1208
- more: () => slices.some((_, j) => j > i && built(j)),
1209
- reviewBase: one ? void 0 : firstBase,
1210
- handedOut,
1211
- laps: reports
1212
- });
1213
- }
1049
+ const seam = await seamBuild(firstBase);
1050
+ const used = gather(handedOut);
1051
+ const closed = await implementing({
1052
+ reference: seam.reference,
1053
+ tier,
1054
+ afterRefactor: seam.refactored,
1055
+ naive: seam.land === true ? seam.reference : void 0,
1056
+ rebuild: seam.rebuild
1057
+ });
1058
+ const carried = seam.rebuild?.acceptance?.tests ?? [];
1059
+ const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
1060
+ const outcome = await reviewedTail(base, base);
1061
+ await featureFeedback(outcome, base, {
1062
+ closed,
1063
+ used
1064
+ });
1214
1065
  };
1215
1066
  const listedIds = () => criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
1216
1067
  const gather = (handedOut) => {
1217
1068
  for (const id of listedIds()) handedOut.add(id);
1218
1069
  return [...handedOut];
1219
1070
  };
1220
- /** A naive pass (judged only with `flagMode` on), then implement and the quality lap; after the last slice, the review. */
1221
- const buildSlice = async (sliceBase, build) => {
1222
- const seam = await seamBuild(sliceBase, {
1223
- criteria: build.criteria,
1224
- refactor: build.refactor
1225
- });
1226
- const used = gather(build.handedOut);
1227
- await slice(build.feature, async () => {
1228
- const closed = await implementing({
1229
- reference: seam.reference,
1230
- tier: build.tier,
1231
- afterRefactor: seam.refactored,
1232
- naive: seam.land === true ? seam.reference : void 0,
1233
- rebuild: seam.rebuild,
1234
- slice: build.criteria,
1235
- more: build.more
1236
- });
1237
- const more = build.more();
1238
- const carried = seam.rebuild?.acceptance?.tests ?? [];
1239
- const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? sliceBase : seam.base;
1240
- if (more) {
1241
- const guard = () => requireCriteriaFrozen(REQUIREMENTS);
1242
- const report = await tail(async (_, report) => report, {
1243
- green: true,
1244
- lensBase: base,
1245
- guard,
1246
- guardRests: false
1247
- });
1248
- build.laps.push({
1249
- slice: build.number,
1250
- report
1251
- });
1252
- return;
1253
- }
1254
- const reviewBase = build.reviewBase ?? base;
1255
- const outcome = await reviewedTail(reviewBase, base, {
1256
- slice: build.number,
1257
- laps: build.laps
1258
- });
1259
- await featureFeedback(outcome, base, {
1260
- closed,
1261
- used
1262
- });
1263
- });
1264
- };
1265
- /**
1266
- * The quality lap of slice `last.slice`, then a reviewer's walkthrough since
1267
- * `base` for the human, carrying what earlier slices' `laps` left too.
1268
- */
1269
- const reviewedTail = (base, lensBase, last = {
1270
- slice: "",
1271
- laps: []
1272
- }, lap = "first") => tail((escalations, report) => humanReview(base, escalations, {
1273
- report: last.laps.length === 0 ? report : joinReports([...last.laps, {
1274
- slice: last.slice,
1275
- report
1276
- }]),
1071
+ /** The quality lap, then a reviewer's walkthrough since `base` for the human. */
1072
+ const reviewedTail = (base, lensBase, lap = "first") => tail((escalations, report) => humanReview(base, escalations, {
1073
+ report,
1277
1074
  walkthrough: true,
1278
1075
  lap
1279
1076
  }), {
@@ -1298,7 +1095,7 @@ const featureFeedback = async (first, lensBase, ids) => {
1298
1095
  tier: riskTierOn() ? recordRisk() : void 0,
1299
1096
  amend: true
1300
1097
  });
1301
- outcome = await reviewedTail(outcome.base, lensBase, void 0, "feedback");
1098
+ outcome = await reviewedTail(outcome.base, lensBase, "feedback");
1302
1099
  }
1303
1100
  };
1304
1101
  /** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
@@ -1306,7 +1103,7 @@ const planAndBuild = async (firstBase, stated) => {
1306
1103
  if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
1307
1104
  await buildFeature(firstBase);
1308
1105
  };
1309
- /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
1106
+ /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. */
1310
1107
  const afterTail = async (first) => {
1311
1108
  let outcome = first;
1312
1109
  while (outcome.verdict === "feedback") {
@@ -1328,7 +1125,6 @@ const ordinaryStart = async () => {
1328
1125
  });
1329
1126
  refusePendingHandOff();
1330
1127
  flagModeOn();
1331
- sliceCap();
1332
1128
  prototypeRounds();
1333
1129
  const stated = statedCriteria(changes(), head());
1334
1130
  await unwind();
@@ -1345,4 +1141,4 @@ const feature = shipped(ordinaryStart);
1345
1141
  //#region src/workflows/feature/index.ts
1346
1142
  var feature_default = feature;
1347
1143
  //#endregion
1348
- export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, handsOnIds, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unstableIds, unwind };
1144
+ export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, handsOnIds, hasImpact, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireImpact, requireImpactOnly, requireNoNewFlags, requireNoTestFiles, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unstableIds, unwind };