@pmelab/gtd 36.0.0 → 38.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 (62) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +30 -9
  3. package/claude/hooks/context.ts +66 -0
  4. package/claude/hooks/drive.ts +3 -0
  5. package/claude/hooks/register.tsx +69 -6
  6. package/dist/chunks/{reserved-BAv71I4i.mjs → reserved-sfMdy7v7.mjs} +83 -22
  7. package/dist/chunks/{tail-kar1zhOb.mjs → tail-Cgca5Gp-.mjs} +1 -1
  8. package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
  9. package/dist/gtd.bundle.mjs +498 -56
  10. package/dist/testkit.mjs +115 -3
  11. package/dist/wire.mjs +2 -2
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +277 -58
  14. package/dist/workflows/fix.mjs +2 -2
  15. package/dist/workflows/review.mjs +2 -2
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +355 -75
  18. package/package.json +1 -1
  19. package/src/cli/Cli.test.ts +10 -3
  20. package/src/cli/Cli.ts +76 -1
  21. package/src/cli/Program.test.ts +1 -0
  22. package/src/cli/Program.ts +28 -0
  23. package/src/cli/ToolCommands.ts +3 -0
  24. package/src/edge/Edge.ts +11 -0
  25. package/src/flows/runtime.ts +11 -0
  26. package/src/replay/Replay.ts +51 -2
  27. package/src/testkit/Layers.ts +1 -0
  28. package/src/testkit/steps.ts +35 -0
  29. package/src/wire/BeatDocument.ts +12 -2
  30. package/src/wire/BeatStatus.ts +3 -0
  31. package/src/wire/ContextLimit.fixture.ts +30 -0
  32. package/src/wire/ContextLimit.test.ts +96 -0
  33. package/src/wire/ContextLimit.ts +169 -0
  34. package/src/wire/index.ts +1 -0
  35. package/src/wire/types.ts +7 -0
  36. package/src/workflow/Workflow.ts +9 -1
  37. package/src/workflows/feature/context-limit.feature +37 -0
  38. package/src/workflows/feature/coverage.ts +22 -8
  39. package/src/workflows/feature/criteria.test.ts +17 -1
  40. package/src/workflows/feature/criteria.ts +26 -3
  41. package/src/workflows/feature/feature.ts +69 -16
  42. package/src/workflows/feature/index.ts +1 -0
  43. package/src/workflows/feature/refactor-absorbs.feature +12 -5
  44. package/src/workflows/feature/refactor-first.feature +11 -2
  45. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  46. package/src/workflows/feature/refactor-rebuild.feature +269 -0
  47. package/src/workflows/feature/refactor-review.feature +45 -1
  48. package/src/workflows/feature/refactor.ts +9 -3
  49. package/src/workflows/feature/seam.test.ts +108 -3
  50. package/src/workflows/feature/seam.ts +169 -31
  51. package/src/workflows/feature/slicing-cap.feature +14 -0
  52. package/src/workflows/feature/slicing.feature +225 -0
  53. package/src/workflows/feature/slicing.test.ts +49 -0
  54. package/src/workflows/feature/slicing.ts +105 -0
  55. package/src/workflows/index.ts +1 -0
  56. package/src/workflows/shared/fences.ts +5 -0
  57. package/src/workflows/shared/options.test.ts +11 -2
  58. package/src/workflows/shared/options.ts +19 -4
  59. package/src/workflows/shared/steps.ts +2 -0
  60. package/src/workflows/shared/text.ts +129 -28
  61. package/src/workflows/shared/vars.ts +1 -0
  62. package/src/workflows/shared/workflows.steps.ts +13 -0
@@ -1,7 +1,7 @@
1
- import { 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";
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 qualityUnresolvedMessage, A as buildReviewCollectingPrompt, B as deferNotesScript, C as planner, D as agentWithSkills, E as reviewing, F as clearCriteriaQuestion, H as designSystem, I as coverageMessage, J as implementBuildingPrompt, L as coverageQuestion, M as buildReviewTriageMessage, N as builderSystem, O as buildReviewAnswerQuestionsPrompt, P as clearCriteriaMessage, Q as normaliseCriteriaScript, R as coveringPrompt, S as fixRisks, U as designTriagePrompt, V as designGateAnswerMessage, X as naiveApplyFailure, Y as implementFixPrompt, Z as naivePassPrompt, _ as REVIEW, _t as testFileGlobs, a as flagModeOn, at as reviewEditsCapture, b as fixCheck, ct as reviewerSystem, d as guarded, dt as startGateBlockedMessage, et as refactorBuildingPrompt, f as healthy, ft as uncoveredMessage, g as REQUIREMENTS, gt as envDefaults, h as REFACTOR, ht as defaults, i as skills, it as reviewEditNotesCapture, j as buildReviewFixNitsPrompt, k as buildReviewAwaitAfterDeferMessage, lt as seamByHandMessage, m as QUALITY, mt as unwindFailure, n as summary, nt as refactorFixPrompt, p as FEEDBACK, pt as unwindFailedMessage, q as idleMessage, r as access, rt as refactorRules, s as shipped, st as reviewNotesCapture, t as steering, tt as refactorDroppedRefusal, u as gate, ut as seamSteerMessage, v as awaitReview, w as reviewMissing, y as coder, z as criterionLine } from "../chunks/reserved-BAv71I4i.mjs";
4
- import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-kar1zhOb.mjs";
3
+ import { $ as naivePassPrompt, A as buildReviewAnswerQuestionsPrompt, B as coveringPrompt, E as reviewMissing, F as builderSystem, G as designTriagePrompt, H as deferNotesScript, I as clearCriteriaMessage, L as clearCriteriaQuestion, M as buildReviewCollectingPrompt, N as buildReviewFixNitsPrompt, O as reviewing, P as buildReviewTriageMessage, Q as naiveApplyFailure, R as coverageMessage, S as fixCheck, T as planner, U as designGateAnswerMessage, V as criterionLine, W as designSystem, X as implementBuildingPrompt, Y as idleMessage, Z as implementFixPrompt, _ as REQUIREMENTS, _t as unwindFailedMessage, a as flagModeOn, at as refactorNotePrompt, b as awaitReview, bt as envDefaults, ct as reviewEditsCapture, d as guarded, dt as reviewerSystem, et as normaliseCriteriaScript, f as healthy, ft as seamByHandMessage, g as REFACTOR_NOTE, gt as uncoveredMessage, h as REFACTOR, ht as startGateBlockedMessage, i as skills, it as refactorFixPrompt, j as buildReviewAwaitAfterDeferMessage, k as agentWithSkills, m as QUALITY, mt as slicingPrompt, n as summary, nt as refactorBuildingPrompt, ot as refactorRules, p as FEEDBACK, pt as seamSteerMessage, r as access, rt as refactorDroppedRefusal, s as shipped, st as reviewEditNotesCapture, t as steering, tt as qualityUnresolvedMessage, u as gate, ut as reviewNotesCapture, v as REVIEW, vt as unwindFailure, w as fixRisks, x as coder, xt as testFileGlobs, y as SLICES, yt as defaults, z as coverageQuestion } from "../chunks/reserved-sfMdy7v7.mjs";
4
+ import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-Cgca5Gp-.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;
@@ -623,7 +629,8 @@ const plan = (shape, options) => {
623
629
  return {
624
630
  pattern,
625
631
  strays,
626
- reference: options.reference?.trim() ? options.reference : void 0
632
+ reference: options.reference?.trim() ? options.reference : void 0,
633
+ note: options.note?.trim() ? options.note : void 0
627
634
  };
628
635
  };
629
636
  const guardOf = ({ pattern, strays }, entry) => () => {
@@ -632,13 +639,22 @@ const guardOf = ({ pattern, strays }, entry) => () => {
632
639
  };
633
640
  /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
634
641
  const refactorLap = async (p, guard, feedback) => {
635
- const { pattern, strays, reference } = p;
636
- await run("deriving", deriveScript([...packageLines(strays, pattern, reference), ...feedback === void 0 ? [] : [
637
- "## Review feedback",
638
- "",
639
- ...demoteHeadings(feedback),
640
- ""
641
- ]]), {
642
+ const { pattern, strays, reference, note } = p;
643
+ await run("deriving", deriveScript([
644
+ ...packageLines(strays, pattern, reference),
645
+ ...note === void 0 ? [] : [
646
+ "## Refactor note",
647
+ "",
648
+ ...demoteHeadings(note),
649
+ ""
650
+ ],
651
+ ...feedback === void 0 ? [] : [
652
+ "## Review feedback",
653
+ "",
654
+ ...demoteHeadings(feedback),
655
+ ""
656
+ ]
657
+ ]), {
642
658
  label: "Deriving the refactor package",
643
659
  file: REFACTOR_PACKAGE
644
660
  });
@@ -647,7 +663,8 @@ const refactorLap = async (p, guard, feedback) => {
647
663
  strays,
648
664
  pattern,
649
665
  reference,
650
- feedback: feedback !== void 0
666
+ feedback: feedback !== void 0,
667
+ note: note !== void 0
651
668
  }), {
652
669
  label: "Refactoring",
653
670
  model: coder(),
@@ -733,22 +750,27 @@ const COVERED_MIN_P = .7;
733
750
  /** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
734
751
  const COVERING_RETRIES = 2;
735
752
  const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
736
- const green = () => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
753
+ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
737
754
  label: "Fixing the check",
738
755
  file: FEEDBACK,
739
756
  model: coder(),
740
757
  system: builderSystem()
741
758
  }), {
742
- guard: frozen,
759
+ guard: () => {
760
+ frozen();
761
+ guard?.();
762
+ },
743
763
  guardRests: false
744
764
  });
745
765
  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");
746
766
  /**
747
767
  * The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
748
- * over — no passing test since `base` covers, by the judge.
768
+ * over, only `slice`'s when given — no passing test since `base` covers, by the judge.
749
769
  */
750
- const uncoveredSince = async (base) => {
751
- const criteria = (criteriaOf(read(".gtd/REQUIREMENTS.md") ?? "") ?? []).filter((c) => c.id !== "");
770
+ const uncoveredSince = async (base, slice) => {
771
+ const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
772
+ const own = criterionIds(requirements, slice);
773
+ const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
752
774
  if (criteria.length === 0) return {
753
775
  covered: [],
754
776
  uncovered: []
@@ -775,14 +797,14 @@ const record = (covered, uncovered = []) => {
775
797
  * sending uncovered IDs back to a covering turn; marks the IDs covered on the
776
798
  * landing that ends it. A human may overrule once the retries are spent.
777
799
  */
778
- const requireCoverage = async (base = start()) => {
800
+ const requireCoverage = async (base = start(), slice) => {
779
801
  let retries = 0;
780
802
  for (;;) {
781
- const { covered, uncovered } = await uncoveredSince(base);
803
+ const { covered, uncovered } = await uncoveredSince(base, slice);
782
804
  if (uncovered.length === 0) return record(covered);
783
805
  if (retries < COVERING_RETRIES) {
784
806
  retries++;
785
- await agentWithSkills("covering", coveringPrompt(uncovered), {
807
+ await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
786
808
  label: "Covering the acceptance criteria",
787
809
  model: coder(),
788
810
  system: builderSystem(),
@@ -813,7 +835,7 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
813
835
  allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
814
836
  exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
815
837
  });
816
- const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
838
+ const naivePass = (retry, slice) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice)), {
817
839
  label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
818
840
  keep: true,
819
841
  model: coder(),
@@ -853,16 +875,20 @@ const addedLines = (change) => {
853
875
  const before = new Set((change.before ?? "").split("\n"));
854
876
  return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
855
877
  };
878
+ const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
879
+ const idsOf = (file, slice) => criterionIds(read(file) ?? "", slice);
880
+ const unnamed = (ids, tests) => {
881
+ const lines = tests.map(addedLines);
882
+ return ids.filter((id) => !lines.some((text) => named(id, text)));
883
+ };
856
884
  /**
857
885
  * Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
858
886
  * test line of the pass names, or a test importing a file the pass added —
859
887
  * naive structure a reset and refactor replaces, which the tests must survive.
860
888
  */
861
- const acceptanceFault = (file, tip) => {
862
- const ids = (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
863
- const tests = tip.changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
864
- const lines = tests.map(addedLines);
865
- const untested = ids.filter((id) => !lines.some((text) => named(id, text)));
889
+ const acceptanceFault = (file, tip, slice) => {
890
+ const tests = acceptanceTests(tip.changes);
891
+ const untested = unnamed(idsOf(file, slice), tests);
866
892
  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]}] …"`;
867
893
  const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
868
894
  const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
@@ -871,6 +897,41 @@ const acceptanceFault = (file, tip) => {
871
897
  });
872
898
  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")}`;
873
899
  };
900
+ /** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
901
+ const naiveAcceptance = (tip, slice) => {
902
+ const ids = idsOf(REQUIREMENTS, slice);
903
+ const perTest = acceptanceTests(tip.changes).map((c) => {
904
+ const lines = addedLines(c);
905
+ return {
906
+ path: c.path,
907
+ ids: ids.filter((id) => named(id, lines))
908
+ };
909
+ });
910
+ return {
911
+ hash: tip.hash,
912
+ tests: perTest.filter((c) => c.ids.length > 0).map((c) => c.path),
913
+ ids: ids.filter((id) => perTest.some((c) => c.ids.includes(id)))
914
+ };
915
+ };
916
+ /**
917
+ * Refuse a rebuild whose test changes since `since` leave out an ID the naive
918
+ * pass's tests named. A skipped test still counts: the gate reads text, never runs it.
919
+ */
920
+ const requireAcceptance = (acceptance, since) => {
921
+ const missing = unnamed(acceptance.ids, acceptanceTests(changesSince(since)));
922
+ if (missing.length === 0) return;
923
+ refuse(`gtd land: rebuild-acceptance: the rebuild's tests leave out ${missing.join(", ")} — carry over the naive pass's acceptance tests (\`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`), each with its ID in the test or scenario name`);
924
+ };
925
+ /**
926
+ * Refuse a fix turn that, on its own, drops an ID the naive pass's tests named
927
+ * from a test file. Turn-local, so a person's edit at an escalation rest never
928
+ * wedges later fix turns.
929
+ */
930
+ const requireAcceptanceKept = (acceptance) => {
931
+ const dropped = acceptance.ids.filter((id) => changes().filter((c) => isCode$1(c.path) && isTestFile(c.path)).some((c) => named(id, c.before ?? "") && !named(id, c.after ?? "")));
932
+ if (dropped.length === 0) return;
933
+ refuse(`gtd land: rebuild-acceptance: this turn drops ${dropped.join(", ")} from the tests — the naive pass's acceptance tests stay; fix the code, not them`);
934
+ };
874
935
  const restore = (path) => `if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`;
875
936
  /**
876
937
  * Run the suite with `tip`'s diff applied, then take it out again; resolves
@@ -914,18 +975,30 @@ function implement(first, tier, amend) {
914
975
  amend
915
976
  };
916
977
  return scope("implement", async () => {
917
- if (options.naive === void 0 || !await applied(options.naive)) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn()), {
978
+ const since = head();
979
+ const acceptance = options.rebuild?.acceptance;
980
+ const built = options.naive === void 0 || !await applied(options.naive);
981
+ if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice)), {
918
982
  label: "Implementing",
919
983
  model: coder(),
920
984
  system: builderSystem(),
921
985
  ...options.afterRefactor === true ? { allowEmpty: true } : {}
922
986
  });
923
987
  requireCriteriaFrozen(REQUIREMENTS);
924
- await green();
925
- await requireCoverage();
926
- await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
988
+ if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
989
+ await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
990
+ await requireCoverage(options.base, options.slice);
991
+ await closing(options.more === true);
927
992
  });
928
993
  }
994
+ /** The last slice's closing, for a last slice a steer left with no criterion to build. */
995
+ const closeOut = () => scope("implement", () => closing(false));
996
+ /** A slice with `more` after it keeps the criteria and the slicing for the next one. */
997
+ const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE] : [
998
+ REQUIREMENTS,
999
+ REFACTOR_NOTE,
1000
+ SLICES
1001
+ ]), { label: "Closing out the implementation" });
929
1002
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
930
1003
  const steer = async (retry) => {
931
1004
  await human("steer", {
@@ -956,8 +1029,8 @@ const budget = () => {
956
1029
  };
957
1030
  };
958
1031
  /** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
959
- const vetted = async (tip, vetting) => {
960
- const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
1032
+ const vetted = async (tip, vetting, slice) => {
1033
+ const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip, slice);
961
1034
  if (fault !== void 0) {
962
1035
  const verdict = await vetting.again({
963
1036
  fault,
@@ -973,33 +1046,56 @@ const vetted = async (tip, vetting) => {
973
1046
  /** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
974
1047
  const ruled = async (tip, again) => {
975
1048
  const shape = await judgeSeam(tip);
976
- if (shape.choice === "land") return { reference: tip.hash };
1049
+ if (shape.choice === "land") return {
1050
+ reference: tip.hash,
1051
+ tip
1052
+ };
977
1053
  if (shape.choice === "refactor-first") return {
978
1054
  reference: tip.hash,
1055
+ tip,
979
1056
  shape
980
1057
  };
981
1058
  const next = await again({
982
1059
  strays: shape.strays,
983
1060
  p: shape.judgment?.answers.shape?.p ?? 0
984
1061
  });
985
- return next.kind === "overruled" ? { reference: tip.hash } : next;
1062
+ return next.kind === "overruled" ? {
1063
+ reference: tip.hash,
1064
+ tip
1065
+ } : next;
986
1066
  };
987
- const decide = () => scope("seam", async () => {
1067
+ const decide = (slice) => scope("seam", async () => {
988
1068
  const vetting = {
989
1069
  again: budget(),
990
1070
  waived: false
991
1071
  };
992
1072
  let retry;
993
1073
  for (;;) {
994
- await naivePass(retry);
1074
+ await naivePass(retry, slice);
995
1075
  const tip = kept("naive");
996
- const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : { reference: tip.hash });
1076
+ const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
1077
+ reference: tip.hash,
1078
+ tip
1079
+ });
997
1080
  if (!("kind" in outcome)) return outcome;
998
1081
  retry = outcome.kind === "retry" ? outcome.retry : void 0;
999
1082
  }
1000
1083
  });
1001
- const seamBuild = async (base) => {
1002
- const { reference, shape } = await decide();
1084
+ /** Write the refactor note from the whole naive pass; a turn that changes anything else is refused. */
1085
+ const refactorNote = async (reference, strays) => {
1086
+ await agent("note", refactorNotePrompt(reference, strays), {
1087
+ label: "Writing the refactor note",
1088
+ file: REFACTOR_NOTE,
1089
+ model: coder(),
1090
+ system: builderSystem()
1091
+ });
1092
+ const others = changes().map((c) => c.path).filter((path) => path !== REFACTOR_NOTE);
1093
+ const note = read(".gtd/REFACTOR-NOTE.md") ?? "";
1094
+ 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")}` : ""}`);
1095
+ return note;
1096
+ };
1097
+ const seamBuild = async (base, slice$1 = {}) => {
1098
+ const { reference, tip, shape } = await decide(slice$1.criteria);
1003
1099
  if (shape === void 0) return {
1004
1100
  reference,
1005
1101
  base,
@@ -1013,42 +1109,164 @@ const seamBuild = async (base) => {
1013
1109
  }));
1014
1110
  return {
1015
1111
  reference,
1016
- base
1112
+ base,
1113
+ rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
1017
1114
  };
1018
1115
  }
1019
- if (refactorStrays(shape).length === 0) return {
1116
+ const strays = refactorStrays(shape);
1117
+ if (strays.length === 0) return {
1020
1118
  reference,
1021
- base
1119
+ base,
1120
+ rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
1022
1121
  };
1023
- const refactored = await slice("refactor", () => reviewedRefactor(shape, { reference }));
1122
+ const note = await scope("seam", () => refactorNote(reference, strays));
1123
+ const refactored = await slice(slice$1.refactor ?? "refactor", () => reviewedRefactor(shape, {
1124
+ reference,
1125
+ note
1126
+ }));
1024
1127
  return {
1025
1128
  reference,
1026
1129
  base: head(),
1027
- refactored
1130
+ refactored,
1131
+ rebuild: {
1132
+ note,
1133
+ acceptance: naiveAcceptance(tip, slice$1.criteria)
1134
+ }
1028
1135
  };
1029
1136
  };
1030
1137
  //#endregion
1138
+ //#region src/workflows/feature/slicing.ts
1139
+ const ID = /^(AC-\d+)(?![\w-])/;
1140
+ /** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
1141
+ const sliceCap = () => {
1142
+ const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria;
1143
+ if (!/^[1-9]\d*$/.test(value)) refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`);
1144
+ return Number(value);
1145
+ };
1146
+ /** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
1147
+ const slicesOf = (text) => {
1148
+ const slices = [];
1149
+ let open = false;
1150
+ for (const { line, fenced } of markFenced(text)) {
1151
+ if (fenced) continue;
1152
+ const heading = HEADING.exec(line);
1153
+ if (heading !== null) {
1154
+ open = heading[1] === "##";
1155
+ if (open) slices.push({
1156
+ title: heading[2],
1157
+ ids: []
1158
+ });
1159
+ continue;
1160
+ }
1161
+ const item = ITEM.exec(line);
1162
+ if (item !== null && open) slices.at(-1).ids.push(ID.exec(item[1])?.[1] ?? "");
1163
+ }
1164
+ return slices;
1165
+ };
1166
+ const twice = (list) => [...new Set(list.filter((id, i) => list.indexOf(id) !== i))];
1167
+ /** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
1168
+ const sliceFault = (slices, ids, cap) => {
1169
+ const listed = slices.flatMap((s) => s.ids);
1170
+ if (slices.length === 0) return `no "## " slice`;
1171
+ const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title);
1172
+ if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`;
1173
+ if (listed.includes("")) return "a list item not led by a criterion ID";
1174
+ const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title);
1175
+ if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`;
1176
+ const repeated = twice(listed);
1177
+ if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`;
1178
+ const unknown = listed.filter((id) => !ids.includes(id));
1179
+ if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`;
1180
+ const missing = ids.filter((id) => !listed.includes(id));
1181
+ if (missing.length > 0) return `no slice for: ${missing.join(", ")}`;
1182
+ };
1183
+ /** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
1184
+ const requireSlices = (ids, cap) => {
1185
+ const slices = slicesOf(read(".gtd/SLICES.md") ?? "");
1186
+ const why = sliceFault(slices, ids, cap);
1187
+ 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`);
1188
+ return slices;
1189
+ };
1190
+ /**
1191
+ * The slices to build, in order. Criteria within `sliceMaxCriteria` are one
1192
+ * slice and no turn; above it `design.slicing` groups them.
1193
+ */
1194
+ const slicing = async () => {
1195
+ const all = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
1196
+ const cap = sliceCap();
1197
+ if (all.length <= cap) return [{
1198
+ title: "",
1199
+ ids: all
1200
+ }];
1201
+ return scope("design", async () => {
1202
+ await agentWithSkills("slicing", slicingPrompt(cap), {
1203
+ label: "Slicing the change",
1204
+ file: SLICES,
1205
+ model: planner(),
1206
+ system: designSystem()
1207
+ });
1208
+ return requireSlices(all, cap);
1209
+ });
1210
+ };
1211
+ /** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
1212
+ 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) };
1213
+ //#endregion
1031
1214
  //#region src/workflows/feature/feature.ts
1032
1215
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
1033
1216
  const unwind = () => {
1034
1217
  const commit = head();
1035
1218
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
1036
1219
  };
1037
- /** A naive pass (judged only with `flagMode` on), then implement and the quality lap; design must have run since `firstBase`. */
1220
+ /**
1221
+ * The slices `design.slicing` grouped the criteria into, each built in turn
1222
+ * from where the previous one landed; design must have run since `firstBase`.
1223
+ */
1038
1224
  const buildFeature = async (firstBase) => {
1039
1225
  const tier = riskTierOn() ? recordRisk() : void 0;
1040
- const seam = await seamBuild(firstBase);
1041
- await slice("feature", async () => {
1226
+ const slices = await slicing();
1227
+ const one = slices.length === 1;
1228
+ for (const i of slices.keys()) {
1229
+ const more = i < slices.length - 1;
1230
+ const criteria = sliceCriteria(slices, i);
1231
+ if (sliceIds(criteria)?.length === 0) {
1232
+ if (!more) await closeOut();
1233
+ continue;
1234
+ }
1235
+ const n = one ? "" : `-${i + 1}`;
1236
+ await buildSlice(i === 0 ? firstBase : head(), {
1237
+ tier,
1238
+ criteria,
1239
+ refactor: `refactor${n}`,
1240
+ feature: `feature${n}`,
1241
+ more
1242
+ });
1243
+ }
1244
+ };
1245
+ /** A naive pass (judged only with `flagMode` on), then implement and the quality lap. */
1246
+ const buildSlice = async (sliceBase, build) => {
1247
+ const seam = await seamBuild(sliceBase, {
1248
+ criteria: build.criteria,
1249
+ refactor: build.refactor
1250
+ });
1251
+ await slice(build.feature, async () => {
1042
1252
  await implement({
1043
1253
  reference: seam.reference,
1044
- tier,
1254
+ tier: build.tier,
1045
1255
  afterRefactor: seam.refactored,
1046
- naive: seam.land === true ? seam.reference : void 0
1256
+ naive: seam.land === true ? seam.reference : void 0,
1257
+ rebuild: seam.rebuild,
1258
+ slice: build.criteria,
1259
+ base: build.criteria === void 0 ? void 0 : sliceBase,
1260
+ more: build.more
1047
1261
  });
1048
- const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path)) ? firstBase : seam.base;
1262
+ const carried = seam.rebuild?.acceptance?.tests ?? [];
1263
+ const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? sliceBase : seam.base;
1264
+ const guard = build.more ? () => requireCriteriaFrozen(REQUIREMENTS) : void 0;
1049
1265
  await tail(settleQuality, {
1050
1266
  green: true,
1051
- lensBase: base
1267
+ lensBase: base,
1268
+ guard,
1269
+ guardRests: false
1052
1270
  });
1053
1271
  });
1054
1272
  };
@@ -1094,6 +1312,7 @@ const ordinaryStart = async () => {
1094
1312
  });
1095
1313
  refusePendingHandOff();
1096
1314
  flagModeOn();
1315
+ sliceCap();
1097
1316
  const stated = statedCriteria(changes(), head());
1098
1317
  await unwind();
1099
1318
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
@@ -1109,4 +1328,4 @@ const feature = shipped(ordinaryStart);
1109
1328
  //#region src/workflows/feature/index.ts
1110
1329
  var feature_default = feature;
1111
1330
  //#endregion
1112
- 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, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unwind };
1331
+ 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, 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 { D as agentWithSkills, G as fixReviewNotesPrompt, W as finisherSystem, _ as REVIEW, c as baseline, gt as envDefaults, ht as defaults, i as skills, n as summary, r as access, s as shipped, t as steering, y as coder } from "../chunks/reserved-BAv71I4i.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-kar1zhOb.mjs";
3
+ import { K as finisherSystem, bt as envDefaults, c as baseline, i as skills, k as agentWithSkills, n as summary, q as fixReviewNotesPrompt, r as access, s as shipped, t as steering, v as REVIEW, x as coder, yt as defaults } from "../chunks/reserved-sfMdy7v7.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-Cgca5Gp-.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 { gt as envDefaults, ht as defaults, i as skills, n as summary, ot as reviewGateBlockedMessage, r as access, s as shipped, t as steering, u as gate } from "../chunks/reserved-BAv71I4i.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-kar1zhOb.mjs";
2
+ import { bt as envDefaults, i as skills, lt as reviewGateBlockedMessage, n as summary, r as access, s as shipped, t as steering, u as gate, yt as defaults } from "../chunks/reserved-sfMdy7v7.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Cgca5Gp-.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 requireFlag, bt as flipProblems, gt as envDefaults, ht as defaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, vt as flagMentions, y as coder } from "../chunks/reserved-BAv71I4i.mjs";
2
+ import { Et as requireFlag, St as flagMentions, bt 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 flipProblems, x as coder, yt as defaults } from "../chunks/reserved-sfMdy7v7.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.";