@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
@@ -2784,7 +2784,7 @@ const characterEntities = {
2784
2784
  };
2785
2785
  //#endregion
2786
2786
  //#region node_modules/decode-named-character-reference/index.js
2787
- const own$1 = {}.hasOwnProperty;
2787
+ const own$2 = {}.hasOwnProperty;
2788
2788
  /**
2789
2789
  * Decode a single character reference (without the `&` or `;`).
2790
2790
  * You probably only need this when you’re building parsers yourself that follow
@@ -2797,7 +2797,7 @@ const own$1 = {}.hasOwnProperty;
2797
2797
  * Decoded reference.
2798
2798
  */
2799
2799
  function decodeNamedCharacterReference(value) {
2800
- return own$1.call(characterEntities, value) ? characterEntities[value] : false;
2800
+ return own$2.call(characterEntities, value) ? characterEntities[value] : false;
2801
2801
  }
2802
2802
  //#endregion
2803
2803
  //#region node_modules/micromark-util-chunked/index.js
@@ -10261,7 +10261,7 @@ function index(value) {
10261
10261
  * Options
10262
10262
  * } from './types.js'
10263
10263
  */
10264
- const own = {}.hasOwnProperty;
10264
+ const own$1 = {}.hasOwnProperty;
10265
10265
  /**
10266
10266
  * Turn markdown into a syntax tree.
10267
10267
  *
@@ -10440,7 +10440,7 @@ function compiler(options) {
10440
10440
  index = -1;
10441
10441
  while (++index < events.length) {
10442
10442
  const handler = config[events[index][0]];
10443
- if (own.call(handler, events[index][1].type)) handler[events[index][1].type].call(Object.assign({ sliceSerialize: events[index][2].sliceSerialize }, context), events[index][1]);
10443
+ if (own$1.call(handler, events[index][1].type)) handler[events[index][1].type].call(Object.assign({ sliceSerialize: events[index][2].sliceSerialize }, context), events[index][1]);
10444
10444
  }
10445
10445
  if (context.tokenStack.length > 0) {
10446
10446
  const tail = context.tokenStack[context.tokenStack.length - 1];
@@ -11152,7 +11152,7 @@ function configure(combined, extensions) {
11152
11152
  function extension(combined, extension) {
11153
11153
  /** @type {keyof Extension} */
11154
11154
  let key;
11155
- for (key in extension) if (own.call(extension, key)) switch (key) {
11155
+ for (key in extension) if (own$1.call(extension, key)) switch (key) {
11156
11156
  case "canContainEols": {
11157
11157
  const right = extension[key];
11158
11158
  if (right) combined[key].push(...right);
@@ -16934,9 +16934,12 @@ const defaults = {
16934
16934
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
16935
16935
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
16936
16936
  flagMode: "true",
16937
+ sliceMaxCriteria: "8",
16937
16938
  flag_risk_tier: "0",
16938
16939
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
16939
- refactorQualityReviews: "correctness, ponytail-review, conventions"
16940
+ refactorQualityReviews: "correctness, ponytail-review, conventions",
16941
+ continuations: "3",
16942
+ continuationStall: "2"
16940
16943
  };
16941
16944
  /** Environment settings. */
16942
16945
  const envDefaults = {
@@ -17393,6 +17396,24 @@ printf '%s\\n' ${[
17393
17396
  ].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
17394
17397
  `;
17395
17398
  };
17399
+ const slicingPrompt = (cap) => `${stateFileRules}
17400
+ - Read \`.gtd/REQUIREMENTS.md\`: its \`## Acceptance Criteria\` are settled
17401
+ and more than ${cap}, too many to build in one slice
17402
+ - Group them into slices in \`.gtd/SLICES.md\`, one \`## <title>\` heading per
17403
+ slice over a \`- AC-<n>\` list item per criterion it builds. Put criteria of
17404
+ the same flow or subsystem together; every criterion in exactly one slice,
17405
+ at most ${cap} per slice
17406
+ - Keep it coarse: as few slices as the cap allows, never one per criterion
17407
+ - Order the slices so each builds on the ones before: they run in that order,
17408
+ each from where the previous one landed
17409
+ - Change nothing but \`.gtd/SLICES.md\` — leave the criteria as they are
17410
+ - Leave everything uncommitted and finish your turn — do not commit
17411
+ `;
17412
+ /** The line telling a slice's turn which criteria are its own; empty for a single slice. */
17413
+ const sliceLine = (ids) => ids === void 0 ? "" : `- This slice builds ${ids.join(", ")} only: every rule here about the criteria
17414
+ means these. The others are for other slices, before or after it — leave
17415
+ them unbuilt and untested
17416
+ `;
17396
17417
  const designSystem = () => `${designPersona}
17397
17418
 
17398
17419
  ${agentConduct}`;
@@ -17436,7 +17457,7 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
17436
17457
  - Introduce no new flag read and add no second read of an existing one:
17437
17458
  every match of \`${pattern}\` may move, never increase in number`;
17438
17459
  const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
17439
- const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
17460
+ const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false, from }) => `${stateFileRules}
17440
17461
  - The package to implement is \`${pkg}\`; never delete it
17441
17462
  ${note ? `- The package's \`## Refactor note\` section names the seams to open and the
17442
17463
  shape each takes, written from the whole naive pass: build exactly those
@@ -17451,8 +17472,9 @@ ${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${st
17451
17472
  restructuring afresh here, minus the feature's own behaviour. A
17452
17473
  refactor that touches none of it is wrong, however clean — never
17453
17474
  swap in an unrelated tidy-up
17454
- `}- Leave everything uncommitted and finish your turn — do not commit
17455
- `;
17475
+ `}${handoffRule()}
17476
+ - Leave everything uncommitted and finish your turn — do not commit
17477
+ ${from === void 0 ? "" : continuationLines(from)}`;
17456
17478
  const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
17457
17479
  ${fixFeedbackPrompt}
17458
17480
  ${refactorRules(strays, pattern)}
@@ -18020,7 +18042,7 @@ Print the closing message and stop — this writes nothing itself.
18020
18042
  const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
18021
18043
  const isRed = (retry) => "red" in retry;
18022
18044
  const isFault = (retry) => "fault" in retry;
18023
- const pickUp = (hash) => `Pick it up — \`git diff --binary ${hash}^ ${hash} | git apply\``;
18045
+ const pickUp = (hash, exclude) => `Pick it up — \`git diff --binary --no-color --no-ext-diff --no-textconv ${hash}^ ${hash}${exclude === void 0 ? "" : ` -- ${quote(":(top)")} ${quote(`:(top,exclude)${exclude}`)}`} | git apply\``;
18024
18046
  const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
18025
18047
  the file, never from memory, and stop only when each one holds. They are
18026
18048
  settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
@@ -18039,9 +18061,35 @@ re-implement. It changed these files outside the expected set:
18039
18061
  ${listed(retry.strays)}
18040
18062
  Start over and choose a different approach; do not rebuild the same diff.
18041
18063
  `;
18042
- const naivePassPrompt = (retry) => `${stateFileRules}
18064
+ const quoted = (text) => text.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n");
18065
+ /** Every turn a continuation may pick up tells its agent where the note goes. */
18066
+ const handoffRule = () => `- Should you be told to stop at a context limit, write the handoff note to
18067
+ \`.gtd/HANDOFF.md\` — what you touched, what was next — and finish your turn:
18068
+ a fresh turn picks up from what you leave, broken or not. That file is the one
18069
+ exception to any path rule here; write it at no other time`;
18070
+ /** `pickUp`: how the snapshot gets into the tree — absent when it already is. */
18071
+ const continuationLines = (from, pickUp) => `
18072
+ Your previous turn stopped at its context limit; this fresh turn picks up from
18073
+ its snapshot (\`${from.snapshot}\`). ${pickUp === void 0 ? "Its work is already in the tree." : `${pickUp}.`} Its handoff note:
18074
+
18075
+ ${quoted(from.note === "" ? "(empty)" : from.note)}
18076
+
18077
+ Still open: ${from.open.length === 0 ? "nothing the workflow can count — finish what the note says is next" : from.open.join(", ")}.
18078
+ ${pickUp === void 0 ? "Delete `.gtd/HANDOFF.md` once you are done; rewrite it should you be stopped again.\n" : ""}`;
18079
+ const oversizedMessage = (phase, from, why, narrowable = true) => `The ${phase} keeps outgrowing its context: ${"cap" in why ? why.cap === 0 ? "continuations are off (`continuations` is 0) and the turn stopped at its context limit" : `${why.cap} continuation${why.cap === 1 ? "" : "s"} ran and the last still stopped at its context limit` : `the last ${why.stall} continuation${why.stall === 1 ? "" : "s"} closed nothing still open`}. The last snapshot is \`${from.snapshot}\`; still open: ${from.open.length === 0 ? "nothing countable" : from.open.join(", ")}.
18080
+
18081
+ Its handoff note:
18082
+
18083
+ ${quoted(from.note === "" ? "(empty)" : from.note)}
18084
+
18085
+ Land to go on: a fresh round of continuations picks up from that snapshot${narrowable ? ` — narrow
18086
+ \`.gtd/REQUIREMENTS.md\` first if the change is too big, or raise the context limit` : ` — raise
18087
+ the context limit first if the refactor is too big`}. \`gtd abandon\` gives the change up.
18088
+ `;
18089
+ /** `from`: a continuation of a pass that stopped at its context limit, in place of `retry`'s pick-up. */
18090
+ const naivePassPrompt = (retry, slice, from) => `${stateFileRules}
18043
18091
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18044
- ${criteriaBinding(false)}
18092
+ ${sliceLine(slice)}${criteriaBinding(false)}
18045
18093
  - Implement them the quickest way that works — the naive version. Do not
18046
18094
  restructure, abstract or refactor around the change; it may land exactly as
18047
18095
  you leave it, and a reviewer makes it fit
@@ -18056,9 +18104,20 @@ ${criteriaBinding(false)}
18056
18104
  - Run \`${env.testCommand}\` and finish only once it is green with every
18057
18105
  criterion's test passing: the next step runs it on your diff and sends a red
18058
18106
  pass back
18059
- - Leave \`.gtd/\` untouched
18107
+ - Leave \`.gtd/\` untouched, a handoff note excepted
18108
+ ${handoffRule()}
18060
18109
  - Leave everything uncommitted and finish your turn — do not commit
18061
- ${retry === void 0 ? "" : naiveRetry(retry)}`;
18110
+ ${from !== void 0 ? `${continuationLines(from, pickUp(from.snapshot, ".gtd/HANDOFF.md"))}${retry === void 0 ? "" : isFault(retry) ? `
18111
+ The pass this continues was a retry: its tests fell short, and still must not:
18112
+ ${retry.fault}
18113
+ ` : isRed(retry) ? `
18114
+ The pass this continues was a retry after a red suite (\`.gtd/FEEDBACK.md\`
18115
+ holds the output): finish only once the suite is green.
18116
+ ` : `
18117
+ The pass this continues was a retry after a \`reject\`: keep away from the
18118
+ approach that changed these files outside the expected set:
18119
+ ${listed(retry.strays)}
18120
+ `}` : retry === void 0 ? "" : naiveRetry(retry)}`;
18062
18121
  const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
18063
18122
  const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
18064
18123
  opened; plug the change into them:
@@ -18091,9 +18150,13 @@ const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed an
18091
18150
  though never a release flag. Build only what is still missing, held to every
18092
18151
  rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
18093
18152
  `;
18094
- const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}) => `${stateFileRules}
18153
+ const flagLine = (flagMode, tier, amend, sliced) => !flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : sliced ? `- Put the new behaviour behind the release flag an earlier slice of this
18154
+ change added; only when none did, behind a new one, following the
18155
+ repository's flag conventions` : `- Put the new behaviour behind a new release flag, following the
18156
+ repository's flag conventions`;
18157
+ const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}, slice, from) => `${stateFileRules}
18095
18158
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18096
- ${criteriaBinding(true)}
18159
+ ${sliceLine(slice)}${criteriaBinding(true)}
18097
18160
  ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
18098
18161
  the re-derived requirements instead of rewriting it
18099
18162
  ` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
@@ -18102,14 +18165,14 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `-
18102
18165
  `}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
18103
18166
  Read it for ideas; never apply, cherry-pick or check it out — write
18104
18167
  the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
18105
- `}${rebuildLines(rebuild)}${!flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
18106
- repository's flag conventions`}
18168
+ `}${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend, slice !== void 0)}
18107
18169
  - Write the tests first, then the code that makes them pass. Name each
18108
18170
  criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
18109
18171
  green a judge checks every ID has a passing test, and sends uncovered ones back
18172
+ ${handoffRule()}
18110
18173
  - Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
18111
18174
  finish your turn
18112
- `;
18175
+ ${from === void 0 ? "" : continuationLines(from)}`;
18113
18176
  const implementFixPrompt = () => `${stateFileRules}
18114
18177
  ${fixFeedbackPrompt}
18115
18178
  - Leave everything uncommitted and finish your turn — do not commit
@@ -18184,9 +18247,9 @@ const coverageMessage = () => `Judging acceptance-criteria coverage. Run \`gtd j
18184
18247
  \`yes\`/\`no\` verdict per criterion ID — or land without a verdict, which
18185
18248
  counts every criterion as uncovered.
18186
18249
  `;
18187
- const coveringPrompt = (uncovered) => `${stateFileRules}
18250
+ const coveringPrompt = (uncovered, slice) => `${stateFileRules}
18188
18251
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18189
- ${criteriaBinding(true)}
18252
+ ${sliceLine(slice)}${criteriaBinding(true)}
18190
18253
  - A judge found no passing test in this change that covers ${uncovered.join(", ")}.
18191
18254
  Write a behaviour-level test for each, naming its ID in the test or scenario
18192
18255
  name (\`[${uncovered[0]}] …\`), and fix the code if the test shows it falls short
@@ -18213,6 +18276,8 @@ const REVIEW = ".gtd/REVIEW.md";
18213
18276
  const REFACTOR = ".gtd/REFACTOR.md";
18214
18277
  const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
18215
18278
  const QUALITY = ".gtd/QUALITY.md";
18279
+ const SLICES = ".gtd/SLICES.md";
18280
+ const HANDOFF = ".gtd/HANDOFF.md";
18216
18281
  const planner = () => env.plannerModel ?? "";
18217
18282
  const coder = () => env.coderModel ?? "";
18218
18283
  const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
@@ -37715,20 +37780,23 @@ Struct({
37715
37780
  });
37716
37781
  //#endregion
37717
37782
  //#region src/workflows/shared/options.ts
37718
- const setting = optionalWith(String$, { exact: true });
37783
+ const setting$1 = optionalWith(String$, { exact: true });
37719
37784
  const ShippedOptions = Struct({
37720
- reviewBase: setting,
37721
- judgeBudgetBytes: setting,
37722
- judgeIdenticalMinP: setting,
37723
- reviewNoteActionable: setting,
37724
- reviewSeverityMinP: setting,
37725
- refactorFlagPattern: setting,
37726
- seamAllow: setting,
37727
- qualityReviews: setting,
37728
- refactorQualityReviews: setting,
37729
- testCommand: setting,
37730
- plannerModel: setting,
37731
- coderModel: setting,
37785
+ reviewBase: setting$1,
37786
+ judgeBudgetBytes: setting$1,
37787
+ judgeIdenticalMinP: setting$1,
37788
+ reviewNoteActionable: setting$1,
37789
+ reviewSeverityMinP: setting$1,
37790
+ refactorFlagPattern: setting$1,
37791
+ seamAllow: setting$1,
37792
+ sliceMaxCriteria: setting$1,
37793
+ qualityReviews: setting$1,
37794
+ refactorQualityReviews: setting$1,
37795
+ continuations: setting$1,
37796
+ continuationStall: setting$1,
37797
+ testCommand: setting$1,
37798
+ plannerModel: setting$1,
37799
+ coderModel: setting$1,
37732
37800
  flagMode: optionalWith(Boolean$, { exact: true }),
37733
37801
  contextLimit: optionalWith(Struct({
37734
37802
  soft: optionalWith(Number$, { exact: true }),
@@ -37759,7 +37827,7 @@ const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOp
37759
37827
  }));
37760
37828
  const FlagOptions = Struct({
37761
37829
  ...ShippedOptions.fields,
37762
- flagRegistry: setting
37830
+ flagRegistry: setting$1
37763
37831
  });
37764
37832
  const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
37765
37833
  onExcessProperty: "error",
@@ -37904,13 +37972,15 @@ const markFenced = (text) => {
37904
37972
  };
37905
37973
  });
37906
37974
  };
37975
+ /** A markdown heading: its `#` run, then its text. */
37976
+ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
37977
+ /** A list item, its checkbox dropped: its text. */
37978
+ const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
37907
37979
  //#endregion
37908
37980
  //#region src/workflows/feature/criteria.ts
37909
37981
  /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
37910
37982
  const CLEAR_MIN_P = .8;
37911
- const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
37912
- const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
37913
- const ID = /^(AC-\d+):\s*(.*)$/;
37983
+ const ID$1 = /^(AC-\d+):\s*(.*)$/;
37914
37984
  /** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
37915
37985
  const listUnder = (text, title, level) => {
37916
37986
  const lines = markFenced(text);
@@ -37938,7 +38008,7 @@ const listUnder = (text, title, level) => {
37938
38008
  };
37939
38009
  /** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
37940
38010
  const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
37941
- const id = ID.exec(item);
38011
+ const id = ID$1.exec(item);
37942
38012
  return id === null ? {
37943
38013
  id: "",
37944
38014
  text: item
@@ -37947,16 +38017,20 @@ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((
37947
38017
  text: id[2]
37948
38018
  };
37949
38019
  });
38020
+ /** The ID'd criteria of `text`, only `slice`'s when given. */
38021
+ 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));
38022
+ /** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
38023
+ const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
37950
38024
  /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
37951
38025
  const numbered = (items) => {
37952
- const ids = items.map((item) => ID.exec(item));
38026
+ const ids = items.map((item) => ID$1.exec(item));
37953
38027
  const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
37954
38028
  return items.map((item, i) => own ? {
37955
38029
  id: ids[i][1],
37956
38030
  text: ids[i][2]
37957
38031
  } : {
37958
38032
  id: `AC-${i + 1}`,
37959
- text: item.replace(ID, "$2")
38033
+ text: item.replace(ID$1, "$2")
37960
38034
  });
37961
38035
  };
37962
38036
  const STATED = /^acceptance criteria:?$/i;
@@ -38026,6 +38100,13 @@ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scop
38026
38100
  });
38027
38101
  return true;
38028
38102
  });
38103
+ /** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
38104
+ const requireCriteriaIfEdited = () => {
38105
+ const change = changes().get(REQUIREMENTS);
38106
+ if (change === void 0) return;
38107
+ if (change.status === "deleted") refuse(`gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`);
38108
+ requireCriteria(REQUIREMENTS);
38109
+ };
38029
38110
  //#endregion
38030
38111
  //#region src/workflows/feature/tier.ts
38031
38112
  /** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
@@ -38374,6 +38455,53 @@ const reUnwind = async (feedback) => {
38374
38455
  requireRevert(edited, base);
38375
38456
  };
38376
38457
  //#endregion
38458
+ //#region src/workflows/shared/continuation.ts
38459
+ const phase = (name, steered) => ({
38460
+ name,
38461
+ spent: false,
38462
+ count: 0,
38463
+ stalled: 0,
38464
+ steered
38465
+ });
38466
+ const own = (fn) => fn();
38467
+ const continued = (fn) => scope("continue", fn);
38468
+ const afresh = (fn) => scope("fresh", fn);
38469
+ const setting = (name, least) => {
38470
+ const raw = vars[name] ?? defaults[name];
38471
+ const n = Number(raw);
38472
+ if (raw.trim() === "" || !Number.isInteger(n) || n < least) refuse(`gtd: ${name} must be a whole number of at least ${least}, got ${JSON.stringify(raw)}`);
38473
+ return n;
38474
+ };
38475
+ /**
38476
+ * Run `turn` until one ends without a handoff note: each stopped turn is picked
38477
+ * up by a continuation in a fresh conversation. Past `continuations` of them in
38478
+ * the phase, or after `continuationStall` in a row that close nothing still
38479
+ * open, the phase rests at `oversized` for a person; landing there grants a new round.
38480
+ */
38481
+ const continuing = async (phase, turn) => {
38482
+ const cap = setting("continuations", 0);
38483
+ const stall = setting("continuationStall", 1);
38484
+ let last = await turn(void 0, phase.spent ? afresh : own);
38485
+ while (last.handoff !== void 0) {
38486
+ phase.spent = true;
38487
+ const from = last.handoff;
38488
+ if (phase.count >= cap || phase.stalled >= stall) {
38489
+ await human("oversized", {
38490
+ message: oversizedMessage(phase.name, from, phase.count >= cap ? { cap } : { stall }, phase.steered !== void 0),
38491
+ label: "Too big for its context",
38492
+ acceptClean: true
38493
+ });
38494
+ phase.steered?.();
38495
+ phase.count = 0;
38496
+ phase.stalled = 0;
38497
+ }
38498
+ last = await turn(from, continued);
38499
+ phase.count++;
38500
+ phase.stalled = from.open.length === 0 || from.open.some((item) => !(last.handoff?.open ?? []).includes(item)) ? 0 : phase.stalled + 1;
38501
+ }
38502
+ return last.result;
38503
+ };
38504
+ //#endregion
38377
38505
  //#region src/workflows/feature/refactor.ts
38378
38506
  const REFACTOR_PACKAGE = REFACTOR;
38379
38507
  const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
@@ -38519,7 +38647,7 @@ const guardOf = ({ pattern, strays }, entry) => () => {
38519
38647
  requireNoNewFlags(pattern, entry);
38520
38648
  };
38521
38649
  /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
38522
- const refactorLap = async (p, guard, feedback) => {
38650
+ const refactorLap = async (p, guard, refactor, feedback) => {
38523
38651
  const { pattern, strays, reference, note } = p;
38524
38652
  await run("deriving", deriveScript([
38525
38653
  ...packageLines(strays, pattern, reference),
@@ -38539,32 +38667,48 @@ const refactorLap = async (p, guard, feedback) => {
38539
38667
  label: "Deriving the refactor package",
38540
38668
  file: REFACTOR_PACKAGE
38541
38669
  });
38542
- await agentWithSkills("building", refactorBuildingPrompt({
38543
- pkg: REFACTOR_PACKAGE,
38544
- strays,
38545
- pattern,
38546
- reference,
38547
- feedback: feedback !== void 0,
38548
- note: note !== void 0
38549
- }), {
38550
- label: "Refactoring",
38551
- model: coder(),
38552
- system: builderSystem()
38553
- });
38554
- guard();
38555
- await healthy(async () => {
38556
- await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
38557
- label: "Fixing the check",
38558
- file: FEEDBACK,
38670
+ const reached = /* @__PURE__ */ new Set();
38671
+ await continuing(refactor, async (from, fresh) => {
38672
+ await fresh(() => agentWithSkills("building", refactorBuildingPrompt({
38673
+ pkg: REFACTOR_PACKAGE,
38674
+ strays,
38675
+ pattern,
38676
+ reference,
38677
+ feedback: feedback !== void 0,
38678
+ note: note !== void 0,
38679
+ from
38680
+ }), {
38681
+ label: from === void 0 ? "Refactoring" : "Continuing the refactor",
38559
38682
  model: coder(),
38560
38683
  system: builderSystem()
38561
- });
38684
+ }));
38685
+ const left = read(HANDOFF);
38562
38686
  guard();
38563
- }, {
38564
- describeGuard: guard,
38565
- rules: refactorRules(strays, pattern)
38687
+ const snapshot = head$1();
38688
+ for (const c of changes()) reached.add(c.path);
38689
+ await healthy(async () => {
38690
+ await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
38691
+ label: "Fixing the check",
38692
+ file: FEEDBACK,
38693
+ model: coder(),
38694
+ system: builderSystem()
38695
+ });
38696
+ guard();
38697
+ }, {
38698
+ describeGuard: guard,
38699
+ rules: refactorRules(strays, pattern)
38700
+ });
38701
+ if (left === void 0) return { result: void 0 };
38702
+ return {
38703
+ result: void 0,
38704
+ handoff: {
38705
+ snapshot,
38706
+ note: left.trim(),
38707
+ open: strays.filter((path) => !reached.has(path))
38708
+ }
38709
+ };
38566
38710
  });
38567
- await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
38711
+ await run("closing", removeScript([REFACTOR_PACKAGE, HANDOFF]), { label: "Closing out the refactor" });
38568
38712
  };
38569
38713
  const strayAccess = (strays) => ({ write: [
38570
38714
  ...strays,
@@ -38578,7 +38722,7 @@ const refactorFirst = async (shape, options = {}) => {
38578
38722
  name: "refactor",
38579
38723
  access: strayAccess(p.strays)
38580
38724
  }, async () => {
38581
- await refactorLap(p, guardOf(p, head$1()));
38725
+ await refactorLap(p, guardOf(p, head$1()), phase("refactor"));
38582
38726
  return true;
38583
38727
  });
38584
38728
  };
@@ -38603,9 +38747,10 @@ const reviewedRefactor = async (shape, options = {}) => {
38603
38747
  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",
38604
38748
  dropped: refactorDroppedRefusal
38605
38749
  };
38750
+ const refactor = phase("refactor");
38606
38751
  let collected;
38607
38752
  for (;;) {
38608
- await refactorLap(p, guard, collected);
38753
+ await refactorLap(p, guard, refactor, collected);
38609
38754
  const outcome = await buildTail(entry, {
38610
38755
  green: true,
38611
38756
  guard,
@@ -38646,10 +38791,12 @@ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", imple
38646
38791
  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");
38647
38792
  /**
38648
38793
  * The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
38649
- * over — no passing test since `base` covers, by the judge.
38794
+ * over, only `slice`'s when given — no passing test since `base` covers, by the judge.
38650
38795
  */
38651
- const uncoveredSince = async (base) => {
38652
- const criteria = (criteriaOf(read(".gtd/REQUIREMENTS.md") ?? "") ?? []).filter((c) => c.id !== "");
38796
+ const uncoveredSince = async (base, slice) => {
38797
+ const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
38798
+ const own = criterionIds(requirements, slice);
38799
+ const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
38653
38800
  if (criteria.length === 0) return {
38654
38801
  covered: [],
38655
38802
  uncovered: []
@@ -38676,14 +38823,14 @@ const record = (covered, uncovered = []) => {
38676
38823
  * sending uncovered IDs back to a covering turn; marks the IDs covered on the
38677
38824
  * landing that ends it. A human may overrule once the retries are spent.
38678
38825
  */
38679
- const requireCoverage = async (base = start()) => {
38826
+ const requireCoverage = async (base = start(), slice) => {
38680
38827
  let retries = 0;
38681
38828
  for (;;) {
38682
- const { covered, uncovered } = await uncoveredSince(base);
38829
+ const { covered, uncovered } = await uncoveredSince(base, slice);
38683
38830
  if (uncovered.length === 0) return record(covered);
38684
38831
  if (retries < COVERING_RETRIES) {
38685
38832
  retries++;
38686
- await agentWithSkills("covering", coveringPrompt(uncovered), {
38833
+ await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
38687
38834
  label: "Covering the acceptance criteria",
38688
38835
  model: coder(),
38689
38836
  system: builderSystem(),
@@ -38714,12 +38861,32 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
38714
38861
  allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
38715
38862
  exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
38716
38863
  });
38717
- const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
38718
- label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
38864
+ const naivePass = (retry, slice, from) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice), from), {
38865
+ label: from !== void 0 ? "Continuing the naive pass" : retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
38719
38866
  keep: true,
38720
38867
  model: coder(),
38721
38868
  system: builderSystem()
38722
38869
  });
38870
+ const handoffOf = (tip, slice) => {
38871
+ const note = tip.changes.find((c) => c.path === ".gtd/HANDOFF.md" && c.after !== void 0);
38872
+ if (note === void 0) return void 0;
38873
+ return {
38874
+ snapshot: tip.hash,
38875
+ note: note.after.trim(),
38876
+ open: unnamed(idsOf(REQUIREMENTS, slice), acceptanceTests(tip.changes))
38877
+ };
38878
+ };
38879
+ /** A naive pass, continued until a pass ends without a handoff note: the snapshot is its kept tip. */
38880
+ const naiveContinued = (retry, slice, naive) => continuing(naive, async (from, fresh) => {
38881
+ const tip = await fresh(async () => {
38882
+ await naivePass(retry, slice, from);
38883
+ return kept("naive");
38884
+ });
38885
+ return {
38886
+ result: tip,
38887
+ handoff: handoffOf(tip, slice)
38888
+ };
38889
+ });
38723
38890
  const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
38724
38891
  const EXTENSION = /\.[\w-]+$/;
38725
38892
  const ALIAS = /^(?:[@~]\/|#)/;
@@ -38755,7 +38922,7 @@ const addedLines = (change) => {
38755
38922
  return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
38756
38923
  };
38757
38924
  const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
38758
- const idsOf = (file) => (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
38925
+ const idsOf = (file, slice) => criterionIds(read(file) ?? "", slice);
38759
38926
  const unnamed = (ids, tests) => {
38760
38927
  const lines = tests.map(addedLines);
38761
38928
  return ids.filter((id) => !lines.some((text) => named(id, text)));
@@ -38765,9 +38932,9 @@ const unnamed = (ids, tests) => {
38765
38932
  * test line of the pass names, or a test importing a file the pass added —
38766
38933
  * naive structure a reset and refactor replaces, which the tests must survive.
38767
38934
  */
38768
- const acceptanceFault = (file, tip) => {
38935
+ const acceptanceFault = (file, tip, slice) => {
38769
38936
  const tests = acceptanceTests(tip.changes);
38770
- const untested = unnamed(idsOf(file), tests);
38937
+ const untested = unnamed(idsOf(file, slice), tests);
38771
38938
  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]}] …"`;
38772
38939
  const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
38773
38940
  const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
@@ -38777,8 +38944,8 @@ const acceptanceFault = (file, tip) => {
38777
38944
  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")}`;
38778
38945
  };
38779
38946
  /** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
38780
- const naiveAcceptance = (tip) => {
38781
- const ids = idsOf(REQUIREMENTS);
38947
+ const naiveAcceptance = (tip, slice) => {
38948
+ const ids = idsOf(REQUIREMENTS, slice);
38782
38949
  const perTest = acceptanceTests(tip.changes).map((c) => {
38783
38950
  const lines = addedLines(c);
38784
38951
  return {
@@ -38847,6 +39014,32 @@ const applied = async (tip) => {
38847
39014
  await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
38848
39015
  return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
38849
39016
  };
39017
+ /** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
39018
+ const settled = (carried, slice) => {
39019
+ const listed = idsOf(REQUIREMENTS, slice);
39020
+ if (carried === void 0) return listed;
39021
+ return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id));
39022
+ };
39023
+ const build = (options) => continuing(phase("build", requireCriteriaIfEdited), async (from, fresh) => {
39024
+ await fresh(() => agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice), from), {
39025
+ label: from === void 0 ? "Implementing" : "Continuing the implementation",
39026
+ model: coder(),
39027
+ system: builderSystem(),
39028
+ ...options.afterRefactor === true ? { allowEmpty: true } : {}
39029
+ }));
39030
+ requireCriteriaFrozen(REQUIREMENTS);
39031
+ const note = read(HANDOFF);
39032
+ if (note === void 0) return { result: void 0 };
39033
+ const ids = settled(options.rebuild?.acceptance?.ids, options.slice);
39034
+ return {
39035
+ result: void 0,
39036
+ handoff: {
39037
+ snapshot: head$1(),
39038
+ note: note.trim(),
39039
+ open: unnamed(ids, acceptanceTests(changesSince(start())))
39040
+ }
39041
+ };
39042
+ });
38850
39043
  function implement(first, tier, amend) {
38851
39044
  const options = typeof first === "object" && first !== null ? first : {
38852
39045
  reference: first,
@@ -38857,19 +39050,26 @@ function implement(first, tier, amend) {
38857
39050
  const since = head$1();
38858
39051
  const acceptance = options.rebuild?.acceptance;
38859
39052
  const built = options.naive === void 0 || !await applied(options.naive);
38860
- if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild), {
38861
- label: "Implementing",
38862
- model: coder(),
38863
- system: builderSystem(),
38864
- ...options.afterRefactor === true ? { allowEmpty: true } : {}
38865
- });
38866
- requireCriteriaFrozen(REQUIREMENTS);
38867
- if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
39053
+ if (built) await build(options);
39054
+ else requireCriteriaFrozen(REQUIREMENTS);
39055
+ if (built && acceptance !== void 0) requireAcceptance({
39056
+ ...acceptance,
39057
+ ids: settled(acceptance.ids, options.slice)
39058
+ }, since);
38868
39059
  await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
38869
- await requireCoverage();
38870
- await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), { label: "Closing out the implementation" });
39060
+ await requireCoverage(options.base, options.slice);
39061
+ await closing(options.more === true);
38871
39062
  });
38872
39063
  }
39064
+ /** The last slice's closing, for a last slice a steer left with no criterion to build. */
39065
+ const closeOut = () => scope("implement", () => closing(false));
39066
+ /** A slice with `more` after it keeps the criteria and the slicing for the next one. */
39067
+ const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE, HANDOFF] : [
39068
+ REQUIREMENTS,
39069
+ REFACTOR_NOTE,
39070
+ SLICES,
39071
+ HANDOFF
39072
+ ]), { label: "Closing out the implementation" });
38873
39073
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
38874
39074
  const steer = async (retry) => {
38875
39075
  await human("steer", {
@@ -38900,8 +39100,8 @@ const budget = () => {
38900
39100
  };
38901
39101
  };
38902
39102
  /** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
38903
- const vetted = async (tip, vetting) => {
38904
- const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
39103
+ const vetted = async (tip, vetting, slice) => {
39104
+ const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip, slice);
38905
39105
  if (fault !== void 0) {
38906
39106
  const verdict = await vetting.again({
38907
39107
  fault,
@@ -38935,16 +39135,16 @@ const ruled = async (tip, again) => {
38935
39135
  tip
38936
39136
  } : next;
38937
39137
  };
38938
- const decide = () => scope("seam", async () => {
39138
+ const decide = (slice) => scope("seam", async () => {
38939
39139
  const vetting = {
38940
39140
  again: budget(),
38941
39141
  waived: false
38942
39142
  };
39143
+ const naive = phase("naive pass", requireCriteriaIfEdited);
38943
39144
  let retry;
38944
39145
  for (;;) {
38945
- await naivePass(retry);
38946
- const tip = kept("naive");
38947
- const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
39146
+ const tip = await naiveContinued(retry, slice, naive);
39147
+ const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
38948
39148
  reference: tip.hash,
38949
39149
  tip
38950
39150
  });
@@ -38965,8 +39165,8 @@ const refactorNote = async (reference, strays) => {
38965
39165
  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")}` : ""}`);
38966
39166
  return note;
38967
39167
  };
38968
- const seamBuild = async (base) => {
38969
- const { reference, tip, shape } = await decide();
39168
+ const seamBuild = async (base, slice$1 = {}) => {
39169
+ const { reference, tip, shape } = await decide(slice$1.criteria);
38970
39170
  if (shape === void 0) return {
38971
39171
  reference,
38972
39172
  base,
@@ -38981,17 +39181,17 @@ const seamBuild = async (base) => {
38981
39181
  return {
38982
39182
  reference,
38983
39183
  base,
38984
- rebuild: { acceptance: naiveAcceptance(tip) }
39184
+ rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
38985
39185
  };
38986
39186
  }
38987
39187
  const strays = refactorStrays(shape);
38988
39188
  if (strays.length === 0) return {
38989
39189
  reference,
38990
39190
  base,
38991
- rebuild: { acceptance: naiveAcceptance(tip) }
39191
+ rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
38992
39192
  };
38993
39193
  const note = await scope("seam", () => refactorNote(reference, strays));
38994
- const refactored = await slice("refactor", () => reviewedRefactor(shape, {
39194
+ const refactored = await slice(slice$1.refactor ?? "refactor", () => reviewedRefactor(shape, {
38995
39195
  reference,
38996
39196
  note
38997
39197
  }));
@@ -39001,34 +39201,143 @@ const seamBuild = async (base) => {
39001
39201
  refactored,
39002
39202
  rebuild: {
39003
39203
  note,
39004
- acceptance: naiveAcceptance(tip)
39204
+ acceptance: naiveAcceptance(tip, slice$1.criteria)
39005
39205
  }
39006
39206
  };
39007
39207
  };
39008
39208
  //#endregion
39209
+ //#region src/workflows/feature/slicing.ts
39210
+ const ID = /^(AC-\d+)(?![\w-])/;
39211
+ /** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
39212
+ const sliceCap = () => {
39213
+ const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria;
39214
+ if (!/^[1-9]\d*$/.test(value)) refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`);
39215
+ return Number(value);
39216
+ };
39217
+ /** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
39218
+ const slicesOf = (text) => {
39219
+ const slices = [];
39220
+ let open = false;
39221
+ for (const { line, fenced } of markFenced(text)) {
39222
+ if (fenced) continue;
39223
+ const heading = HEADING.exec(line);
39224
+ if (heading !== null) {
39225
+ open = heading[1] === "##";
39226
+ if (open) slices.push({
39227
+ title: heading[2],
39228
+ ids: []
39229
+ });
39230
+ continue;
39231
+ }
39232
+ const item = ITEM.exec(line);
39233
+ if (item !== null && open) slices.at(-1).ids.push(ID.exec(item[1])?.[1] ?? "");
39234
+ }
39235
+ return slices;
39236
+ };
39237
+ const twice = (list) => [...new Set(list.filter((id, i) => list.indexOf(id) !== i))];
39238
+ /** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
39239
+ const sliceFault = (slices, ids, cap) => {
39240
+ const listed = slices.flatMap((s) => s.ids);
39241
+ if (slices.length === 0) return `no "## " slice`;
39242
+ const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title);
39243
+ if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`;
39244
+ if (listed.includes("")) return "a list item not led by a criterion ID";
39245
+ const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title);
39246
+ if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`;
39247
+ const repeated = twice(listed);
39248
+ if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`;
39249
+ const unknown = listed.filter((id) => !ids.includes(id));
39250
+ if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`;
39251
+ const missing = ids.filter((id) => !listed.includes(id));
39252
+ if (missing.length > 0) return `no slice for: ${missing.join(", ")}`;
39253
+ };
39254
+ /** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
39255
+ const requireSlices = (ids, cap) => {
39256
+ const slices = slicesOf(read(".gtd/SLICES.md") ?? "");
39257
+ const why = sliceFault(slices, ids, cap);
39258
+ 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`);
39259
+ return slices;
39260
+ };
39261
+ /**
39262
+ * The slices to build, in order. Criteria within `sliceMaxCriteria` are one
39263
+ * slice and no turn; above it `design.slicing` groups them.
39264
+ */
39265
+ const slicing = async () => {
39266
+ const all = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
39267
+ const cap = sliceCap();
39268
+ if (all.length <= cap) return [{
39269
+ title: "",
39270
+ ids: all
39271
+ }];
39272
+ return scope("design", async () => {
39273
+ await agentWithSkills("slicing", slicingPrompt(cap), {
39274
+ label: "Slicing the change",
39275
+ file: SLICES,
39276
+ model: planner(),
39277
+ system: designSystem()
39278
+ });
39279
+ return requireSlices(all, cap);
39280
+ });
39281
+ };
39282
+ /** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
39283
+ 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) };
39284
+ //#endregion
39009
39285
  //#region src/workflows/feature/feature.ts
39010
39286
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
39011
39287
  const unwind = () => {
39012
39288
  const commit = head$1();
39013
39289
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
39014
39290
  };
39015
- /** A naive pass (judged only with `flagMode` on), then implement and the quality lap; design must have run since `firstBase`. */
39291
+ /**
39292
+ * The slices `design.slicing` grouped the criteria into, each built in turn
39293
+ * from where the previous one landed; design must have run since `firstBase`.
39294
+ */
39016
39295
  const buildFeature = async (firstBase) => {
39017
39296
  const tier = riskTierOn() ? recordRisk() : void 0;
39018
- const seam = await seamBuild(firstBase);
39019
- await slice("feature", async () => {
39297
+ const slices = await slicing();
39298
+ const one = slices.length === 1;
39299
+ for (const i of slices.keys()) {
39300
+ const more = i < slices.length - 1;
39301
+ const criteria = sliceCriteria(slices, i);
39302
+ if (sliceIds(criteria)?.length === 0) {
39303
+ if (!more) await closeOut();
39304
+ continue;
39305
+ }
39306
+ const n = one ? "" : `-${i + 1}`;
39307
+ await buildSlice(i === 0 ? firstBase : head$1(), {
39308
+ tier,
39309
+ criteria,
39310
+ refactor: `refactor${n}`,
39311
+ feature: `feature${n}`,
39312
+ more
39313
+ });
39314
+ }
39315
+ };
39316
+ /** A naive pass (judged only with `flagMode` on), then implement and the quality lap. */
39317
+ const buildSlice = async (sliceBase, build) => {
39318
+ const seam = await seamBuild(sliceBase, {
39319
+ criteria: build.criteria,
39320
+ refactor: build.refactor
39321
+ });
39322
+ await slice(build.feature, async () => {
39020
39323
  await implement({
39021
39324
  reference: seam.reference,
39022
- tier,
39325
+ tier: build.tier,
39023
39326
  afterRefactor: seam.refactored,
39024
39327
  naive: seam.land === true ? seam.reference : void 0,
39025
- rebuild: seam.rebuild
39328
+ rebuild: seam.rebuild,
39329
+ slice: build.criteria,
39330
+ base: build.criteria === void 0 ? void 0 : sliceBase,
39331
+ more: build.more
39026
39332
  });
39027
39333
  const carried = seam.rebuild?.acceptance?.tests ?? [];
39028
- const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
39334
+ const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? sliceBase : seam.base;
39335
+ const guard = build.more ? () => requireCriteriaFrozen(REQUIREMENTS) : void 0;
39029
39336
  await tail(settleQuality, {
39030
39337
  green: true,
39031
- lensBase: base
39338
+ lensBase: base,
39339
+ guard,
39340
+ guardRests: false
39032
39341
  });
39033
39342
  });
39034
39343
  };
@@ -39074,6 +39383,7 @@ const ordinaryStart = async () => {
39074
39383
  });
39075
39384
  refusePendingHandOff();
39076
39385
  flagModeOn();
39386
+ sliceCap();
39077
39387
  const stated = statedCriteria(changes(), head$1());
39078
39388
  await unwind();
39079
39389
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
@@ -39268,4 +39578,4 @@ var workflows_default = feature;
39268
39578
  */
39269
39579
  const doors = {};
39270
39580
  //#endregion
39271
- export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
39581
+ export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unwind };