@pmelab/gtd 37.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "37.0.0",
3
+ "version": "38.0.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -638,6 +638,15 @@ out is `gtd abandon`. A process from 27.5.0 or later whose judge ruled
638
638
  that slice. Details in
639
639
  [the naive-pass path](docs/configuration.md#the-naive-pass-path).
640
640
 
641
+ **Coarse slices (`sliceMaxCriteria`, default 8).** A change with more acceptance
642
+ criteria than the cap is built in slices: after design an agent groups related
643
+ criteria (same flow or subsystem) into `.gtd/SLICES.md`, as few slices as the
644
+ cap allows, and each slice — naive pass, refactor and rebuild when judged so,
645
+ health loop, quality lap — runs in order from where the previous one landed.
646
+ `gtd slices` lists them as `feature-1`, `feature-2`, … (`refactor-<n>` ahead of
647
+ a refactored one). A change within the cap is one `feature` slice, as before.
648
+ See [Coarse slices](docs/configuration.md#coarse-slices).
649
+
641
650
  **Flag mode (`flagMode`, on by default).** On, the naive pass's judge runs as
642
651
  above and a rebuilt implementation is asked for a new release flag (none at a
643
652
  `low` risk tier); a pass the judge lets land is not asked for one. A project
@@ -133,6 +133,7 @@ const defaults = {
133
133
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
134
134
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
135
135
  flagMode: "true",
136
+ sliceMaxCriteria: "8",
136
137
  flag_risk_tier: "0",
137
138
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
138
139
  refactorQualityReviews: "correctness, ponytail-review, conventions"
@@ -592,6 +593,24 @@ printf '%s\\n' ${[
592
593
  ].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
593
594
  `;
594
595
  };
596
+ const slicingPrompt = (cap) => `${stateFileRules}
597
+ - Read \`.gtd/REQUIREMENTS.md\`: its \`## Acceptance Criteria\` are settled
598
+ and more than ${cap}, too many to build in one slice
599
+ - Group them into slices in \`.gtd/SLICES.md\`, one \`## <title>\` heading per
600
+ slice over a \`- AC-<n>\` list item per criterion it builds. Put criteria of
601
+ the same flow or subsystem together; every criterion in exactly one slice,
602
+ at most ${cap} per slice
603
+ - Keep it coarse: as few slices as the cap allows, never one per criterion
604
+ - Order the slices so each builds on the ones before: they run in that order,
605
+ each from where the previous one landed
606
+ - Change nothing but \`.gtd/SLICES.md\` — leave the criteria as they are
607
+ - Leave everything uncommitted and finish your turn — do not commit
608
+ `;
609
+ /** The line telling a slice's turn which criteria are its own; empty for a single slice. */
610
+ const sliceLine = (ids) => ids === void 0 ? "" : `- This slice builds ${ids.join(", ")} only: every rule here about the criteria
611
+ means these. The others are for other slices, before or after it — leave
612
+ them unbuilt and untested
613
+ `;
595
614
  const designSystem = () => `${designPersona}
596
615
 
597
616
  ${agentConduct}`;
@@ -1238,9 +1257,9 @@ re-implement. It changed these files outside the expected set:
1238
1257
  ${listed(retry.strays)}
1239
1258
  Start over and choose a different approach; do not rebuild the same diff.
1240
1259
  `;
1241
- const naivePassPrompt = (retry) => `${stateFileRules}
1260
+ const naivePassPrompt = (retry, slice) => `${stateFileRules}
1242
1261
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1243
- ${criteriaBinding(false)}
1262
+ ${sliceLine(slice)}${criteriaBinding(false)}
1244
1263
  - Implement them the quickest way that works — the naive version. Do not
1245
1264
  restructure, abstract or refactor around the change; it may land exactly as
1246
1265
  you leave it, and a reviewer makes it fit
@@ -1290,9 +1309,13 @@ const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed an
1290
1309
  though never a release flag. Build only what is still missing, held to every
1291
1310
  rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
1292
1311
  `;
1293
- const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}) => `${stateFileRules}
1312
+ 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
1313
+ change added; only when none did, behind a new one, following the
1314
+ repository's flag conventions` : `- Put the new behaviour behind a new release flag, following the
1315
+ repository's flag conventions`;
1316
+ const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}, slice) => `${stateFileRules}
1294
1317
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1295
- ${criteriaBinding(true)}
1318
+ ${sliceLine(slice)}${criteriaBinding(true)}
1296
1319
  ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
1297
1320
  the re-derived requirements instead of rewriting it
1298
1321
  ` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
@@ -1301,8 +1324,7 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `-
1301
1324
  `}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
1302
1325
  Read it for ideas; never apply, cherry-pick or check it out — write
1303
1326
  the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
1304
- `}${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
1305
- repository's flag conventions`}
1327
+ `}${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend, slice !== void 0)}
1306
1328
  - Write the tests first, then the code that makes them pass. Name each
1307
1329
  criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
1308
1330
  green a judge checks every ID has a passing test, and sends uncovered ones back
@@ -1383,9 +1405,9 @@ const coverageMessage = () => `Judging acceptance-criteria coverage. Run \`gtd j
1383
1405
  \`yes\`/\`no\` verdict per criterion ID — or land without a verdict, which
1384
1406
  counts every criterion as uncovered.
1385
1407
  `;
1386
- const coveringPrompt = (uncovered) => `${stateFileRules}
1408
+ const coveringPrompt = (uncovered, slice) => `${stateFileRules}
1387
1409
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1388
- ${criteriaBinding(true)}
1410
+ ${sliceLine(slice)}${criteriaBinding(true)}
1389
1411
  - A judge found no passing test in this change that covers ${uncovered.join(", ")}.
1390
1412
  Write a behaviour-level test for each, naming its ID in the test or scenario
1391
1413
  name (\`[${uncovered[0]}] …\`), and fix the code if the test shows it falls short
@@ -1412,6 +1434,7 @@ const REVIEW = ".gtd/REVIEW.md";
1412
1434
  const REFACTOR = ".gtd/REFACTOR.md";
1413
1435
  const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
1414
1436
  const QUALITY = ".gtd/QUALITY.md";
1437
+ const SLICES = ".gtd/SLICES.md";
1415
1438
  const planner = () => env.plannerModel ?? "";
1416
1439
  const coder = () => env.coderModel ?? "";
1417
1440
  const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
@@ -1605,6 +1628,7 @@ const ShippedOptions = Struct({
1605
1628
  reviewSeverityMinP: setting,
1606
1629
  refactorFlagPattern: setting,
1607
1630
  seamAllow: setting,
1631
+ sliceMaxCriteria: setting,
1608
1632
  qualityReviews: setting,
1609
1633
  refactorQualityReviews: setting,
1610
1634
  testCommand: setting,
@@ -1710,4 +1734,4 @@ const steering = {
1710
1734
  [REVIEW]: "review"
1711
1735
  };
1712
1736
  //#endregion
1713
- export { normaliseCriteriaScript as $, buildReviewAwaitAfterDeferMessage as A, criterionLine as B, fixRisks as C, registryEntry as Ct, reviewing as D, reviewQuality as E, clearCriteriaMessage as F, finisherSystem as G, designGateAnswerMessage as H, clearCriteriaQuestion as I, idleMessage as J, fixReviewNotesPrompt as K, coverageMessage as L, buildReviewFixNitsPrompt as M, buildReviewTriageMessage as N, agentWithSkills as O, builderSystem as P, naivePassPrompt as Q, coverageQuestion as R, fixQuality as S, flipProblems as St, reviewMissing as T, designSystem as U, deferNotesScript as V, designTriagePrompt as W, implementFixPrompt as X, implementBuildingPrompt as Y, naiveApplyFailure as Z, REQUIREMENTS as _, defaults as _t, flagModeOn as a, refactorRules as at, coder as b, flagMentions as bt, baseline as c, reviewGateBlockedMessage as ct, guarded as d, seamByHandMessage as dt, qualityUnresolvedMessage as et, healthy as f, seamSteerMessage as ft, REFACTOR_NOTE as g, unwindFailure as gt, REFACTOR as h, unwindFailedMessage as ht, skills as i, refactorNotePrompt as it, buildReviewCollectingPrompt as j, buildReviewAnswerQuestionsPrompt as k, escalation as l, reviewNotesCapture as lt, QUALITY as m, uncoveredMessage as mt, summary as n, refactorDroppedRefusal as nt, flagShipped as o, reviewEditNotesCapture as ot, FEEDBACK as p, startGateBlockedMessage as pt, handOffAwaitReviewMessage as q, access as r, refactorFixPrompt as rt, shipped as s, reviewEditsCapture as st, steering as t, refactorBuildingPrompt as tt, gate as u, reviewerSystem as ut, REVIEW as v, envDefaults as vt, planner as w, requireFlag as wt, fixCheck as x, flagReferences as xt, awaitReview as y, testFileGlobs as yt, coveringPrompt as z };
1737
+ export { naivePassPrompt as $, buildReviewAnswerQuestionsPrompt as A, coveringPrompt as B, fixQuality as C, flagReferences as Ct, reviewQuality as D, reviewMissing as E, requireFlag as Et, builderSystem as F, designTriagePrompt as G, deferNotesScript as H, clearCriteriaMessage as I, handOffAwaitReviewMessage as J, finisherSystem as K, clearCriteriaQuestion as L, buildReviewCollectingPrompt as M, buildReviewFixNitsPrompt as N, reviewing as O, buildReviewTriageMessage as P, naiveApplyFailure as Q, coverageMessage as R, fixCheck as S, flagMentions as St, planner as T, registryEntry as Tt, designGateAnswerMessage as U, criterionLine as V, designSystem as W, implementBuildingPrompt as X, idleMessage as Y, implementFixPrompt as Z, REQUIREMENTS as _, unwindFailedMessage as _t, flagModeOn as a, refactorNotePrompt as at, awaitReview as b, envDefaults as bt, baseline as c, reviewEditsCapture as ct, guarded as d, reviewerSystem as dt, normaliseCriteriaScript as et, healthy as f, seamByHandMessage as ft, REFACTOR_NOTE as g, uncoveredMessage as gt, REFACTOR as h, startGateBlockedMessage as ht, skills as i, refactorFixPrompt as it, buildReviewAwaitAfterDeferMessage as j, agentWithSkills as k, escalation as l, reviewGateBlockedMessage as lt, QUALITY as m, slicingPrompt as mt, summary as n, refactorBuildingPrompt as nt, flagShipped as o, refactorRules as ot, FEEDBACK as p, seamSteerMessage as pt, fixReviewNotesPrompt as q, access as r, refactorDroppedRefusal as rt, shipped as s, reviewEditNotesCapture as st, steering as t, qualityUnresolvedMessage as tt, gate as u, reviewNotesCapture as ut, REVIEW as v, unwindFailure as vt, fixRisks as w, flipProblems as wt, coder as x, testFileGlobs as xt, SLICES as y, defaults as yt, coverageQuestion as z };
@@ -1,6 +1,6 @@
1
1
  import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
2
2
  import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { C as fixRisks, D as reviewing, E as reviewQuality, S as fixQuality, T as reviewMissing, d as guarded, f as healthy, l as escalation, m as QUALITY, q as handOffAwaitReviewMessage, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-zAfoJtR7.mjs";
3
+ import { C as fixQuality, D as reviewQuality, E as reviewMissing, J as handOffAwaitReviewMessage, O as reviewing, S as fixCheck, b as awaitReview, d as guarded, f as healthy, l as escalation, m as QUALITY, v as REVIEW, w as fixRisks } from "./reserved-sfMdy7v7.mjs";
4
4
  //#region src/workflows/shared/tail.ts
5
5
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
6
6
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { Ct as registryEntry, _t as defaults, b as coder, i as skills, n as summary, o as flagShipped, r as access, t as steering, vt as envDefaults, wt as requireFlag, xt as flagReferences } from "../chunks/reserved-zAfoJtR7.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-BRC4DGdV.mjs";
2
+ import { Ct as flagReferences, Et as requireFlag, Tt as registryEntry, bt as envDefaults, i as skills, n as summary, o as flagShipped, r as access, t as steering, x as coder, 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/cleanup/cleanup.ts
5
5
  const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
6
6
  const cleanupFlow = async () => {
@@ -1,7 +1,7 @@
1
1
  import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
2
2
  import { lensEntriesOf, review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as normaliseCriteriaScript, A as buildReviewAwaitAfterDeferMessage, B as criterionLine, C as fixRisks, D as reviewing, F as clearCriteriaMessage, H as designGateAnswerMessage, I as clearCriteriaQuestion, J as idleMessage, L as coverageMessage, M as buildReviewFixNitsPrompt, N as buildReviewTriageMessage, O as agentWithSkills, P as builderSystem, Q as naivePassPrompt, R as coverageQuestion, T as reviewMissing, U as designSystem, V as deferNotesScript, W as designTriagePrompt, X as implementFixPrompt, Y as implementBuildingPrompt, Z as naiveApplyFailure, _ as REQUIREMENTS, _t as defaults, a as flagModeOn, at as refactorRules, b as coder, d as guarded, dt as seamByHandMessage, et as qualityUnresolvedMessage, f as healthy, ft as seamSteerMessage, g as REFACTOR_NOTE, gt as unwindFailure, h as REFACTOR, ht as unwindFailedMessage, i as skills, it as refactorNotePrompt, j as buildReviewCollectingPrompt, k as buildReviewAnswerQuestionsPrompt, lt as reviewNotesCapture, m as QUALITY, mt as uncoveredMessage, n as summary, nt as refactorDroppedRefusal, ot as reviewEditNotesCapture, p as FEEDBACK, pt as startGateBlockedMessage, r as access, rt as refactorFixPrompt, s as shipped, st as reviewEditsCapture, t as steering, tt as refactorBuildingPrompt, u as gate, ut as reviewerSystem, v as REVIEW, vt as envDefaults, w as planner, x as fixCheck, y as awaitReview, yt as testFileGlobs, z as coveringPrompt } from "../chunks/reserved-zAfoJtR7.mjs";
4
- import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-BRC4DGdV.mjs";
3
+ import { $ as 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;
@@ -759,10 +765,12 @@ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", imple
759
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");
760
766
  /**
761
767
  * The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
762
- * 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.
763
769
  */
764
- const uncoveredSince = async (base) => {
765
- 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));
766
774
  if (criteria.length === 0) return {
767
775
  covered: [],
768
776
  uncovered: []
@@ -789,14 +797,14 @@ const record = (covered, uncovered = []) => {
789
797
  * sending uncovered IDs back to a covering turn; marks the IDs covered on the
790
798
  * landing that ends it. A human may overrule once the retries are spent.
791
799
  */
792
- const requireCoverage = async (base = start()) => {
800
+ const requireCoverage = async (base = start(), slice) => {
793
801
  let retries = 0;
794
802
  for (;;) {
795
- const { covered, uncovered } = await uncoveredSince(base);
803
+ const { covered, uncovered } = await uncoveredSince(base, slice);
796
804
  if (uncovered.length === 0) return record(covered);
797
805
  if (retries < COVERING_RETRIES) {
798
806
  retries++;
799
- await agentWithSkills("covering", coveringPrompt(uncovered), {
807
+ await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
800
808
  label: "Covering the acceptance criteria",
801
809
  model: coder(),
802
810
  system: builderSystem(),
@@ -827,7 +835,7 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
827
835
  allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
828
836
  exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
829
837
  });
830
- const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
838
+ const naivePass = (retry, slice) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice)), {
831
839
  label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
832
840
  keep: true,
833
841
  model: coder(),
@@ -868,7 +876,7 @@ const addedLines = (change) => {
868
876
  return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
869
877
  };
870
878
  const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
871
- const idsOf = (file) => (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
879
+ const idsOf = (file, slice) => criterionIds(read(file) ?? "", slice);
872
880
  const unnamed = (ids, tests) => {
873
881
  const lines = tests.map(addedLines);
874
882
  return ids.filter((id) => !lines.some((text) => named(id, text)));
@@ -878,9 +886,9 @@ const unnamed = (ids, tests) => {
878
886
  * test line of the pass names, or a test importing a file the pass added —
879
887
  * naive structure a reset and refactor replaces, which the tests must survive.
880
888
  */
881
- const acceptanceFault = (file, tip) => {
889
+ const acceptanceFault = (file, tip, slice) => {
882
890
  const tests = acceptanceTests(tip.changes);
883
- const untested = unnamed(idsOf(file), tests);
891
+ const untested = unnamed(idsOf(file, slice), tests);
884
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]}] …"`;
885
893
  const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
886
894
  const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
@@ -890,8 +898,8 @@ const acceptanceFault = (file, tip) => {
890
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")}`;
891
899
  };
892
900
  /** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
893
- const naiveAcceptance = (tip) => {
894
- const ids = idsOf(REQUIREMENTS);
901
+ const naiveAcceptance = (tip, slice) => {
902
+ const ids = idsOf(REQUIREMENTS, slice);
895
903
  const perTest = acceptanceTests(tip.changes).map((c) => {
896
904
  const lines = addedLines(c);
897
905
  return {
@@ -970,7 +978,7 @@ function implement(first, tier, amend) {
970
978
  const since = head();
971
979
  const acceptance = options.rebuild?.acceptance;
972
980
  const built = options.naive === void 0 || !await applied(options.naive);
973
- if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild), {
981
+ if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice)), {
974
982
  label: "Implementing",
975
983
  model: coder(),
976
984
  system: builderSystem(),
@@ -979,10 +987,18 @@ function implement(first, tier, amend) {
979
987
  requireCriteriaFrozen(REQUIREMENTS);
980
988
  if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
981
989
  await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
982
- await requireCoverage();
983
- await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), { label: "Closing out the implementation" });
990
+ await requireCoverage(options.base, options.slice);
991
+ await closing(options.more === true);
984
992
  });
985
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" });
986
1002
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
987
1003
  const steer = async (retry) => {
988
1004
  await human("steer", {
@@ -1013,8 +1029,8 @@ const budget = () => {
1013
1029
  };
1014
1030
  };
1015
1031
  /** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
1016
- const vetted = async (tip, vetting) => {
1017
- const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
1032
+ const vetted = async (tip, vetting, slice) => {
1033
+ const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip, slice);
1018
1034
  if (fault !== void 0) {
1019
1035
  const verdict = await vetting.again({
1020
1036
  fault,
@@ -1048,16 +1064,16 @@ const ruled = async (tip, again) => {
1048
1064
  tip
1049
1065
  } : next;
1050
1066
  };
1051
- const decide = () => scope("seam", async () => {
1067
+ const decide = (slice) => scope("seam", async () => {
1052
1068
  const vetting = {
1053
1069
  again: budget(),
1054
1070
  waived: false
1055
1071
  };
1056
1072
  let retry;
1057
1073
  for (;;) {
1058
- await naivePass(retry);
1074
+ await naivePass(retry, slice);
1059
1075
  const tip = kept("naive");
1060
- const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
1076
+ const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
1061
1077
  reference: tip.hash,
1062
1078
  tip
1063
1079
  });
@@ -1078,8 +1094,8 @@ const refactorNote = async (reference, strays) => {
1078
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")}` : ""}`);
1079
1095
  return note;
1080
1096
  };
1081
- const seamBuild = async (base) => {
1082
- const { reference, tip, shape } = await decide();
1097
+ const seamBuild = async (base, slice$1 = {}) => {
1098
+ const { reference, tip, shape } = await decide(slice$1.criteria);
1083
1099
  if (shape === void 0) return {
1084
1100
  reference,
1085
1101
  base,
@@ -1094,17 +1110,17 @@ const seamBuild = async (base) => {
1094
1110
  return {
1095
1111
  reference,
1096
1112
  base,
1097
- rebuild: { acceptance: naiveAcceptance(tip) }
1113
+ rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
1098
1114
  };
1099
1115
  }
1100
1116
  const strays = refactorStrays(shape);
1101
1117
  if (strays.length === 0) return {
1102
1118
  reference,
1103
1119
  base,
1104
- rebuild: { acceptance: naiveAcceptance(tip) }
1120
+ rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
1105
1121
  };
1106
1122
  const note = await scope("seam", () => refactorNote(reference, strays));
1107
- const refactored = await slice("refactor", () => reviewedRefactor(shape, {
1123
+ const refactored = await slice(slice$1.refactor ?? "refactor", () => reviewedRefactor(shape, {
1108
1124
  reference,
1109
1125
  note
1110
1126
  }));
@@ -1114,34 +1130,143 @@ const seamBuild = async (base) => {
1114
1130
  refactored,
1115
1131
  rebuild: {
1116
1132
  note,
1117
- acceptance: naiveAcceptance(tip)
1133
+ acceptance: naiveAcceptance(tip, slice$1.criteria)
1118
1134
  }
1119
1135
  };
1120
1136
  };
1121
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
1122
1214
  //#region src/workflows/feature/feature.ts
1123
1215
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
1124
1216
  const unwind = () => {
1125
1217
  const commit = head();
1126
1218
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
1127
1219
  };
1128
- /** 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
+ */
1129
1224
  const buildFeature = async (firstBase) => {
1130
1225
  const tier = riskTierOn() ? recordRisk() : void 0;
1131
- const seam = await seamBuild(firstBase);
1132
- 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 () => {
1133
1252
  await implement({
1134
1253
  reference: seam.reference,
1135
- tier,
1254
+ tier: build.tier,
1136
1255
  afterRefactor: seam.refactored,
1137
1256
  naive: seam.land === true ? seam.reference : void 0,
1138
- rebuild: seam.rebuild
1257
+ rebuild: seam.rebuild,
1258
+ slice: build.criteria,
1259
+ base: build.criteria === void 0 ? void 0 : sliceBase,
1260
+ more: build.more
1139
1261
  });
1140
1262
  const carried = seam.rebuild?.acceptance?.tests ?? [];
1141
- const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
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;
1142
1265
  await tail(settleQuality, {
1143
1266
  green: true,
1144
- lensBase: base
1267
+ lensBase: base,
1268
+ guard,
1269
+ guardRests: false
1145
1270
  });
1146
1271
  });
1147
1272
  };
@@ -1187,6 +1312,7 @@ const ordinaryStart = async () => {
1187
1312
  });
1188
1313
  refusePendingHandOff();
1189
1314
  flagModeOn();
1315
+ sliceCap();
1190
1316
  const stated = statedCriteria(changes(), head());
1191
1317
  await unwind();
1192
1318
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
@@ -1202,4 +1328,4 @@ const feature = shipped(ordinaryStart);
1202
1328
  //#region src/workflows/feature/index.ts
1203
1329
  var feature_default = feature;
1204
1330
  //#endregion
1205
- export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, holdsCallSites, implement, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unwind };
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 { G as finisherSystem, K as fixReviewNotesPrompt, O as agentWithSkills, _t as defaults, b as coder, c as baseline, i as skills, n as summary, r as access, s as shipped, t as steering, v as REVIEW, vt as envDefaults } from "../chunks/reserved-zAfoJtR7.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-BRC4DGdV.mjs";
3
+ import { 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 { _t as defaults, ct as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate, vt as envDefaults } from "../chunks/reserved-zAfoJtR7.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-BRC4DGdV.mjs";
2
+ import { 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 flipProblems, _t as defaults, b as coder, bt as flagMentions, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, vt as envDefaults, wt as requireFlag } from "../chunks/reserved-zAfoJtR7.mjs";
2
+ import { Et as 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.";