@pmelab/gtd 51.0.1 → 52.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 (74) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +46 -40
  3. package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-DxB5CROj.mjs} +73 -38
  4. package/dist/chunks/{review-BaCXlIT2.mjs → review-CKMSdvgW.mjs} +11 -9
  5. package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
  6. package/dist/chunks/{tail-CX6Pdsql.mjs → tail-QZc7Uz2U.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +198 -43
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +1 -1
  10. package/dist/web/main.css +1 -1
  11. package/dist/web/main.js +510 -147
  12. package/dist/workflow-kit.mjs +1 -1
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +228 -222
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +3 -3
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +570 -317
  19. package/package.json +5 -4
  20. package/src/steering/FORMATS.md +4 -1
  21. package/src/steering/SteeringFormat.ts +7 -1
  22. package/src/steering/contract.test.ts +2 -1
  23. package/src/steering/fixtures/review/sectioned.md +31 -0
  24. package/src/steering/fixtures/review/sectioned.read.json +119 -0
  25. package/src/steering/index.ts +4 -0
  26. package/src/steering/qa.ts +1 -1
  27. package/src/steering/review.test.ts +270 -4
  28. package/src/steering/review.ts +328 -69
  29. package/src/testkit/steps.ts +13 -0
  30. package/src/workflows/feature/acceptance-criteria.feature +159 -3
  31. package/src/workflows/feature/continuation-cap.feature +4 -0
  32. package/src/workflows/feature/continuation-rebuild.feature +4 -0
  33. package/src/workflows/feature/continuation-slices.feature +4 -0
  34. package/src/workflows/feature/continuation.feature +4 -0
  35. package/src/workflows/feature/criteria.test.ts +60 -1
  36. package/src/workflows/feature/criteria.ts +87 -8
  37. package/src/workflows/feature/fast-path.feature +5 -6
  38. package/src/workflows/feature/feature.test.ts +6 -5
  39. package/src/workflows/feature/feature.ts +1 -2
  40. package/src/workflows/feature/flag-mode.feature +5 -6
  41. package/src/workflows/feature/green.ts +22 -0
  42. package/src/workflows/feature/hands-on-skip.feature +384 -0
  43. package/src/workflows/feature/lens-models.feature +4 -4
  44. package/src/workflows/feature/lens-review.feature +10 -8
  45. package/src/workflows/feature/naive-acceptance.feature +4 -0
  46. package/src/workflows/feature/one-step-land.feature +5 -6
  47. package/src/workflows/feature/planning.ts +2 -1
  48. package/src/workflows/feature/prototype-rounds.feature +5 -1
  49. package/src/workflows/feature/prototype.feature +18 -9
  50. package/src/workflows/feature/prototype.ts +48 -17
  51. package/src/workflows/feature/refactor-absorbs.feature +7 -8
  52. package/src/workflows/feature/refactor-first.feature +7 -8
  53. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  54. package/src/workflows/feature/refactor-rebuild.feature +11 -12
  55. package/src/workflows/feature/refactor-review.feature +72 -72
  56. package/src/workflows/feature/review-answers.feature +94 -5
  57. package/src/workflows/feature/review-gate.feature +23 -53
  58. package/src/workflows/feature/reviewer-walkthrough.feature +33 -12
  59. package/src/workflows/feature/risk-tier.feature +32 -0
  60. package/src/workflows/feature/seam-rejection.feature +9 -6
  61. package/src/workflows/feature/seam.feature +9 -6
  62. package/src/workflows/feature/seam.test.ts +5 -3
  63. package/src/workflows/feature/seam.ts +2 -3
  64. package/src/workflows/feature/severity-budget.feature +11 -7
  65. package/src/workflows/feature/severity-sorted-review.feature +54 -54
  66. package/src/workflows/feature/slice-findings.feature +9 -15
  67. package/src/workflows/feature/slicing.feature +36 -24
  68. package/src/workflows/feature/standards.feature +4 -4
  69. package/src/workflows/feature/tier-pin.feature +4 -0
  70. package/src/workflows/shared/review.ts +26 -6
  71. package/src/workflows/shared/text.test.ts +85 -11
  72. package/src/workflows/shared/text.ts +95 -42
  73. package/src/workflows/feature/coverage.feature +0 -165
  74. package/src/workflows/feature/coverage.ts +0 -120
@@ -1,5 +1,5 @@
1
1
  import { changes, changesSince, hasThreadFor, head, mark, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
2
- import { E as parseThreads, _ as reviewRisks, c as stripCodeThreads, g as reviewNotes, h as reviewDecisions, y as unansweredNotes } from "./chunks/steering-DtUVUz9l.mjs";
2
+ import { S as unansweredNotes, _ as reviewNotes, c as stripCodeThreads, g as reviewDecisions, k as parseThreads, v as reviewRisks } from "./chunks/steering-CLhu8C7q.mjs";
3
3
  //#region src/workflow-kit/health.ts
4
4
  /** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
5
5
  const escalation = async (count, steps) => {
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { C as coder, It as defaults, Lt as envDefaults, Qt as requireFlag, Yt as flagReferences, Zt as registryEntry, i as skills, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-CUkk9XgL.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CX6Pdsql.mjs";
2
+ import { C as coder, Ft as defaults, It as envDefaults, Jt as flagReferences, Xt as registryEntry, Zt as requireFlag, i as skills, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-DxB5CROj.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-QZc7Uz2U.mjs";
4
4
  //#region src/workflows/cleanup/cleanup.ts
5
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.`;
6
6
  const cleanupFlow = async () => {
@@ -1,8 +1,8 @@
1
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";
2
2
  import { lensEntriesOf } from "../workflow-kit.mjs";
3
- import { $ as implementFixPrompt, C as coder, Ct as seamByHandMessage, Dt as testerSystem, E as fixNits, Et as startGateBlockedMessage, G as designGateAnswerMessage, H as coverageQuestion, I as builderSystem, It as defaults, K as designSystem, Lt as envDefaults, Mt as unwindFailure, N as agentWithSkills, P as buildReviewCollectingPrompt, Q as implementBuildingPrompt, R as clearCriteriaMessage, Rt as testFileGlobs, St as reviewerSystem, Tt as slicingPrompt, U as coveringPrompt, V as coverageMessage, W as criterionLine, Z as idleMessage, _ as REFACTOR_NOTE, a as flagModeOn, at as prototypeOpenMessage, b as SLICES, d as healthy, dt as refactorBuildingPrompt, et as naiveApplyFailure, f as FEEDBACK, ft as refactorDroppedRefusal, g as REFACTOR, ht as refactorRules, i as skills, it as prototypeFixingPrompt, jt as unwindFailedMessage, k as planner, kt as uncoveredMessage, l as gate, m as PROTOTYPE, mt as refactorNotePrompt, n as summary, nt as normaliseCriteriaScript, ot as prototypeTestingPrompt, p as HANDOFF, pt as refactorFixPrompt, q as designTriagePrompt, r as access, rt as oversizedMessage, s as shipped, st as prototypeUnguidedMessage, t as steering, tt as naivePassPrompt, v as REQUIREMENTS, w as collecting, wt as seamSteerMessage, x as answerReviewQuestions, z as clearCriteriaQuestion } from "../chunks/reserved-CUkk9XgL.mjs";
4
- import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-CX6Pdsql.mjs";
5
- import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-BaCXlIT2.mjs";
3
+ import { $ as naivePassPrompt, At as unwindFailedMessage, C as coder, Ct as seamByHandMessage, Dt as testerSystem, E as fixNits, Et as startGateBlockedMessage, Ft as defaults, G as designTriagePrompt, H as designGateAnswerMessage, I as builderSystem, It as envDefaults, Lt as testFileGlobs, N as agentWithSkills, P as buildReviewCollectingPrompt, Q as naiveApplyFailure, R as clearCriteriaMessage, St as reviewerSystem, Tt as slicingPrompt, U as designImpactPrompt, V as criterionLine, W as designSystem, X as implementBuildingPrompt, Y as idleMessage, Z as implementFixPrompt, _ as REFACTOR_NOTE, a as flagModeOn, at as prototypeOpenMessage, b as SLICES, d as healthy, dt as refactorBuildingPrompt, et as normaliseCriteriaScript, f as FEEDBACK, ft as refactorDroppedRefusal, g as REFACTOR, ht as refactorRules, i as skills, it as prototypeFixingPrompt, jt as unwindFailure, k as planner, l as gate, m as PROTOTYPE, mt as refactorNotePrompt, n as summary, nt as observableQuestion, ot as prototypeTestingPrompt, p as HANDOFF, pt as refactorFixPrompt, r as access, rt as oversizedMessage, s as shipped, st as prototypeUnguidedMessage, t as steering, tt as observableMessage, v as REQUIREMENTS, w as collecting, wt as seamSteerMessage, x as answerReviewQuestions, z as clearCriteriaQuestion } from "../chunks/reserved-DxB5CROj.mjs";
4
+ import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-QZc7Uz2U.mjs";
5
+ import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-CKMSdvgW.mjs";
6
6
  import { posix } from "node:path";
7
7
  //#region src/workflows/shared/fences.ts
8
8
  const markFenced = (text) => {
@@ -26,7 +26,12 @@ 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*(.*)$/;
29
+ const ID$1 = /^(AC-\d+)(\s*\(no hands-on\))?:\s*(.*)$/;
30
+ const criterion = (id, as = id[1]) => ({
31
+ id: as,
32
+ text: id[3],
33
+ ...id[2] === void 0 ? {} : { noHandsOn: true }
34
+ });
30
35
  /** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
31
36
  const listUnder = (text, title, level) => {
32
37
  const lines = markFenced(text);
@@ -58,25 +63,28 @@ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((
58
63
  return id === null ? {
59
64
  id: "",
60
65
  text: item
61
- } : {
62
- id: id[1],
63
- text: id[2]
64
- };
66
+ } : criterion(id);
65
67
  });
66
68
  /** The ID'd criteria of `text`, only `slice`'s when given. */
67
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) => {
72
+ const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id);
73
+ return criterionIds(text, slice).filter((id) => !tagged.includes(id));
74
+ };
68
75
  /** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
69
76
  const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
70
77
  /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
71
78
  const numbered = (items) => {
72
79
  const ids = items.map((item) => ID$1.exec(item));
73
80
  const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
74
- return items.map((item, i) => own ? {
75
- id: ids[i][1],
76
- text: ids[i][2]
77
- } : {
78
- id: `AC-${i + 1}`,
79
- text: item.replace(ID$1, "$2")
81
+ return items.map((item, i) => {
82
+ const id = ids[i];
83
+ if (own) return criterion(id);
84
+ return id === null ? {
85
+ id: `AC-${i + 1}`,
86
+ text: item
87
+ } : criterion(id, `AC-${i + 1}`);
80
88
  });
81
89
  };
82
90
  const STATED = /^acceptance criteria:?$/i;
@@ -113,6 +121,38 @@ const requireCriteria = (file) => {
113
121
  if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
114
122
  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`);
115
123
  };
124
+ const isImpactHeading = (h) => h !== null && h[1].length === 2 && /^impact$/i.test(h[2].trim());
125
+ /** Whether `text` has an unfenced, level-2 `## Impact` heading. */
126
+ const hasImpact = (text) => markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)));
127
+ /** `text` without its `## Impact` section, whitespace collapsed. */
128
+ const withoutImpact = (text) => {
129
+ let inImpact = false;
130
+ return markFenced(text).filter((l) => {
131
+ const h = l.fenced ? null : HEADING.exec(l.line);
132
+ if (h !== null && h[1].length <= 2) inImpact = isImpactHeading(h);
133
+ return !inImpact;
134
+ }).map((l) => l.line).join(" ").replace(/\s+/g, " ").trim();
135
+ };
136
+ /**
137
+ * Refuse a turn that leaves `file` without a `## Impact` section. Only the
138
+ * pending turn (`head()` is `""`): a triage lap recorded before Impact existed
139
+ * replays without one, and refusing there would fail every in-flight process.
140
+ * A human's edit at a later gate is not checked — the human owns that file then.
141
+ */
142
+ const requireImpact = (file) => {
143
+ if (head() !== "") return;
144
+ const text = read(file);
145
+ if (text === void 0) return;
146
+ if (hasImpact(text)) return;
147
+ 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`);
148
+ };
149
+ /** Refuse a turn that changed `file` beyond its `## Impact` section. */
150
+ const requireImpactOnly = (file) => {
151
+ const change = changes().get(file);
152
+ if (change === void 0) return;
153
+ if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return;
154
+ refuse(`gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`);
155
+ };
116
156
  const idNumber = (id) => Number(id.slice(3));
117
157
  /** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
118
158
  const unstableIds = (listed, ids) => {
@@ -146,6 +186,19 @@ const clear = async (stated) => {
146
186
  });
147
187
  return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
148
188
  };
189
+ /** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
190
+ const impact = async () => {
191
+ await agentWithSkills("impact", designImpactPrompt(), {
192
+ label: "Mapping the change's impact",
193
+ file: REQUIREMENTS,
194
+ model: planner(),
195
+ system: designSystem()
196
+ });
197
+ requireProgress(REQUIREMENTS);
198
+ requireCriteriaFrozen(REQUIREMENTS);
199
+ requireImpact(REQUIREMENTS);
200
+ requireImpactOnly(REQUIREMENTS);
201
+ };
149
202
  /**
150
203
  * The skip path: when the sketch stated clear, checkable criteria, write them
151
204
  * to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
@@ -157,6 +210,7 @@ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scop
157
210
  label: "Recording the stated acceptance criteria",
158
211
  file: REQUIREMENTS
159
212
  });
213
+ await impact();
160
214
  return true;
161
215
  });
162
216
  /** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
@@ -253,10 +307,169 @@ const design = (base, guard) => scope("design", async () => {
253
307
  requireProgress(REQUIREMENTS);
254
308
  requireReplies(REQUIREMENTS);
255
309
  requireCriteria(REQUIREMENTS);
310
+ requireImpact(REQUIREMENTS);
256
311
  guard?.();
257
312
  } while (await noteGate(REQUIREMENTS, answerProductQuestions));
258
313
  });
259
314
  //#endregion
315
+ //#region src/workflows/feature/green.ts
316
+ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
317
+ label: "Fixing the check",
318
+ file: FEEDBACK,
319
+ model: coder(),
320
+ system: builderSystem()
321
+ }), {
322
+ guard: () => {
323
+ requireCriteriaFrozen(REQUIREMENTS);
324
+ guard?.();
325
+ },
326
+ guardRests: false
327
+ });
328
+ //#endregion
329
+ //#region src/workflows/feature/prototype.ts
330
+ /** Only a `no` at this or above skips a criterion: no verdict, or an unsure one, sends it to the hands-on test. */
331
+ const UNOBSERVABLE_MIN_P = .5;
332
+ /** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
333
+ const prototypeRounds = () => {
334
+ const value = vars.prototypeRounds ?? defaults.prototypeRounds;
335
+ if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
336
+ return Number(value);
337
+ };
338
+ const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
339
+ const UNGUIDED = /^no testing instructions$/i;
340
+ /** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
341
+ const reportOf = (text) => {
342
+ let unguided = false;
343
+ const items = [];
344
+ const malformed = [];
345
+ for (const { line, fenced } of markFenced(text)) {
346
+ const heading = fenced ? null : HEADING.exec(line);
347
+ if (heading?.[1] !== "##") continue;
348
+ if (UNGUIDED.test(heading[2])) unguided = true;
349
+ const chunk = CHUNK.exec(heading[2]);
350
+ if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
351
+ if (chunk !== null) items.push({
352
+ subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
353
+ verdict: chunk[2].toLowerCase()
354
+ });
355
+ }
356
+ return {
357
+ unguided,
358
+ items,
359
+ malformed
360
+ };
361
+ };
362
+ /**
363
+ * The report the testing turn left, refused unless it is the only change and
364
+ * names each of `ids` once, with its open items: any other criterion is not
365
+ * this test's to fix.
366
+ */
367
+ const requireReport = (ids, retest) => {
368
+ const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
369
+ if (others.length > 0) refuse(`gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`);
370
+ const text = read(PROTOTYPE);
371
+ if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
372
+ const report = reportOf(text);
373
+ if (report.malformed.length > 0) refuse(`gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`);
374
+ if (report.unguided && report.items.length === 0 && !retest) return {
375
+ unguided: true,
376
+ open: []
377
+ };
378
+ const elsewhere = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "").filter((id) => !ids.includes(id));
379
+ const count = (id) => report.items.filter((i) => i.subject === id).length;
380
+ const wrong = ids.filter((id) => count(id) !== 1);
381
+ if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
382
+ return {
383
+ unguided: false,
384
+ open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
385
+ };
386
+ };
387
+ /**
388
+ * The slice's criteria a person can observe by using the change, by the
389
+ * judge: those tagged `(no hands-on)` are never asked about.
390
+ */
391
+ const observable = async (slice) => {
392
+ const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
393
+ const own = handsOnIds(requirements, slice);
394
+ const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
395
+ if (criteria.length === 0) return [];
396
+ const { answers } = await judge("observable", {
397
+ questions: criteria.map(observableQuestion),
398
+ evidence: { requirements },
399
+ message: observableMessage(),
400
+ label: "Judging which criteria to test by hand"
401
+ });
402
+ return own.filter((id) => !answered(answers[id], "no", UNOBSERVABLE_MIN_P));
403
+ };
404
+ const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
405
+ const testing = async (slice, ids, retest) => {
406
+ const skipped = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice).filter((id) => !ids.includes(id));
407
+ await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), skipped, retest), {
408
+ label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
409
+ file: PROTOTYPE,
410
+ model: coder(),
411
+ system: testerSystem(),
412
+ allowEmpty: true
413
+ });
414
+ return requireReport(ids, retest);
415
+ };
416
+ const fixing = async (slice, guard) => {
417
+ await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
418
+ label: "Fixing what the hands-on test found",
419
+ file: PROTOTYPE,
420
+ model: coder(),
421
+ system: builderSystem(),
422
+ allowEmpty: true
423
+ }));
424
+ if (changes().some((c) => c.path === ".gtd/PROTOTYPE.md")) refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`);
425
+ frozen();
426
+ guard?.();
427
+ };
428
+ /** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
429
+ const askPerson = async (name, message, label) => {
430
+ await human(name, {
431
+ message,
432
+ label,
433
+ file: PROTOTYPE,
434
+ acceptClean: true
435
+ });
436
+ if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
437
+ frozen();
438
+ return true;
439
+ };
440
+ /**
441
+ * Use the slice by hand: a testing turn reports on every criterion a person
442
+ * can observe, a fix turn takes each failure, and the slice is tested again —
443
+ * until the report is clean, or `prototypeRounds` fixes are spent and a person
444
+ * decides. A slice with no such criterion is not tested by hand. `guard` holds
445
+ * every fix turn, as it holds the implementation's.
446
+ */
447
+ const prototypeTest = async (slice, guard) => {
448
+ const cap = prototypeRounds();
449
+ if (cap === 0) return;
450
+ await scope("prototype", async () => {
451
+ const ids = await observable(slice);
452
+ if (ids.length === 0) return;
453
+ let fixes = 0;
454
+ let retest = false;
455
+ for (;;) {
456
+ const { unguided, open } = await testing(slice, ids, retest);
457
+ retest = !unguided;
458
+ if (unguided) {
459
+ if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
460
+ } else if (open.length === 0) return;
461
+ else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
462
+ fixes++;
463
+ await fixing(slice, guard);
464
+ } else {
465
+ if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
466
+ fixes = 0;
467
+ }
468
+ await green(guard);
469
+ }
470
+ });
471
+ };
472
+ //#endregion
260
473
  //#region src/workflows/shared/continuation.ts
261
474
  const phase = (name, steered) => ({
262
475
  name,
@@ -573,211 +786,6 @@ const reviewedRefactor = async (shape, options = {}) => {
573
786
  });
574
787
  };
575
788
  //#endregion
576
- //#region src/workflows/feature/coverage.ts
577
- /** Below this the judge's `yes` is not trusted: a wrongly covered ID ends the feature without its test. */
578
- const COVERED_MIN_P = .7;
579
- /** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
580
- const COVERING_RETRIES = 2;
581
- const frozen$1 = () => requireCriteriaFrozen(REQUIREMENTS);
582
- const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
583
- label: "Fixing the check",
584
- file: FEEDBACK,
585
- model: coder(),
586
- system: builderSystem()
587
- }), {
588
- guard: () => {
589
- frozen$1();
590
- guard?.();
591
- },
592
- guardRests: false
593
- });
594
- const testsSince = (base) => changesSince(base).filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path)).sort((a, b) => Number(/\bAC-\d/.test(a.after)) - Number(/\bAC-\d/.test(b.after))).map((c) => `=== ${c.path} (${c.status})\n${c.after}`).join("\n");
595
- /**
596
- * The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
597
- * over, only `slice`'s when given — no passing test since `base` covers, by the judge.
598
- */
599
- const uncoveredSince = async (base, slice) => {
600
- const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
601
- const own = criterionIds(requirements, slice);
602
- const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
603
- if (criteria.length === 0) return {
604
- covered: [],
605
- uncovered: []
606
- };
607
- const { answers } = await judge("coverage", {
608
- questions: criteria.map(coverageQuestion),
609
- evidence: { tests: testsSince(base) },
610
- message: coverageMessage(),
611
- label: "Judging acceptance-criteria coverage"
612
- });
613
- const covers = (id) => answered(answers[id], "yes", COVERED_MIN_P);
614
- const ids = criteria.map((c) => c.id);
615
- return {
616
- covered: ids.filter(covers),
617
- uncovered: ids.filter((id) => !covers(id))
618
- };
619
- };
620
- const record = (covered, uncovered = []) => {
621
- for (const id of covered) mark("covers", id);
622
- for (const id of uncovered) mark("uncovered", id);
623
- };
624
- /**
625
- * Hold a green change until every acceptance criterion has a passing test,
626
- * sending uncovered IDs back to a covering turn; marks the IDs covered on the
627
- * landing that ends it. A human may overrule once the retries are spent.
628
- */
629
- const requireCoverage = async (base = start(), slice) => {
630
- let retries = 0;
631
- for (;;) {
632
- const { covered, uncovered } = await uncoveredSince(base, slice);
633
- if (uncovered.length === 0) return record(covered);
634
- if (retries < COVERING_RETRIES) {
635
- retries++;
636
- await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
637
- label: "Covering the acceptance criteria",
638
- model: coder(),
639
- system: builderSystem(),
640
- allowEmpty: true
641
- });
642
- frozen$1();
643
- } else {
644
- await human("uncovered", {
645
- message: uncoveredMessage(uncovered),
646
- label: "Acceptance criteria without a test",
647
- file: REQUIREMENTS,
648
- acceptClean: true
649
- });
650
- if (!changes().some((c) => isCode$1(c.path))) return record(covered, uncovered);
651
- frozen$1();
652
- retries = 0;
653
- }
654
- await green();
655
- }
656
- };
657
- //#endregion
658
- //#region src/workflows/feature/prototype.ts
659
- /** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
660
- const prototypeRounds = () => {
661
- const value = vars.prototypeRounds ?? defaults.prototypeRounds;
662
- if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
663
- return Number(value);
664
- };
665
- const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
666
- const UNGUIDED = /^no testing instructions$/i;
667
- /** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
668
- const reportOf = (text) => {
669
- let unguided = false;
670
- const items = [];
671
- const malformed = [];
672
- for (const { line, fenced } of markFenced(text)) {
673
- const heading = fenced ? null : HEADING.exec(line);
674
- if (heading?.[1] !== "##") continue;
675
- if (UNGUIDED.test(heading[2])) unguided = true;
676
- const chunk = CHUNK.exec(heading[2]);
677
- if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
678
- if (chunk !== null) items.push({
679
- subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
680
- verdict: chunk[2].toLowerCase()
681
- });
682
- }
683
- return {
684
- unguided,
685
- items,
686
- malformed
687
- };
688
- };
689
- /**
690
- * The report the testing turn left, refused unless it is the only change and
691
- * names every criterion once, with its open items: another slice's criteria
692
- * are not this slice's to fix.
693
- */
694
- const requireReport = (slice, retest) => {
695
- const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
696
- if (others.length > 0) refuse(`gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`);
697
- const text = read(PROTOTYPE);
698
- if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
699
- const report = reportOf(text);
700
- if (report.malformed.length > 0) refuse(`gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`);
701
- if (report.unguided && report.items.length === 0 && !retest) return {
702
- unguided: true,
703
- open: []
704
- };
705
- const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
706
- const ids = criterionIds(requirements, slice);
707
- const elsewhere = criterionIds(requirements).filter((id) => !ids.includes(id));
708
- const count = (id) => report.items.filter((i) => i.subject === id).length;
709
- const wrong = ids.filter((id) => count(id) !== 1);
710
- if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
711
- return {
712
- unguided: false,
713
- open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
714
- };
715
- };
716
- const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
717
- const testing = async (slice, retest) => {
718
- await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), retest), {
719
- label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
720
- file: PROTOTYPE,
721
- model: coder(),
722
- system: testerSystem(),
723
- allowEmpty: true
724
- });
725
- return requireReport(slice, retest);
726
- };
727
- const fixing = async (slice, guard) => {
728
- await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
729
- label: "Fixing what the hands-on test found",
730
- file: PROTOTYPE,
731
- model: coder(),
732
- system: builderSystem(),
733
- allowEmpty: true
734
- }));
735
- if (changes().some((c) => c.path === ".gtd/PROTOTYPE.md")) refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`);
736
- frozen();
737
- guard?.();
738
- };
739
- /** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
740
- const askPerson = async (name, message, label) => {
741
- await human(name, {
742
- message,
743
- label,
744
- file: PROTOTYPE,
745
- acceptClean: true
746
- });
747
- if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
748
- frozen();
749
- return true;
750
- };
751
- /**
752
- * Use the slice by hand: a testing turn reports on every criterion, a fix turn
753
- * takes each failure, and the slice is tested again — until the report is
754
- * clean, or `prototypeRounds` fixes are spent and a person decides. `guard`
755
- * holds every fix turn, as it holds the implementation's.
756
- */
757
- const prototypeTest = async (slice, guard) => {
758
- const cap = prototypeRounds();
759
- if (cap === 0) return;
760
- await scope("prototype", async () => {
761
- let fixes = 0;
762
- let retest = false;
763
- for (;;) {
764
- const { unguided, open } = await testing(slice, retest);
765
- retest = !unguided;
766
- if (unguided) {
767
- if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
768
- } else if (open.length === 0) return;
769
- else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
770
- fixes++;
771
- await fixing(slice, guard);
772
- } else {
773
- if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
774
- fixes = 0;
775
- }
776
- await green(guard);
777
- }
778
- });
779
- };
780
- //#endregion
781
789
  //#region src/workflows/feature/seam.ts
782
790
  /** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
783
791
  const NAIVE_RETRIES = 2;
@@ -986,7 +994,6 @@ const implementing = (options) => scope("implement", async () => {
986
994
  }, since);
987
995
  const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
988
996
  await green(keepsTests);
989
- await requireCoverage(options.base, options.slice);
990
997
  await prototypeTest(options.slice, keepsTests);
991
998
  const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
992
999
  await closing(moreToBuild(options.more));
@@ -1272,7 +1279,6 @@ const buildSlice = async (sliceBase, build) => {
1272
1279
  naive: seam.land === true ? seam.reference : void 0,
1273
1280
  rebuild: seam.rebuild,
1274
1281
  slice: build.criteria,
1275
- base: build.criteria === void 0 ? void 0 : sliceBase,
1276
1282
  more: build.more
1277
1283
  });
1278
1284
  const more = build.more();
@@ -1386,4 +1392,4 @@ const feature = shipped(ordinaryStart);
1386
1392
  //#region src/workflows/feature/index.ts
1387
1393
  var feature_default = feature;
1388
1394
  //#endregion
1389
- export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, 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 };
1395
+ 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, hasImpact, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireImpact, requireImpactOnly, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unstableIds, unwind };
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
- import { _ as reviewRisks, g as reviewNotes } from "../chunks/steering-DtUVUz9l.mjs";
3
- import { C as coder, It as defaults, J as finisherSystem, Lt as envDefaults, N as agentWithSkills, Y as fixReviewNotesPrompt, c as baseline, i as skills, n as summary, r as access, s as shipped, t as steering, y as REVIEW } from "../chunks/reserved-CUkk9XgL.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CX6Pdsql.mjs";
2
+ import { _ as reviewNotes, v as reviewRisks } from "../chunks/steering-CLhu8C7q.mjs";
3
+ import { C as coder, Ft as defaults, It as envDefaults, K as finisherSystem, N as agentWithSkills, c as baseline, i as skills, n as summary, q as fixReviewNotesPrompt, r as access, s as shipped, t as steering, y as REVIEW } from "../chunks/reserved-DxB5CROj.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-QZc7Uz2U.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,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, start } from "../flows.mjs";
2
- import { C as coder, It as defaults, J as finisherSystem, Lt as envDefaults, N as agentWithSkills, i as skills, l as gate, n as summary, r as access, s as shipped, t as steering, v as REQUIREMENTS, vt as reviewFeedbackFixPrompt, yt as reviewGateBlockedMessage } from "../chunks/reserved-CUkk9XgL.mjs";
3
- import { a as refusePendingHandOff } from "../chunks/tail-CX6Pdsql.mjs";
4
- import { i as reUnwind, t as buildTail } from "../chunks/review-BaCXlIT2.mjs";
2
+ import { C as coder, Ft as defaults, It as envDefaults, K as finisherSystem, N as agentWithSkills, i as skills, l as gate, n as summary, r as access, s as shipped, t as steering, v as REQUIREMENTS, vt as reviewFeedbackFixPrompt, yt as reviewGateBlockedMessage } from "../chunks/reserved-DxB5CROj.mjs";
3
+ import { a as refusePendingHandOff } from "../chunks/tail-QZc7Uz2U.mjs";
4
+ import { i as reUnwind, t as buildTail } from "../chunks/review-CKMSdvgW.mjs";
5
5
  //#region src/workflows/review/review.ts
6
6
  const fixFeedback = async () => {
7
7
  await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { C as coder, It as defaults, Jt as flagMentions, Lt as envDefaults, Qt as requireFlag, Xt as flipProblems, f as FEEDBACK, i as skills, l as gate, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-CUkk9XgL.mjs";
2
+ import { C as coder, Ft as defaults, It as envDefaults, Yt as flipProblems, Zt as requireFlag, f as FEEDBACK, i as skills, l as gate, n as summary, o as flagShipped, qt as flagMentions, r as access, t as steering } from "../chunks/reserved-DxB5CROj.mjs";
3
3
  //#region src/workflows/rollout/rollout.ts
4
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.`;
5
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.";