@pmelab/gtd 37.0.0 → 38.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 (41) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -0
  3. package/dist/chunks/{reserved-zAfoJtR7.mjs → reserved-CfQ8eurM.mjs} +85 -17
  4. package/dist/chunks/{tail-BRC4DGdV.mjs → tail-C7yaWvaS.mjs} +1 -1
  5. package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
  6. package/dist/gtd.bundle.mjs +1 -1
  7. package/dist/testkit.mjs +1 -1
  8. package/dist/wire.mjs +1 -1
  9. package/dist/workflows/cleanup.mjs +2 -2
  10. package/dist/workflows/feature.mjs +318 -76
  11. package/dist/workflows/fix.mjs +2 -2
  12. package/dist/workflows/review.mjs +2 -2
  13. package/dist/workflows/rollout.mjs +1 -1
  14. package/dist/workflows.mjs +419 -109
  15. package/package.json +1 -1
  16. package/src/testkit/steps.ts +39 -0
  17. package/src/wire/ContextLimit.ts +1 -1
  18. package/src/workflows/feature/continuation-cap.feature +65 -0
  19. package/src/workflows/feature/continuation-rebuild.feature +88 -0
  20. package/src/workflows/feature/continuation-slices.feature +64 -0
  21. package/src/workflows/feature/continuation.feature +205 -0
  22. package/src/workflows/feature/coverage.ts +14 -6
  23. package/src/workflows/feature/criteria.test.ts +17 -1
  24. package/src/workflows/feature/criteria.ts +37 -3
  25. package/src/workflows/feature/feature.ts +64 -15
  26. package/src/workflows/feature/index.ts +1 -0
  27. package/src/workflows/feature/refactor.ts +64 -30
  28. package/src/workflows/feature/seam.ts +157 -48
  29. package/src/workflows/feature/slicing-cap.feature +14 -0
  30. package/src/workflows/feature/slicing.feature +225 -0
  31. package/src/workflows/feature/slicing.test.ts +49 -0
  32. package/src/workflows/feature/slicing.ts +105 -0
  33. package/src/workflows/index.ts +1 -0
  34. package/src/workflows/shared/continuation.ts +92 -0
  35. package/src/workflows/shared/fences.ts +5 -0
  36. package/src/workflows/shared/options.ts +3 -0
  37. package/src/workflows/shared/steps.ts +2 -0
  38. package/src/workflows/shared/text.test.ts +6 -2
  39. package/src/workflows/shared/text.ts +153 -22
  40. package/src/workflows/shared/vars.ts +6 -0
  41. package/src/workflows/shared/workflows.steps.ts +13 -0
@@ -1,7 +1,7 @@
1
1
  import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
2
2
  import { lensEntriesOf, review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as normaliseCriteriaScript, A as buildReviewAwaitAfterDeferMessage, B as criterionLine, C as fixRisks, D as reviewing, F as clearCriteriaMessage, H as designGateAnswerMessage, I as clearCriteriaQuestion, J as idleMessage, L as coverageMessage, M as buildReviewFixNitsPrompt, N as buildReviewTriageMessage, O as agentWithSkills, P as builderSystem, Q as naivePassPrompt, R as coverageQuestion, T as reviewMissing, U as designSystem, V as deferNotesScript, W as designTriagePrompt, X as implementFixPrompt, Y as implementBuildingPrompt, Z as naiveApplyFailure, _ as REQUIREMENTS, _t as defaults, a as flagModeOn, at as refactorRules, b as coder, d as guarded, dt as seamByHandMessage, et as qualityUnresolvedMessage, f as healthy, ft as seamSteerMessage, g as REFACTOR_NOTE, gt as unwindFailure, h as REFACTOR, ht as unwindFailedMessage, i as skills, it as refactorNotePrompt, j as buildReviewCollectingPrompt, k as buildReviewAnswerQuestionsPrompt, lt as reviewNotesCapture, m as QUALITY, mt as uncoveredMessage, n as summary, nt as refactorDroppedRefusal, ot as reviewEditNotesCapture, p as FEEDBACK, pt as startGateBlockedMessage, r as access, rt as refactorFixPrompt, s as shipped, st as reviewEditsCapture, t as steering, tt as refactorBuildingPrompt, u as gate, ut as reviewerSystem, v as REVIEW, vt as envDefaults, w as planner, x as fixCheck, y as awaitReview, yt as testFileGlobs, z as coveringPrompt } from "../chunks/reserved-zAfoJtR7.mjs";
4
- import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-BRC4DGdV.mjs";
3
+ import { $ as naiveApplyFailure, A as agentWithSkills, B as coverageQuestion, C as fixCheck, Ct as testFileGlobs, D as reviewMissing, E as planner, F as buildReviewTriageMessage, G as designSystem, H as criterionLine, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, M as buildReviewAwaitAfterDeferMessage, N as buildReviewCollectingPrompt, P as buildReviewFixNitsPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, S as coder, St as envDefaults, T as fixRisks, U as deferNotesScript, V as coveringPrompt, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as startGateBlockedMessage, a as flagModeOn, at as refactorDroppedRefusal, b as SLICES, bt as unwindFailure, ct as refactorRules, d as guarded, et as naivePassPrompt, f as healthy, ft as reviewNotesCapture, g as REFACTOR, gt as slicingPrompt, h as QUALITY, ht as seamSteerMessage, i as skills, it as refactorBuildingPrompt, j as buildReviewAnswerQuestionsPrompt, k as reviewing, lt as reviewEditNotesCapture, m as HANDOFF, mt as seamByHandMessage, n as summary, nt as oversizedMessage, ot as refactorFixPrompt, p as FEEDBACK, pt as reviewerSystem, r as access, rt as qualityUnresolvedMessage, s as shipped, st as refactorNotePrompt, t as steering, tt as normaliseCriteriaScript, u as gate, ut as reviewEditsCapture, v as REQUIREMENTS, vt as uncoveredMessage, x as awaitReview, xt as defaults, y as REVIEW, yt as unwindFailedMessage, z as coverageMessage } from "../chunks/reserved-CfQ8eurM.mjs";
4
+ import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-C7yaWvaS.mjs";
5
5
  import { posix } from "node:path";
6
6
  //#region src/workflows/shared/fences.ts
7
7
  const markFenced = (text) => {
@@ -17,13 +17,15 @@ const markFenced = (text) => {
17
17
  };
18
18
  });
19
19
  };
20
+ /** A markdown heading: its `#` run, then its text. */
21
+ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
22
+ /** A list item, its checkbox dropped: its text. */
23
+ const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
20
24
  //#endregion
21
25
  //#region src/workflows/feature/criteria.ts
22
26
  /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
23
27
  const CLEAR_MIN_P = .8;
24
- const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
25
- const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
26
- const ID = /^(AC-\d+):\s*(.*)$/;
28
+ const ID$1 = /^(AC-\d+):\s*(.*)$/;
27
29
  /** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
28
30
  const listUnder = (text, title, level) => {
29
31
  const lines = markFenced(text);
@@ -51,7 +53,7 @@ const listUnder = (text, title, level) => {
51
53
  };
52
54
  /** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
53
55
  const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
54
- const id = ID.exec(item);
56
+ const id = ID$1.exec(item);
55
57
  return id === null ? {
56
58
  id: "",
57
59
  text: item
@@ -60,16 +62,20 @@ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((
60
62
  text: id[2]
61
63
  };
62
64
  });
65
+ /** The ID'd criteria of `text`, only `slice`'s when given. */
66
+ 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));
67
+ /** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
68
+ const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
63
69
  /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
64
70
  const numbered = (items) => {
65
- const ids = items.map((item) => ID.exec(item));
71
+ const ids = items.map((item) => ID$1.exec(item));
66
72
  const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
67
73
  return items.map((item, i) => own ? {
68
74
  id: ids[i][1],
69
75
  text: ids[i][2]
70
76
  } : {
71
77
  id: `AC-${i + 1}`,
72
- text: item.replace(ID, "$2")
78
+ text: item.replace(ID$1, "$2")
73
79
  });
74
80
  };
75
81
  const STATED = /^acceptance criteria:?$/i;
@@ -139,6 +145,13 @@ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scop
139
145
  });
140
146
  return true;
141
147
  });
148
+ /** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
149
+ const requireCriteriaIfEdited = () => {
150
+ const change = changes().get(REQUIREMENTS);
151
+ if (change === void 0) return;
152
+ if (change.status === "deleted") refuse(`gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`);
153
+ requireCriteria(REQUIREMENTS);
154
+ };
142
155
  //#endregion
143
156
  //#region src/workflows/feature/tier.ts
144
157
  /** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
@@ -487,6 +500,53 @@ const reUnwind = async (feedback) => {
487
500
  requireRevert(edited, base);
488
501
  };
489
502
  //#endregion
503
+ //#region src/workflows/shared/continuation.ts
504
+ const phase = (name, steered) => ({
505
+ name,
506
+ spent: false,
507
+ count: 0,
508
+ stalled: 0,
509
+ steered
510
+ });
511
+ const own = (fn) => fn();
512
+ const continued = (fn) => scope("continue", fn);
513
+ const afresh = (fn) => scope("fresh", fn);
514
+ const setting = (name, least) => {
515
+ const raw = vars[name] ?? defaults[name];
516
+ const n = Number(raw);
517
+ if (raw.trim() === "" || !Number.isInteger(n) || n < least) refuse(`gtd: ${name} must be a whole number of at least ${least}, got ${JSON.stringify(raw)}`);
518
+ return n;
519
+ };
520
+ /**
521
+ * Run `turn` until one ends without a handoff note: each stopped turn is picked
522
+ * up by a continuation in a fresh conversation. Past `continuations` of them in
523
+ * the phase, or after `continuationStall` in a row that close nothing still
524
+ * open, the phase rests at `oversized` for a person; landing there grants a new round.
525
+ */
526
+ const continuing = async (phase, turn) => {
527
+ const cap = setting("continuations", 0);
528
+ const stall = setting("continuationStall", 1);
529
+ let last = await turn(void 0, phase.spent ? afresh : own);
530
+ while (last.handoff !== void 0) {
531
+ phase.spent = true;
532
+ const from = last.handoff;
533
+ if (phase.count >= cap || phase.stalled >= stall) {
534
+ await human("oversized", {
535
+ message: oversizedMessage(phase.name, from, phase.count >= cap ? { cap } : { stall }, phase.steered !== void 0),
536
+ label: "Too big for its context",
537
+ acceptClean: true
538
+ });
539
+ phase.steered?.();
540
+ phase.count = 0;
541
+ phase.stalled = 0;
542
+ }
543
+ last = await turn(from, continued);
544
+ phase.count++;
545
+ phase.stalled = from.open.length === 0 || from.open.some((item) => !(last.handoff?.open ?? []).includes(item)) ? 0 : phase.stalled + 1;
546
+ }
547
+ return last.result;
548
+ };
549
+ //#endregion
490
550
  //#region src/workflows/feature/refactor.ts
491
551
  const REFACTOR_PACKAGE = REFACTOR;
492
552
  const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
@@ -632,7 +692,7 @@ const guardOf = ({ pattern, strays }, entry) => () => {
632
692
  requireNoNewFlags(pattern, entry);
633
693
  };
634
694
  /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
635
- const refactorLap = async (p, guard, feedback) => {
695
+ const refactorLap = async (p, guard, refactor, feedback) => {
636
696
  const { pattern, strays, reference, note } = p;
637
697
  await run("deriving", deriveScript([
638
698
  ...packageLines(strays, pattern, reference),
@@ -652,32 +712,48 @@ const refactorLap = async (p, guard, feedback) => {
652
712
  label: "Deriving the refactor package",
653
713
  file: REFACTOR_PACKAGE
654
714
  });
655
- await agentWithSkills("building", refactorBuildingPrompt({
656
- pkg: REFACTOR_PACKAGE,
657
- strays,
658
- pattern,
659
- reference,
660
- feedback: feedback !== void 0,
661
- note: note !== void 0
662
- }), {
663
- label: "Refactoring",
664
- model: coder(),
665
- system: builderSystem()
666
- });
667
- guard();
668
- await healthy(async () => {
669
- await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
670
- label: "Fixing the check",
671
- file: FEEDBACK,
715
+ const reached = /* @__PURE__ */ new Set();
716
+ await continuing(refactor, async (from, fresh) => {
717
+ await fresh(() => agentWithSkills("building", refactorBuildingPrompt({
718
+ pkg: REFACTOR_PACKAGE,
719
+ strays,
720
+ pattern,
721
+ reference,
722
+ feedback: feedback !== void 0,
723
+ note: note !== void 0,
724
+ from
725
+ }), {
726
+ label: from === void 0 ? "Refactoring" : "Continuing the refactor",
672
727
  model: coder(),
673
728
  system: builderSystem()
674
- });
729
+ }));
730
+ const left = read(HANDOFF);
675
731
  guard();
676
- }, {
677
- describeGuard: guard,
678
- rules: refactorRules(strays, pattern)
732
+ const snapshot = head();
733
+ for (const c of changes()) reached.add(c.path);
734
+ await healthy(async () => {
735
+ await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
736
+ label: "Fixing the check",
737
+ file: FEEDBACK,
738
+ model: coder(),
739
+ system: builderSystem()
740
+ });
741
+ guard();
742
+ }, {
743
+ describeGuard: guard,
744
+ rules: refactorRules(strays, pattern)
745
+ });
746
+ if (left === void 0) return { result: void 0 };
747
+ return {
748
+ result: void 0,
749
+ handoff: {
750
+ snapshot,
751
+ note: left.trim(),
752
+ open: strays.filter((path) => !reached.has(path))
753
+ }
754
+ };
679
755
  });
680
- await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
756
+ await run("closing", removeScript([REFACTOR_PACKAGE, HANDOFF]), { label: "Closing out the refactor" });
681
757
  };
682
758
  const strayAccess = (strays) => ({ write: [
683
759
  ...strays,
@@ -691,7 +767,7 @@ const refactorFirst = async (shape, options = {}) => {
691
767
  name: "refactor",
692
768
  access: strayAccess(p.strays)
693
769
  }, async () => {
694
- await refactorLap(p, guardOf(p, head()));
770
+ await refactorLap(p, guardOf(p, head()), phase("refactor"));
695
771
  return true;
696
772
  });
697
773
  };
@@ -716,9 +792,10 @@ const reviewedRefactor = async (shape, options = {}) => {
716
792
  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",
717
793
  dropped: refactorDroppedRefusal
718
794
  };
795
+ const refactor = phase("refactor");
719
796
  let collected;
720
797
  for (;;) {
721
- await refactorLap(p, guard, collected);
798
+ await refactorLap(p, guard, refactor, collected);
722
799
  const outcome = await buildTail(entry, {
723
800
  green: true,
724
801
  guard,
@@ -759,10 +836,12 @@ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", imple
759
836
  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");
760
837
  /**
761
838
  * The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
762
- * over — no passing test since `base` covers, by the judge.
839
+ * over, only `slice`'s when given — no passing test since `base` covers, by the judge.
763
840
  */
764
- const uncoveredSince = async (base) => {
765
- const criteria = (criteriaOf(read(".gtd/REQUIREMENTS.md") ?? "") ?? []).filter((c) => c.id !== "");
841
+ const uncoveredSince = async (base, slice) => {
842
+ const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
843
+ const own = criterionIds(requirements, slice);
844
+ const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
766
845
  if (criteria.length === 0) return {
767
846
  covered: [],
768
847
  uncovered: []
@@ -789,14 +868,14 @@ const record = (covered, uncovered = []) => {
789
868
  * sending uncovered IDs back to a covering turn; marks the IDs covered on the
790
869
  * landing that ends it. A human may overrule once the retries are spent.
791
870
  */
792
- const requireCoverage = async (base = start()) => {
871
+ const requireCoverage = async (base = start(), slice) => {
793
872
  let retries = 0;
794
873
  for (;;) {
795
- const { covered, uncovered } = await uncoveredSince(base);
874
+ const { covered, uncovered } = await uncoveredSince(base, slice);
796
875
  if (uncovered.length === 0) return record(covered);
797
876
  if (retries < COVERING_RETRIES) {
798
877
  retries++;
799
- await agentWithSkills("covering", coveringPrompt(uncovered), {
878
+ await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
800
879
  label: "Covering the acceptance criteria",
801
880
  model: coder(),
802
881
  system: builderSystem(),
@@ -827,12 +906,32 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
827
906
  allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
828
907
  exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
829
908
  });
830
- const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
831
- label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
909
+ const naivePass = (retry, slice, from) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice), from), {
910
+ label: from !== void 0 ? "Continuing the naive pass" : retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
832
911
  keep: true,
833
912
  model: coder(),
834
913
  system: builderSystem()
835
914
  });
915
+ const handoffOf = (tip, slice) => {
916
+ const note = tip.changes.find((c) => c.path === ".gtd/HANDOFF.md" && c.after !== void 0);
917
+ if (note === void 0) return void 0;
918
+ return {
919
+ snapshot: tip.hash,
920
+ note: note.after.trim(),
921
+ open: unnamed(idsOf(REQUIREMENTS, slice), acceptanceTests(tip.changes))
922
+ };
923
+ };
924
+ /** A naive pass, continued until a pass ends without a handoff note: the snapshot is its kept tip. */
925
+ const naiveContinued = (retry, slice, naive) => continuing(naive, async (from, fresh) => {
926
+ const tip = await fresh(async () => {
927
+ await naivePass(retry, slice, from);
928
+ return kept("naive");
929
+ });
930
+ return {
931
+ result: tip,
932
+ handoff: handoffOf(tip, slice)
933
+ };
934
+ });
836
935
  const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
837
936
  const EXTENSION = /\.[\w-]+$/;
838
937
  const ALIAS = /^(?:[@~]\/|#)/;
@@ -868,7 +967,7 @@ const addedLines = (change) => {
868
967
  return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
869
968
  };
870
969
  const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
871
- const idsOf = (file) => (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
970
+ const idsOf = (file, slice) => criterionIds(read(file) ?? "", slice);
872
971
  const unnamed = (ids, tests) => {
873
972
  const lines = tests.map(addedLines);
874
973
  return ids.filter((id) => !lines.some((text) => named(id, text)));
@@ -878,9 +977,9 @@ const unnamed = (ids, tests) => {
878
977
  * test line of the pass names, or a test importing a file the pass added —
879
978
  * naive structure a reset and refactor replaces, which the tests must survive.
880
979
  */
881
- const acceptanceFault = (file, tip) => {
980
+ const acceptanceFault = (file, tip, slice) => {
882
981
  const tests = acceptanceTests(tip.changes);
883
- const untested = unnamed(idsOf(file), tests);
982
+ const untested = unnamed(idsOf(file, slice), tests);
884
983
  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]}] …"`;
885
984
  const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
886
985
  const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
@@ -890,8 +989,8 @@ const acceptanceFault = (file, tip) => {
890
989
  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")}`;
891
990
  };
892
991
  /** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
893
- const naiveAcceptance = (tip) => {
894
- const ids = idsOf(REQUIREMENTS);
992
+ const naiveAcceptance = (tip, slice) => {
993
+ const ids = idsOf(REQUIREMENTS, slice);
895
994
  const perTest = acceptanceTests(tip.changes).map((c) => {
896
995
  const lines = addedLines(c);
897
996
  return {
@@ -960,6 +1059,32 @@ const applied = async (tip) => {
960
1059
  await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
961
1060
  return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
962
1061
  };
1062
+ /** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
1063
+ const settled = (carried, slice) => {
1064
+ const listed = idsOf(REQUIREMENTS, slice);
1065
+ if (carried === void 0) return listed;
1066
+ return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id));
1067
+ };
1068
+ const build = (options) => continuing(phase("build", requireCriteriaIfEdited), async (from, fresh) => {
1069
+ await fresh(() => agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice), from), {
1070
+ label: from === void 0 ? "Implementing" : "Continuing the implementation",
1071
+ model: coder(),
1072
+ system: builderSystem(),
1073
+ ...options.afterRefactor === true ? { allowEmpty: true } : {}
1074
+ }));
1075
+ requireCriteriaFrozen(REQUIREMENTS);
1076
+ const note = read(HANDOFF);
1077
+ if (note === void 0) return { result: void 0 };
1078
+ const ids = settled(options.rebuild?.acceptance?.ids, options.slice);
1079
+ return {
1080
+ result: void 0,
1081
+ handoff: {
1082
+ snapshot: head(),
1083
+ note: note.trim(),
1084
+ open: unnamed(ids, acceptanceTests(changesSince(start())))
1085
+ }
1086
+ };
1087
+ });
963
1088
  function implement(first, tier, amend) {
964
1089
  const options = typeof first === "object" && first !== null ? first : {
965
1090
  reference: first,
@@ -970,19 +1095,26 @@ function implement(first, tier, amend) {
970
1095
  const since = head();
971
1096
  const acceptance = options.rebuild?.acceptance;
972
1097
  const built = options.naive === void 0 || !await applied(options.naive);
973
- if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild), {
974
- label: "Implementing",
975
- model: coder(),
976
- system: builderSystem(),
977
- ...options.afterRefactor === true ? { allowEmpty: true } : {}
978
- });
979
- requireCriteriaFrozen(REQUIREMENTS);
980
- if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
1098
+ if (built) await build(options);
1099
+ else requireCriteriaFrozen(REQUIREMENTS);
1100
+ if (built && acceptance !== void 0) requireAcceptance({
1101
+ ...acceptance,
1102
+ ids: settled(acceptance.ids, options.slice)
1103
+ }, since);
981
1104
  await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
982
- await requireCoverage();
983
- await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), { label: "Closing out the implementation" });
1105
+ await requireCoverage(options.base, options.slice);
1106
+ await closing(options.more === true);
984
1107
  });
985
1108
  }
1109
+ /** The last slice's closing, for a last slice a steer left with no criterion to build. */
1110
+ const closeOut = () => scope("implement", () => closing(false));
1111
+ /** A slice with `more` after it keeps the criteria and the slicing for the next one. */
1112
+ const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE, HANDOFF] : [
1113
+ REQUIREMENTS,
1114
+ REFACTOR_NOTE,
1115
+ SLICES,
1116
+ HANDOFF
1117
+ ]), { label: "Closing out the implementation" });
986
1118
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
987
1119
  const steer = async (retry) => {
988
1120
  await human("steer", {
@@ -1013,8 +1145,8 @@ const budget = () => {
1013
1145
  };
1014
1146
  };
1015
1147
  /** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
1016
- const vetted = async (tip, vetting) => {
1017
- const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
1148
+ const vetted = async (tip, vetting, slice) => {
1149
+ const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip, slice);
1018
1150
  if (fault !== void 0) {
1019
1151
  const verdict = await vetting.again({
1020
1152
  fault,
@@ -1048,16 +1180,16 @@ const ruled = async (tip, again) => {
1048
1180
  tip
1049
1181
  } : next;
1050
1182
  };
1051
- const decide = () => scope("seam", async () => {
1183
+ const decide = (slice) => scope("seam", async () => {
1052
1184
  const vetting = {
1053
1185
  again: budget(),
1054
1186
  waived: false
1055
1187
  };
1188
+ const naive = phase("naive pass", requireCriteriaIfEdited);
1056
1189
  let retry;
1057
1190
  for (;;) {
1058
- await naivePass(retry);
1059
- const tip = kept("naive");
1060
- const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
1191
+ const tip = await naiveContinued(retry, slice, naive);
1192
+ const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
1061
1193
  reference: tip.hash,
1062
1194
  tip
1063
1195
  });
@@ -1078,8 +1210,8 @@ const refactorNote = async (reference, strays) => {
1078
1210
  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")}` : ""}`);
1079
1211
  return note;
1080
1212
  };
1081
- const seamBuild = async (base) => {
1082
- const { reference, tip, shape } = await decide();
1213
+ const seamBuild = async (base, slice$1 = {}) => {
1214
+ const { reference, tip, shape } = await decide(slice$1.criteria);
1083
1215
  if (shape === void 0) return {
1084
1216
  reference,
1085
1217
  base,
@@ -1094,17 +1226,17 @@ const seamBuild = async (base) => {
1094
1226
  return {
1095
1227
  reference,
1096
1228
  base,
1097
- rebuild: { acceptance: naiveAcceptance(tip) }
1229
+ rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
1098
1230
  };
1099
1231
  }
1100
1232
  const strays = refactorStrays(shape);
1101
1233
  if (strays.length === 0) return {
1102
1234
  reference,
1103
1235
  base,
1104
- rebuild: { acceptance: naiveAcceptance(tip) }
1236
+ rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
1105
1237
  };
1106
1238
  const note = await scope("seam", () => refactorNote(reference, strays));
1107
- const refactored = await slice("refactor", () => reviewedRefactor(shape, {
1239
+ const refactored = await slice(slice$1.refactor ?? "refactor", () => reviewedRefactor(shape, {
1108
1240
  reference,
1109
1241
  note
1110
1242
  }));
@@ -1114,34 +1246,143 @@ const seamBuild = async (base) => {
1114
1246
  refactored,
1115
1247
  rebuild: {
1116
1248
  note,
1117
- acceptance: naiveAcceptance(tip)
1249
+ acceptance: naiveAcceptance(tip, slice$1.criteria)
1118
1250
  }
1119
1251
  };
1120
1252
  };
1121
1253
  //#endregion
1254
+ //#region src/workflows/feature/slicing.ts
1255
+ const ID = /^(AC-\d+)(?![\w-])/;
1256
+ /** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
1257
+ const sliceCap = () => {
1258
+ const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria;
1259
+ if (!/^[1-9]\d*$/.test(value)) refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`);
1260
+ return Number(value);
1261
+ };
1262
+ /** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
1263
+ const slicesOf = (text) => {
1264
+ const slices = [];
1265
+ let open = false;
1266
+ for (const { line, fenced } of markFenced(text)) {
1267
+ if (fenced) continue;
1268
+ const heading = HEADING.exec(line);
1269
+ if (heading !== null) {
1270
+ open = heading[1] === "##";
1271
+ if (open) slices.push({
1272
+ title: heading[2],
1273
+ ids: []
1274
+ });
1275
+ continue;
1276
+ }
1277
+ const item = ITEM.exec(line);
1278
+ if (item !== null && open) slices.at(-1).ids.push(ID.exec(item[1])?.[1] ?? "");
1279
+ }
1280
+ return slices;
1281
+ };
1282
+ const twice = (list) => [...new Set(list.filter((id, i) => list.indexOf(id) !== i))];
1283
+ /** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
1284
+ const sliceFault = (slices, ids, cap) => {
1285
+ const listed = slices.flatMap((s) => s.ids);
1286
+ if (slices.length === 0) return `no "## " slice`;
1287
+ const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title);
1288
+ if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`;
1289
+ if (listed.includes("")) return "a list item not led by a criterion ID";
1290
+ const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title);
1291
+ if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`;
1292
+ const repeated = twice(listed);
1293
+ if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`;
1294
+ const unknown = listed.filter((id) => !ids.includes(id));
1295
+ if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`;
1296
+ const missing = ids.filter((id) => !listed.includes(id));
1297
+ if (missing.length > 0) return `no slice for: ${missing.join(", ")}`;
1298
+ };
1299
+ /** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
1300
+ const requireSlices = (ids, cap) => {
1301
+ const slices = slicesOf(read(".gtd/SLICES.md") ?? "");
1302
+ const why = sliceFault(slices, ids, cap);
1303
+ 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`);
1304
+ return slices;
1305
+ };
1306
+ /**
1307
+ * The slices to build, in order. Criteria within `sliceMaxCriteria` are one
1308
+ * slice and no turn; above it `design.slicing` groups them.
1309
+ */
1310
+ const slicing = async () => {
1311
+ const all = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
1312
+ const cap = sliceCap();
1313
+ if (all.length <= cap) return [{
1314
+ title: "",
1315
+ ids: all
1316
+ }];
1317
+ return scope("design", async () => {
1318
+ await agentWithSkills("slicing", slicingPrompt(cap), {
1319
+ label: "Slicing the change",
1320
+ file: SLICES,
1321
+ model: planner(),
1322
+ system: designSystem()
1323
+ });
1324
+ return requireSlices(all, cap);
1325
+ });
1326
+ };
1327
+ /** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
1328
+ 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) };
1329
+ //#endregion
1122
1330
  //#region src/workflows/feature/feature.ts
1123
1331
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
1124
1332
  const unwind = () => {
1125
1333
  const commit = head();
1126
1334
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
1127
1335
  };
1128
- /** A naive pass (judged only with `flagMode` on), then implement and the quality lap; design must have run since `firstBase`. */
1336
+ /**
1337
+ * The slices `design.slicing` grouped the criteria into, each built in turn
1338
+ * from where the previous one landed; design must have run since `firstBase`.
1339
+ */
1129
1340
  const buildFeature = async (firstBase) => {
1130
1341
  const tier = riskTierOn() ? recordRisk() : void 0;
1131
- const seam = await seamBuild(firstBase);
1132
- await slice("feature", async () => {
1342
+ const slices = await slicing();
1343
+ const one = slices.length === 1;
1344
+ for (const i of slices.keys()) {
1345
+ const more = i < slices.length - 1;
1346
+ const criteria = sliceCriteria(slices, i);
1347
+ if (sliceIds(criteria)?.length === 0) {
1348
+ if (!more) await closeOut();
1349
+ continue;
1350
+ }
1351
+ const n = one ? "" : `-${i + 1}`;
1352
+ await buildSlice(i === 0 ? firstBase : head(), {
1353
+ tier,
1354
+ criteria,
1355
+ refactor: `refactor${n}`,
1356
+ feature: `feature${n}`,
1357
+ more
1358
+ });
1359
+ }
1360
+ };
1361
+ /** A naive pass (judged only with `flagMode` on), then implement and the quality lap. */
1362
+ const buildSlice = async (sliceBase, build) => {
1363
+ const seam = await seamBuild(sliceBase, {
1364
+ criteria: build.criteria,
1365
+ refactor: build.refactor
1366
+ });
1367
+ await slice(build.feature, async () => {
1133
1368
  await implement({
1134
1369
  reference: seam.reference,
1135
- tier,
1370
+ tier: build.tier,
1136
1371
  afterRefactor: seam.refactored,
1137
1372
  naive: seam.land === true ? seam.reference : void 0,
1138
- rebuild: seam.rebuild
1373
+ rebuild: seam.rebuild,
1374
+ slice: build.criteria,
1375
+ base: build.criteria === void 0 ? void 0 : sliceBase,
1376
+ more: build.more
1139
1377
  });
1140
1378
  const carried = seam.rebuild?.acceptance?.tests ?? [];
1141
- const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
1379
+ const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? sliceBase : seam.base;
1380
+ const guard = build.more ? () => requireCriteriaFrozen(REQUIREMENTS) : void 0;
1142
1381
  await tail(settleQuality, {
1143
1382
  green: true,
1144
- lensBase: base
1383
+ lensBase: base,
1384
+ guard,
1385
+ guardRests: false
1145
1386
  });
1146
1387
  });
1147
1388
  };
@@ -1187,6 +1428,7 @@ const ordinaryStart = async () => {
1187
1428
  });
1188
1429
  refusePendingHandOff();
1189
1430
  flagModeOn();
1431
+ sliceCap();
1190
1432
  const stated = statedCriteria(changes(), head());
1191
1433
  await unwind();
1192
1434
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
@@ -1202,4 +1444,4 @@ const feature = shipped(ordinaryStart);
1202
1444
  //#region src/workflows/feature/index.ts
1203
1445
  var feature_default = feature;
1204
1446
  //#endregion
1205
- export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, holdsCallSites, implement, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unwind };
1447
+ export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, holdsCallSites, implement, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unwind };
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
3
- import { G as finisherSystem, K as fixReviewNotesPrompt, O as agentWithSkills, _t as defaults, b as coder, c as baseline, i as skills, n as summary, r as access, s as shipped, t as steering, v as REVIEW, vt as envDefaults } from "../chunks/reserved-zAfoJtR7.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-BRC4DGdV.mjs";
3
+ import { A as agentWithSkills, J as fixReviewNotesPrompt, S as coder, St as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, xt as defaults, y as REVIEW } from "../chunks/reserved-CfQ8eurM.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-C7yaWvaS.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,6 +1,6 @@
1
1
  import { start } from "../flows.mjs";
2
- import { _t as defaults, ct as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate, vt as envDefaults } from "../chunks/reserved-zAfoJtR7.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-BRC4DGdV.mjs";
2
+ import { St as envDefaults, dt as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate, xt as defaults } from "../chunks/reserved-CfQ8eurM.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C7yaWvaS.mjs";
4
4
  //#region src/workflows/review/review.ts
5
5
  /**
6
6
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { St as flipProblems, _t as defaults, b as coder, bt as flagMentions, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, vt as envDefaults, wt as requireFlag } from "../chunks/reserved-zAfoJtR7.mjs";
2
+ import { Et as flipProblems, Ot as requireFlag, S as coder, St as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as flagMentions, xt as defaults } from "../chunks/reserved-CfQ8eurM.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.";