@pmelab/gtd 51.0.0 → 52.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 (66) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +41 -38
  3. package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-D0MKVFFa.mjs} +28 -34
  4. package/dist/chunks/{review-BaCXlIT2.mjs → review-ZzUQH1KZ.mjs} +11 -9
  5. package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
  6. package/dist/chunks/{tail-CX6Pdsql.mjs → tail-I1-vVq0P.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +224 -68
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +27 -26
  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 +181 -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 +478 -313
  19. package/package.json +1 -1
  20. package/src/edge/Edge.ts +1 -30
  21. package/src/replay/Replay.test.ts +4 -8
  22. package/src/replay/Replay.ts +35 -3
  23. package/src/replay/index.ts +7 -1
  24. package/src/steering/FORMATS.md +4 -1
  25. package/src/steering/SteeringFormat.ts +7 -1
  26. package/src/steering/contract.test.ts +2 -1
  27. package/src/steering/fixtures/review/sectioned.md +31 -0
  28. package/src/steering/fixtures/review/sectioned.read.json +119 -0
  29. package/src/steering/index.ts +4 -0
  30. package/src/steering/qa.ts +1 -1
  31. package/src/steering/review.test.ts +270 -4
  32. package/src/steering/review.ts +328 -69
  33. package/src/workflows/feature/criteria.test.ts +39 -1
  34. package/src/workflows/feature/criteria.ts +21 -7
  35. package/src/workflows/feature/fast-path.feature +1 -6
  36. package/src/workflows/feature/feature.ts +1 -2
  37. package/src/workflows/feature/flag-mode.feature +1 -6
  38. package/src/workflows/feature/green.ts +22 -0
  39. package/src/workflows/feature/hands-on-skip.feature +344 -0
  40. package/src/workflows/feature/lens-models.feature +0 -4
  41. package/src/workflows/feature/lens-review.feature +6 -8
  42. package/src/workflows/feature/one-step-land.feature +1 -6
  43. package/src/workflows/feature/prototype-rounds.feature +1 -1
  44. package/src/workflows/feature/prototype.feature +10 -9
  45. package/src/workflows/feature/prototype.ts +48 -17
  46. package/src/workflows/feature/refactor-absorbs.feature +3 -8
  47. package/src/workflows/feature/refactor-first.feature +3 -8
  48. package/src/workflows/feature/refactor-rebuild.feature +7 -12
  49. package/src/workflows/feature/refactor-review.feature +52 -72
  50. package/src/workflows/feature/review-answers.feature +90 -5
  51. package/src/workflows/feature/review-gate.feature +11 -53
  52. package/src/workflows/feature/reviewer-walkthrough.feature +29 -12
  53. package/src/workflows/feature/seam-rejection.feature +1 -6
  54. package/src/workflows/feature/seam.feature +1 -6
  55. package/src/workflows/feature/seam.test.ts +1 -1
  56. package/src/workflows/feature/seam.ts +2 -3
  57. package/src/workflows/feature/severity-budget.feature +7 -7
  58. package/src/workflows/feature/severity-sorted-review.feature +50 -54
  59. package/src/workflows/feature/slice-findings.feature +5 -15
  60. package/src/workflows/feature/slicing.feature +4 -24
  61. package/src/workflows/feature/standards.feature +0 -4
  62. package/src/workflows/shared/review.ts +26 -6
  63. package/src/workflows/shared/text.test.ts +45 -11
  64. package/src/workflows/shared/text.ts +45 -38
  65. package/src/workflows/feature/coverage.feature +0 -165
  66. 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 envDefaults, Pt as defaults, Xt as requireFlag, Yt as registryEntry, i as skills, n as summary, o as flagShipped, qt as flagReferences, r as access, t as steering } from "../chunks/reserved-D0MKVFFa.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-I1-vVq0P.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 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";
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;
@@ -257,6 +265,164 @@ const design = (base, guard) => scope("design", async () => {
257
265
  } while (await noteGate(REQUIREMENTS, answerProductQuestions));
258
266
  });
259
267
  //#endregion
268
+ //#region src/workflows/feature/green.ts
269
+ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
270
+ label: "Fixing the check",
271
+ file: FEEDBACK,
272
+ model: coder(),
273
+ system: builderSystem()
274
+ }), {
275
+ guard: () => {
276
+ requireCriteriaFrozen(REQUIREMENTS);
277
+ guard?.();
278
+ },
279
+ guardRests: false
280
+ });
281
+ //#endregion
282
+ //#region src/workflows/feature/prototype.ts
283
+ /** Only a `no` at this or above skips a criterion: no verdict, or an unsure one, sends it to the hands-on test. */
284
+ const UNOBSERVABLE_MIN_P = .5;
285
+ /** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
286
+ const prototypeRounds = () => {
287
+ const value = vars.prototypeRounds ?? defaults.prototypeRounds;
288
+ if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
289
+ return Number(value);
290
+ };
291
+ const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
292
+ const UNGUIDED = /^no testing instructions$/i;
293
+ /** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
294
+ const reportOf = (text) => {
295
+ let unguided = false;
296
+ const items = [];
297
+ const malformed = [];
298
+ for (const { line, fenced } of markFenced(text)) {
299
+ const heading = fenced ? null : HEADING.exec(line);
300
+ if (heading?.[1] !== "##") continue;
301
+ if (UNGUIDED.test(heading[2])) unguided = true;
302
+ const chunk = CHUNK.exec(heading[2]);
303
+ if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
304
+ if (chunk !== null) items.push({
305
+ subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
306
+ verdict: chunk[2].toLowerCase()
307
+ });
308
+ }
309
+ return {
310
+ unguided,
311
+ items,
312
+ malformed
313
+ };
314
+ };
315
+ /**
316
+ * The report the testing turn left, refused unless it is the only change and
317
+ * names each of `ids` once, with its open items: any other criterion is not
318
+ * this test's to fix.
319
+ */
320
+ const requireReport = (ids, retest) => {
321
+ const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
322
+ 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")}`);
323
+ const text = read(PROTOTYPE);
324
+ if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
325
+ const report = reportOf(text);
326
+ 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")}`);
327
+ if (report.unguided && report.items.length === 0 && !retest) return {
328
+ unguided: true,
329
+ open: []
330
+ };
331
+ const elsewhere = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "").filter((id) => !ids.includes(id));
332
+ const count = (id) => report.items.filter((i) => i.subject === id).length;
333
+ const wrong = ids.filter((id) => count(id) !== 1);
334
+ if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
335
+ return {
336
+ unguided: false,
337
+ open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
338
+ };
339
+ };
340
+ /**
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.
343
+ */
344
+ const observable = async (slice) => {
345
+ const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
346
+ const own = handsOnIds(requirements, slice);
347
+ const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
348
+ if (criteria.length === 0) return [];
349
+ const { answers } = await judge("observable", {
350
+ questions: criteria.map(observableQuestion),
351
+ evidence: { requirements },
352
+ message: observableMessage(),
353
+ label: "Judging which criteria to test by hand"
354
+ });
355
+ return own.filter((id) => !answered(answers[id], "no", UNOBSERVABLE_MIN_P));
356
+ };
357
+ 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",
362
+ file: PROTOTYPE,
363
+ model: coder(),
364
+ system: testerSystem(),
365
+ allowEmpty: true
366
+ });
367
+ return requireReport(ids, retest);
368
+ };
369
+ const fixing = async (slice, guard) => {
370
+ await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
371
+ label: "Fixing what the hands-on test found",
372
+ file: PROTOTYPE,
373
+ model: coder(),
374
+ system: builderSystem(),
375
+ allowEmpty: true
376
+ }));
377
+ 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`);
378
+ frozen();
379
+ guard?.();
380
+ };
381
+ /** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
382
+ const askPerson = async (name, message, label) => {
383
+ await human(name, {
384
+ message,
385
+ label,
386
+ file: PROTOTYPE,
387
+ acceptClean: true
388
+ });
389
+ if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
390
+ frozen();
391
+ return true;
392
+ };
393
+ /**
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 —
396
+ * 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.
399
+ */
400
+ const prototypeTest = async (slice, guard) => {
401
+ const cap = prototypeRounds();
402
+ if (cap === 0) return;
403
+ await scope("prototype", async () => {
404
+ const ids = await observable(slice);
405
+ if (ids.length === 0) return;
406
+ let fixes = 0;
407
+ let retest = false;
408
+ for (;;) {
409
+ const { unguided, open } = await testing(slice, ids, retest);
410
+ retest = !unguided;
411
+ if (unguided) {
412
+ if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
413
+ } else if (open.length === 0) return;
414
+ else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
415
+ fixes++;
416
+ await fixing(slice, guard);
417
+ } else {
418
+ if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
419
+ fixes = 0;
420
+ }
421
+ await green(guard);
422
+ }
423
+ });
424
+ };
425
+ //#endregion
260
426
  //#region src/workflows/shared/continuation.ts
261
427
  const phase = (name, steered) => ({
262
428
  name,
@@ -573,211 +739,6 @@ const reviewedRefactor = async (shape, options = {}) => {
573
739
  });
574
740
  };
575
741
  //#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
742
  //#region src/workflows/feature/seam.ts
782
743
  /** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
783
744
  const NAIVE_RETRIES = 2;
@@ -986,7 +947,6 @@ const implementing = (options) => scope("implement", async () => {
986
947
  }, since);
987
948
  const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
988
949
  await green(keepsTests);
989
- await requireCoverage(options.base, options.slice);
990
950
  await prototypeTest(options.slice, keepsTests);
991
951
  const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
992
952
  await closing(moreToBuild(options.more));
@@ -1272,7 +1232,6 @@ const buildSlice = async (sliceBase, build) => {
1272
1232
  naive: seam.land === true ? seam.reference : void 0,
1273
1233
  rebuild: seam.rebuild,
1274
1234
  slice: build.criteria,
1275
- base: build.criteria === void 0 ? void 0 : sliceBase,
1276
1235
  more: build.more
1277
1236
  });
1278
1237
  const more = build.more();
@@ -1386,4 +1345,4 @@ const feature = shipped(ordinaryStart);
1386
1345
  //#region src/workflows/feature/index.ts
1387
1346
  var feature_default = feature;
1388
1347
  //#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 };
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 };
@@ -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 envDefaults, G as finisherSystem, K as fixReviewNotesPrompt, N as agentWithSkills, Pt as defaults, c as baseline, i as skills, n as summary, r as access, s as shipped, t as steering, y as REVIEW } from "../chunks/reserved-D0MKVFFa.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-I1-vVq0P.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 envDefaults, G as finisherSystem, N as agentWithSkills, Pt as defaults, _t as reviewFeedbackFixPrompt, i as skills, l as gate, n as summary, r as access, s as shipped, t as steering, v as REQUIREMENTS, vt as reviewGateBlockedMessage } from "../chunks/reserved-D0MKVFFa.mjs";
3
+ import { a as refusePendingHandOff } from "../chunks/tail-I1-vVq0P.mjs";
4
+ import { i as reUnwind, t as buildTail } from "../chunks/review-ZzUQH1KZ.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 envDefaults, Jt as flipProblems, Kt as flagMentions, Pt as defaults, Xt as requireFlag, f as FEEDBACK, i as skills, l as gate, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-D0MKVFFa.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.";