@pmelab/gtd 35.0.0 → 37.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +45 -29
  3. package/claude/hooks/context.ts +66 -0
  4. package/claude/hooks/drive.ts +3 -0
  5. package/claude/hooks/register.tsx +69 -6
  6. package/dist/chunks/{reserved-C8Fm3s72.mjs → reserved-zAfoJtR7.mjs} +62 -24
  7. package/dist/chunks/{tail-oaYewBN_.mjs → tail-BRC4DGdV.mjs} +1 -1
  8. package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
  9. package/dist/flows.mjs +15 -10
  10. package/dist/gtd.bundle.mjs +511 -64
  11. package/dist/testkit.mjs +158 -26
  12. package/dist/wire.mjs +2 -2
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +138 -38
  15. package/dist/workflows/fix.mjs +2 -2
  16. package/dist/workflows/review.mjs +2 -2
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +208 -66
  19. package/package.json +1 -1
  20. package/skills/authoring/SKILL.md +3 -3
  21. package/src/cli/Cli.test.ts +10 -3
  22. package/src/cli/Cli.ts +76 -1
  23. package/src/cli/Program.test.ts +1 -0
  24. package/src/cli/Program.ts +28 -0
  25. package/src/cli/ToolCommands.ts +3 -0
  26. package/src/edge/Edge.ts +11 -0
  27. package/src/flows/runtime.ts +11 -0
  28. package/src/flows/scripts.ts +31 -9
  29. package/src/replay/Replay.ts +51 -2
  30. package/src/testkit/FlowScripts.test.ts +58 -0
  31. package/src/testkit/FlowScripts.ts +61 -24
  32. package/src/testkit/Layers.ts +1 -0
  33. package/src/wire/BeatDocument.ts +12 -2
  34. package/src/wire/BeatStatus.ts +3 -0
  35. package/src/wire/ContextLimit.fixture.ts +30 -0
  36. package/src/wire/ContextLimit.test.ts +96 -0
  37. package/src/wire/ContextLimit.ts +169 -0
  38. package/src/wire/index.ts +1 -0
  39. package/src/wire/types.ts +7 -0
  40. package/src/workflow/Workflow.ts +9 -1
  41. package/src/workflows/feature/acceptance-criteria.feature +5 -1
  42. package/src/workflows/feature/context-limit.feature +37 -0
  43. package/src/workflows/feature/coverage.feature +6 -3
  44. package/src/workflows/feature/coverage.ts +8 -2
  45. package/src/workflows/feature/fast-path.feature +11 -19
  46. package/src/workflows/feature/feature.test.ts +24 -4
  47. package/src/workflows/feature/feature.ts +14 -10
  48. package/src/workflows/feature/flag-mode.feature +17 -9
  49. package/src/workflows/feature/lens-models.feature +7 -1
  50. package/src/workflows/feature/naive-acceptance.feature +2 -2
  51. package/src/workflows/feature/one-step-land.feature +10 -12
  52. package/src/workflows/feature/quality-unresolved.feature +7 -1
  53. package/src/workflows/feature/refactor-absorbs.feature +12 -5
  54. package/src/workflows/feature/refactor-first.feature +11 -2
  55. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  56. package/src/workflows/feature/refactor-rebuild.feature +269 -0
  57. package/src/workflows/feature/refactor-review.feature +45 -1
  58. package/src/workflows/feature/refactor.ts +9 -3
  59. package/src/workflows/feature/risk-tier.feature +12 -4
  60. package/src/workflows/feature/seam-rejection.feature +4 -9
  61. package/src/workflows/feature/seam.feature +29 -26
  62. package/src/workflows/feature/seam.test.ts +117 -6
  63. package/src/workflows/feature/seam.ts +158 -39
  64. package/src/workflows/review/review-door.feature +12 -4
  65. package/src/workflows/shared/options.test.ts +11 -2
  66. package/src/workflows/shared/options.ts +18 -4
  67. package/src/workflows/shared/steps.ts +1 -0
  68. package/src/workflows/shared/text.test.ts +8 -0
  69. package/src/workflows/shared/text.ts +91 -22
@@ -214,25 +214,21 @@ const checkScript = (command, options) => {
214
214
  ""
215
215
  ].join("\n");
216
216
  };
217
- /**
218
- * Reverse-apply `commit`'s changes to the working tree. A failure — the
219
- * patch no longer applies — writes `failure` and git's error to `report`,
220
- * since a failed revert and one with nothing to undo both leave the tree clean.
221
- */
222
- const revertScript = (commit, report, failure) => {
217
+ const patchScript = (commit, report, failure, reverse, exclude = []) => {
218
+ const paths = exclude.length === 0 ? "" : ` . ${exclude.map((p) => quote(`:(exclude,literal)${p}`)).join(" ")}`;
223
219
  const range = `${quote(`${commit}^`)} ${quote(commit)}`;
224
220
  const error = quote(`${report}.output`);
225
221
  return [
226
222
  "#!/usr/bin/env sh",
227
223
  "set +e",
228
- `if ! git diff --quiet ${range} --; then`,
224
+ `if ! git diff --quiet ${range} --${paths}; then`,
229
225
  ` mkdir -p "$(dirname ${quote(report)})"`,
230
- ` git diff --binary ${range} -- | git apply -R 2> ${error}`,
226
+ ` git diff --binary --no-color --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ ${range} --${paths} | git apply --whitespace=nowarn${reverse ? " -R" : ""} 2> ${error}`,
231
227
  " code=$?",
232
228
  " if [ \"$code\" -ne 0 ]; then",
233
229
  " {",
234
230
  ` printf '%s\\n\\n' ${quote(failure)}`,
235
- ` if [ -s ${error} ]; then cat ${error}; else printf 'Reverting it exited %s and produced no output.\\n' "$code"; fi`,
231
+ ` if [ -s ${error} ]; then cat ${error}; else printf '${reverse ? "Reverting" : "Applying"} it exited %s and produced no output.\\n' "$code"; fi`,
236
232
  ` } > ${quote(report)}`,
237
233
  " fi",
238
234
  ` rm -f ${error}`,
@@ -241,6 +237,14 @@ const revertScript = (commit, report, failure) => {
241
237
  ].join("\n");
242
238
  };
243
239
  /**
240
+ * Reverse-apply `commit`'s changes to the working tree. A failure — the
241
+ * patch no longer applies — writes `failure` and git's error to `report`,
242
+ * since a failed revert and one with nothing to undo both leave the tree clean.
243
+ */
244
+ const revertScript = (commit, report, failure) => patchScript(commit, report, failure, true);
245
+ /** Apply `commit`'s changes outside `exclude` to the working tree; a failure is reported as `revertScript`'s is. */
246
+ const applyScript = (commit, report, failure, exclude = []) => patchScript(commit, report, failure, false, exclude);
247
+ /**
244
248
  * Undo the edits one commit made to `paths`: each goes back to its content
245
249
  * before `commit` — or away, when `commit` added it — but only while it is
246
250
  * still exactly as `commit` left it. A path changed since is left alone.
@@ -17188,6 +17192,7 @@ const agentWithSkills = (name, prompt, options = {}) => {
17188
17192
  return agent(name, withSkills(skillsFor(name).join(", "), body), options);
17189
17193
  };
17190
17194
  const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
17195
+ const naiveApplyFailure = (commit) => `gtd could not apply the naive pass ${commit} to the working tree; it is built fresh instead.`;
17191
17196
  const idleMessage = () => `No active gtd process.
17192
17197
 
17193
17198
  To start one, make ANY change — a hand-edit to real code, a scratch
@@ -17431,9 +17436,11 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
17431
17436
  - Introduce no new flag read and add no second read of an existing one:
17432
17437
  every match of \`${pattern}\` may move, never increase in number`;
17433
17438
  const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
17434
- const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
17439
+ const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
17435
17440
  - The package to implement is \`${pkg}\`; never delete it
17436
- ${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
17441
+ ${note ? `- The package's \`## Refactor note\` section names the seams to open and the
17442
+ shape each takes, written from the whole naive pass: build exactly those
17443
+ ` : ""}${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
17437
17444
  last refactor. Act on it, within the rules below
17438
17445
  ` : ""}${refactorRules(strays, pattern)}
17439
17446
  ${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
@@ -18036,8 +18043,8 @@ const naivePassPrompt = (retry) => `${stateFileRules}
18036
18043
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18037
18044
  ${criteriaBinding(false)}
18038
18045
  - Implement them the quickest way that works — the naive version. Do not
18039
- restructure, abstract or refactor around the change; a later step judges
18040
- the shape of your diff and a reviewer makes it fit
18046
+ restructure, abstract or refactor around the change; it may land exactly as
18047
+ you leave it, and a reviewer makes it fit
18041
18048
  - Work breadth-first: rough out every part of the change before finishing any
18042
18049
  one, so a run cut off midway still shows the whole shape
18043
18050
  - Write behaviour-level acceptance tests, at least one per criterion, each
@@ -18052,21 +18059,50 @@ ${criteriaBinding(false)}
18052
18059
  - Leave \`.gtd/\` untouched
18053
18060
  - Leave everything uncommitted and finish your turn — do not commit
18054
18061
  ${retry === void 0 ? "" : naiveRetry(retry)}`;
18055
- const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
18062
+ const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
18063
+ const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
18064
+ opened; plug the change into them:
18065
+
18066
+ ${note.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n")}
18067
+
18068
+ `}${acceptance === void 0 || !carries({ acceptance }) ? "" : `- Carry over the naive pass's acceptance tests for ${acceptance.ids.join(", ")}, the
18069
+ one part of it you take as it is: \`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`.
18070
+ Copy each test with its \`[AC-<n>]\` name into the tree as the refactor left
18071
+ it — check a file out of the tip whole only where the refactor did not touch
18072
+ it — changing only what reaches the release flag. A turn whose changed tests
18073
+ leave an ID out is refused, and the suite must be green with them
18074
+ `}`;
18075
+ const refactorNotePrompt = (reference, strays) => `${stateFileRules}
18076
+ - The seam judge ruled the naive pass refactor-first: it is never applied.
18077
+ A behaviour-neutral refactor lands first, then the change is rebuilt on
18078
+ the refactored base. Both turns read the note you write
18079
+ - Read the whole naive pass: \`git show ${reference}\`. It changed these
18080
+ existing files outside the allowlist:
18081
+ ${listed(strays)}
18082
+ - Write \`.gtd/REFACTOR-NOTE.md\`: which seams the refactor opens (the existing
18083
+ code to restructure, by path and symbol) and the shape each takes — the
18084
+ signatures and where the new behaviour will plug in. Name only the files
18085
+ listed above: the refactor may change no other path. Never the feature's own
18086
+ behaviour, and no flag read: the refactor adds neither
18087
+ - Change nothing else — a turn that does is refused
18088
+ - Leave everything uncommitted and finish your turn — do not commit
18089
+ `;
18090
+ const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed and may already carry part of the change,
18091
+ though never a release flag. Build only what is still missing, held to every
18092
+ rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
18093
+ `;
18094
+ const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}) => `${stateFileRules}
18056
18095
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18057
18096
  ${criteriaBinding(true)}
18058
- ${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
18059
- though never a release flag. Build only what is still missing, held to every
18060
- rule below; if nothing is, change nothing and finish your turn
18061
- ` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
18097
+ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
18062
18098
  the re-derived requirements instead of rewriting it
18063
18099
  ` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
18064
18100
  ` : `- Implement them the quickest way that works — the naive version. Do not
18065
18101
  restructure, abstract or refactor around the change
18066
18102
  `}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
18067
18103
  Read it for ideas; never apply, cherry-pick or check it out — write
18068
- the change fresh
18069
- `}${!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
18104
+ 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
18070
18106
  repository's flag conventions`}
18071
18107
  - Write the tests first, then the code that makes them pass. Name each
18072
18108
  criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
@@ -18087,12 +18123,13 @@ An edit together with an approval counts as steer. Only
18087
18123
  \`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
18088
18124
  branch unjudged.
18089
18125
  `;
18090
- const seamSteerMessage = (retry) => isFault(retry) ? `The naive pass failed three times and the retries are spent. The last
18126
+ /** `judged`: whether an overruled pass still meets the seam judge (`flagMode` on). */
18127
+ const seamSteerMessage = (retry, judged = true) => isFault(retry) ? `The naive pass failed three times and the retries are spent. The last
18091
18128
  attempt's tests (\`git show ${retry.hash}\`) fall short:
18092
18129
  ${retry.fault}
18093
18130
 
18094
18131
  ${steerChoices(`land a clean tree to go on with the last attempt as it is:
18095
- \`seam.check\` runs its suite and the seam judge rules on its shape. Use it
18132
+ \`seam.check\` runs its suite and ${judged ? "the seam judge rules on its shape" : "it lands as the implementation"}. Use it
18096
18133
  when the rule cannot fit this change (tests the globs do not see, or no
18097
18134
  entry point that existed before). Risk: a criterion may have no test that
18098
18135
  survives a later refactor — the choice is recorded in history.`)}` : isRed(retry) ? `The naive pass failed three times and the retries are spent. The last
@@ -18103,7 +18140,7 @@ Two ways forward (then run \`gtd land\`):
18103
18140
  - **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
18104
18141
  pass runs again with a fresh retry budget
18105
18142
  - **Overrule** — land a clean tree to go on with the last attempt as it is:
18106
- the seam judge rules on its shape, and the implementation's health loop
18143
+ ${judged ? "the seam judge rules on its shape" : "it lands as the implementation"}, and the implementation's health loop
18107
18144
  must turn the suite green. Risk: the criteria's tests never passed on the
18108
18145
  naive pass — the choice is recorded in history.
18109
18146
 
@@ -18119,9 +18156,8 @@ Two ways forward (then run \`gtd land\`):
18119
18156
  - **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
18120
18157
  pass runs again with a fresh retry budget (the risk tier stays as the
18121
18158
  design gate settled it; a tier edit here is not applied)
18122
- - **Overrule** — land a clean tree to accept the last attempt's approach:
18123
- the implementation is built against it as reference, with no seam gate
18124
- and no refactor. Risk: this ships the seam the judge objected to,
18159
+ - **Overrule** — land a clean tree to accept the last attempt as it is: it
18160
+ lands as the implementation, with no seam gate and no refactor. Risk: this ships the seam the judge objected to,
18125
18161
  unaddressed — the choice is recorded in history.
18126
18162
 
18127
18163
  An edit together with an approval counts as steer. Only
@@ -18175,6 +18211,7 @@ const ESCALATION = ".gtd/ESCALATION.md";
18175
18211
  const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
18176
18212
  const REVIEW = ".gtd/REVIEW.md";
18177
18213
  const REFACTOR = ".gtd/REFACTOR.md";
18214
+ const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
18178
18215
  const QUALITY = ".gtd/QUALITY.md";
18179
18216
  const planner = () => env.plannerModel ?? "";
18180
18217
  const coder = () => env.coderModel ?? "";
@@ -37692,7 +37729,11 @@ const ShippedOptions = Struct({
37692
37729
  testCommand: setting,
37693
37730
  plannerModel: setting,
37694
37731
  coderModel: setting,
37695
- flagMode: optionalWith(Boolean$, { exact: true })
37732
+ flagMode: optionalWith(Boolean$, { exact: true }),
37733
+ contextLimit: optionalWith(Struct({
37734
+ soft: optionalWith(Number$, { exact: true }),
37735
+ hard: optionalWith(Number$, { exact: true })
37736
+ }), { exact: true })
37696
37737
  });
37697
37738
  const over = (base, options) => ({
37698
37739
  ...base,
@@ -37704,12 +37745,13 @@ const flagModeOn = () => {
37704
37745
  if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
37705
37746
  return value === "true";
37706
37747
  };
37748
+ const limited = (flow, context) => context === void 0 ? flow : () => scope({ context }, flow);
37707
37749
  /** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
37708
37750
  const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
37709
37751
  onExcessProperty: "error",
37710
37752
  errors: "all"
37711
- }), (options) => ({
37712
- flow,
37753
+ }), ({ contextLimit, ...options }) => ({
37754
+ flow: limited(flow, contextLimit),
37713
37755
  defaults: over(defaults, options),
37714
37756
  envDefaults: over(envDefaults, options),
37715
37757
  doors,
@@ -37722,14 +37764,14 @@ const FlagOptions = Struct({
37722
37764
  const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
37723
37765
  onExcessProperty: "error",
37724
37766
  errors: "all"
37725
- }), (options) => {
37767
+ }), ({ contextLimit, ...options }) => {
37726
37768
  const flagDefaults = {
37727
37769
  ...defaults,
37728
37770
  flag: "",
37729
37771
  flagRegistry: defaultFlagRegistry
37730
37772
  };
37731
37773
  return {
37732
- flow,
37774
+ flow: limited(flow, contextLimit),
37733
37775
  defaults: over(flagDefaults, options),
37734
37776
  envDefaults: over(envDefaults, options),
37735
37777
  doors: { [name]: {
@@ -38468,7 +38510,8 @@ const plan = (shape, options) => {
38468
38510
  return {
38469
38511
  pattern,
38470
38512
  strays,
38471
- reference: options.reference?.trim() ? options.reference : void 0
38513
+ reference: options.reference?.trim() ? options.reference : void 0,
38514
+ note: options.note?.trim() ? options.note : void 0
38472
38515
  };
38473
38516
  };
38474
38517
  const guardOf = ({ pattern, strays }, entry) => () => {
@@ -38477,13 +38520,22 @@ const guardOf = ({ pattern, strays }, entry) => () => {
38477
38520
  };
38478
38521
  /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
38479
38522
  const refactorLap = async (p, guard, feedback) => {
38480
- const { pattern, strays, reference } = p;
38481
- await run("deriving", deriveScript([...packageLines(strays, pattern, reference), ...feedback === void 0 ? [] : [
38482
- "## Review feedback",
38483
- "",
38484
- ...demoteHeadings(feedback),
38485
- ""
38486
- ]]), {
38523
+ const { pattern, strays, reference, note } = p;
38524
+ await run("deriving", deriveScript([
38525
+ ...packageLines(strays, pattern, reference),
38526
+ ...note === void 0 ? [] : [
38527
+ "## Refactor note",
38528
+ "",
38529
+ ...demoteHeadings(note),
38530
+ ""
38531
+ ],
38532
+ ...feedback === void 0 ? [] : [
38533
+ "## Review feedback",
38534
+ "",
38535
+ ...demoteHeadings(feedback),
38536
+ ""
38537
+ ]
38538
+ ]), {
38487
38539
  label: "Deriving the refactor package",
38488
38540
  file: REFACTOR_PACKAGE
38489
38541
  });
@@ -38492,7 +38544,8 @@ const refactorLap = async (p, guard, feedback) => {
38492
38544
  strays,
38493
38545
  pattern,
38494
38546
  reference,
38495
- feedback: feedback !== void 0
38547
+ feedback: feedback !== void 0,
38548
+ note: note !== void 0
38496
38549
  }), {
38497
38550
  label: "Refactoring",
38498
38551
  model: coder(),
@@ -38578,13 +38631,16 @@ const COVERED_MIN_P = .7;
38578
38631
  /** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
38579
38632
  const COVERING_RETRIES = 2;
38580
38633
  const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
38581
- const green = () => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
38634
+ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
38582
38635
  label: "Fixing the check",
38583
38636
  file: FEEDBACK,
38584
38637
  model: coder(),
38585
38638
  system: builderSystem()
38586
38639
  }), {
38587
- guard: frozen,
38640
+ guard: () => {
38641
+ frozen();
38642
+ guard?.();
38643
+ },
38588
38644
  guardRests: false
38589
38645
  });
38590
38646
  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");
@@ -38698,16 +38754,20 @@ const addedLines = (change) => {
38698
38754
  const before = new Set((change.before ?? "").split("\n"));
38699
38755
  return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
38700
38756
  };
38757
+ 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 !== "");
38759
+ const unnamed = (ids, tests) => {
38760
+ const lines = tests.map(addedLines);
38761
+ return ids.filter((id) => !lines.some((text) => named(id, text)));
38762
+ };
38701
38763
  /**
38702
38764
  * Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
38703
38765
  * test line of the pass names, or a test importing a file the pass added —
38704
38766
  * naive structure a reset and refactor replaces, which the tests must survive.
38705
38767
  */
38706
38768
  const acceptanceFault = (file, tip) => {
38707
- const ids = (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
38708
- const tests = tip.changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
38709
- const lines = tests.map(addedLines);
38710
- const untested = ids.filter((id) => !lines.some((text) => named(id, text)));
38769
+ const tests = acceptanceTests(tip.changes);
38770
+ const untested = unnamed(idsOf(file), tests);
38711
38771
  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]}] …"`;
38712
38772
  const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
38713
38773
  const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
@@ -38716,6 +38776,41 @@ const acceptanceFault = (file, tip) => {
38716
38776
  });
38717
38777
  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")}`;
38718
38778
  };
38779
+ /** 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);
38782
+ const perTest = acceptanceTests(tip.changes).map((c) => {
38783
+ const lines = addedLines(c);
38784
+ return {
38785
+ path: c.path,
38786
+ ids: ids.filter((id) => named(id, lines))
38787
+ };
38788
+ });
38789
+ return {
38790
+ hash: tip.hash,
38791
+ tests: perTest.filter((c) => c.ids.length > 0).map((c) => c.path),
38792
+ ids: ids.filter((id) => perTest.some((c) => c.ids.includes(id)))
38793
+ };
38794
+ };
38795
+ /**
38796
+ * Refuse a rebuild whose test changes since `since` leave out an ID the naive
38797
+ * pass's tests named. A skipped test still counts: the gate reads text, never runs it.
38798
+ */
38799
+ const requireAcceptance = (acceptance, since) => {
38800
+ const missing = unnamed(acceptance.ids, acceptanceTests(changesSince(since)));
38801
+ if (missing.length === 0) return;
38802
+ refuse(`gtd land: rebuild-acceptance: the rebuild's tests leave out ${missing.join(", ")} — carry over the naive pass's acceptance tests (\`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`), each with its ID in the test or scenario name`);
38803
+ };
38804
+ /**
38805
+ * Refuse a fix turn that, on its own, drops an ID the naive pass's tests named
38806
+ * from a test file. Turn-local, so a person's edit at an escalation rest never
38807
+ * wedges later fix turns.
38808
+ */
38809
+ const requireAcceptanceKept = (acceptance) => {
38810
+ const dropped = acceptance.ids.filter((id) => changes().filter((c) => isCode$1(c.path) && isTestFile(c.path)).some((c) => named(id, c.before ?? "") && !named(id, c.after ?? "")));
38811
+ if (dropped.length === 0) return;
38812
+ refuse(`gtd land: rebuild-acceptance: this turn drops ${dropped.join(", ")} from the tests — the naive pass's acceptance tests stay; fix the code, not them`);
38813
+ };
38719
38814
  const restore = (path) => `if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`;
38720
38815
  /**
38721
38816
  * Run the suite with `tip`'s diff applied, then take it out again; resolves
@@ -38728,7 +38823,7 @@ const naiveGreen = (tip) => {
38728
38823
  return check("check", [
38729
38824
  ...paths,
38730
38825
  "dirty=$(git -c core.quotePath=false ls-files -m -d -o --exclude-standard)",
38731
- `if ! git diff --binary ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply; then`,
38826
+ `if ! git diff --binary --no-color --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply --whitespace=nowarn; then`,
38732
38827
  " echo 'gtd: the kept naive pass no longer applies to HEAD; no test ran'",
38733
38828
  " exit 1",
38734
38829
  "fi",
@@ -38747,6 +38842,11 @@ const naiveGreen = (tip) => {
38747
38842
  label: "Running the naive pass's tests"
38748
38843
  });
38749
38844
  };
38845
+ /** Resolves `false` when the tip no longer applies (the failure is in `.gtd/FEEDBACK.md`) or has nothing outside `.gtd/` to apply. */
38846
+ const applied = async (tip) => {
38847
+ await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
38848
+ return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
38849
+ };
38750
38850
  function implement(first, tier, amend) {
38751
38851
  const options = typeof first === "object" && first !== null ? first : {
38752
38852
  reference: first,
@@ -38754,22 +38854,26 @@ function implement(first, tier, amend) {
38754
38854
  amend
38755
38855
  };
38756
38856
  return scope("implement", async () => {
38757
- await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn()), {
38857
+ const since = head$1();
38858
+ const acceptance = options.rebuild?.acceptance;
38859
+ 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), {
38758
38861
  label: "Implementing",
38759
38862
  model: coder(),
38760
38863
  system: builderSystem(),
38761
38864
  ...options.afterRefactor === true ? { allowEmpty: true } : {}
38762
38865
  });
38763
38866
  requireCriteriaFrozen(REQUIREMENTS);
38764
- await green();
38867
+ if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
38868
+ await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
38765
38869
  await requireCoverage();
38766
- await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
38870
+ await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), { label: "Closing out the implementation" });
38767
38871
  });
38768
38872
  }
38769
38873
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
38770
38874
  const steer = async (retry) => {
38771
38875
  await human("steer", {
38772
- message: seamSteerMessage(retry),
38876
+ message: seamSteerMessage(retry, flagModeOn()),
38773
38877
  label: "Steer or overrule the naive pass",
38774
38878
  file: REQUIREMENTS,
38775
38879
  acceptClean: true
@@ -38813,16 +38917,23 @@ const vetted = async (tip, vetting) => {
38813
38917
  /** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
38814
38918
  const ruled = async (tip, again) => {
38815
38919
  const shape = await judgeSeam(tip);
38816
- if (shape.choice === "land") return { reference: tip.hash };
38920
+ if (shape.choice === "land") return {
38921
+ reference: tip.hash,
38922
+ tip
38923
+ };
38817
38924
  if (shape.choice === "refactor-first") return {
38818
38925
  reference: tip.hash,
38926
+ tip,
38819
38927
  shape
38820
38928
  };
38821
38929
  const next = await again({
38822
38930
  strays: shape.strays,
38823
38931
  p: shape.judgment?.answers.shape?.p ?? 0
38824
38932
  });
38825
- return next.kind === "overruled" ? { reference: tip.hash } : next;
38933
+ return next.kind === "overruled" ? {
38934
+ reference: tip.hash,
38935
+ tip
38936
+ } : next;
38826
38937
  };
38827
38938
  const decide = () => scope("seam", async () => {
38828
38939
  const vetting = {
@@ -38833,16 +38944,33 @@ const decide = () => scope("seam", async () => {
38833
38944
  for (;;) {
38834
38945
  await naivePass(retry);
38835
38946
  const tip = kept("naive");
38836
- const outcome = await vetted(tip, vetting) ?? await ruled(tip, vetting.again);
38947
+ const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
38948
+ reference: tip.hash,
38949
+ tip
38950
+ });
38837
38951
  if (!("kind" in outcome)) return outcome;
38838
38952
  retry = outcome.kind === "retry" ? outcome.retry : void 0;
38839
38953
  }
38840
38954
  });
38955
+ /** Write the refactor note from the whole naive pass; a turn that changes anything else is refused. */
38956
+ const refactorNote = async (reference, strays) => {
38957
+ await agent("note", refactorNotePrompt(reference, strays), {
38958
+ label: "Writing the refactor note",
38959
+ file: REFACTOR_NOTE,
38960
+ model: coder(),
38961
+ system: builderSystem()
38962
+ });
38963
+ const others = changes().map((c) => c.path).filter((path) => path !== REFACTOR_NOTE);
38964
+ const note = read(".gtd/REFACTOR-NOTE.md") ?? "";
38965
+ 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
+ return note;
38967
+ };
38841
38968
  const seamBuild = async (base) => {
38842
- const { reference, shape } = await decide();
38969
+ const { reference, tip, shape } = await decide();
38843
38970
  if (shape === void 0) return {
38844
38971
  reference,
38845
- base
38972
+ base,
38973
+ land: true
38846
38974
  };
38847
38975
  if (shape.unreadable.length > 0) {
38848
38976
  await scope("seam", () => human("by-hand", {
@@ -38852,18 +38980,29 @@ const seamBuild = async (base) => {
38852
38980
  }));
38853
38981
  return {
38854
38982
  reference,
38855
- base
38983
+ base,
38984
+ rebuild: { acceptance: naiveAcceptance(tip) }
38856
38985
  };
38857
38986
  }
38858
- if (refactorStrays(shape).length === 0) return {
38987
+ const strays = refactorStrays(shape);
38988
+ if (strays.length === 0) return {
38859
38989
  reference,
38860
- base
38990
+ base,
38991
+ rebuild: { acceptance: naiveAcceptance(tip) }
38861
38992
  };
38862
- const refactored = await slice("refactor", () => reviewedRefactor(shape, { reference }));
38993
+ const note = await scope("seam", () => refactorNote(reference, strays));
38994
+ const refactored = await slice("refactor", () => reviewedRefactor(shape, {
38995
+ reference,
38996
+ note
38997
+ }));
38863
38998
  return {
38864
38999
  reference,
38865
39000
  base: head$1(),
38866
- refactored
39001
+ refactored,
39002
+ rebuild: {
39003
+ note,
39004
+ acceptance: naiveAcceptance(tip)
39005
+ }
38867
39006
  };
38868
39007
  };
38869
39008
  //#endregion
@@ -38873,17 +39012,20 @@ const unwind = () => {
38873
39012
  const commit = head$1();
38874
39013
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
38875
39014
  };
38876
- /** A judged naive pass (none with `flagMode` off), then implement and the quality lap; design must have run since `firstBase`. */
39015
+ /** A naive pass (judged only with `flagMode` on), then implement and the quality lap; design must have run since `firstBase`. */
38877
39016
  const buildFeature = async (firstBase) => {
38878
39017
  const tier = riskTierOn() ? recordRisk() : void 0;
38879
- const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
39018
+ const seam = await seamBuild(firstBase);
38880
39019
  await slice("feature", async () => {
38881
39020
  await implement({
38882
39021
  reference: seam.reference,
38883
39022
  tier,
38884
- afterRefactor: seam.refactored
39023
+ afterRefactor: seam.refactored,
39024
+ naive: seam.land === true ? seam.reference : void 0,
39025
+ rebuild: seam.rebuild
38885
39026
  });
38886
- const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path)) ? firstBase : seam.base;
39027
+ 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;
38887
39029
  await tail(settleQuality, {
38888
39030
  green: true,
38889
39031
  lensBase: base
@@ -39126,4 +39268,4 @@ var workflows_default = feature;
39126
39268
  */
39127
39269
  const doors = {};
39128
39270
  //#endregion
39129
- export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, 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, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, 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 };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "35.0.0",
3
+ "version": "37.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -223,9 +223,9 @@ workflow.
223
223
  ## Worked example: add an approval gate before building
224
224
 
225
225
  In the bundled default, `planAndBuild` runs the design phase, then
226
- `buildFeature`: the naive pass and its seam judge, then the implementation and
227
- the quality lap, ending with no human review. Add a human sign-off before the
228
- build:
226
+ `buildFeature`: the naive pass and its seam judge, then the implementation (the
227
+ pass itself on `land`) and the quality lap, ending with no human review. Add a
228
+ human sign-off before the build:
229
229
 
230
230
  ```ts
231
231
  const planAndBuild = async (firstBase: string): Promise<void> => {
@@ -1106,6 +1106,7 @@ describe("standaloneKinds / needsOf", () => {
1106
1106
  "uncheck",
1107
1107
  "install",
1108
1108
  "loop",
1109
+ "contextHook",
1109
1110
  "judgeRun",
1110
1111
  "strays",
1111
1112
  ])
@@ -1117,6 +1118,7 @@ describe("standaloneKinds / needsOf", () => {
1117
1118
  expect(needsOf("uncheck")).toBe("fs")
1118
1119
  expect(needsOf("install")).toBe("none")
1119
1120
  expect(needsOf("loop")).toBe("none")
1121
+ expect(needsOf("contextHook")).toBe("none")
1120
1122
  expect(needsOf("judgeRun")).toBe("none")
1121
1123
  expect(needsOf("strays")).toBe("none")
1122
1124
  for (const kind of [
@@ -1202,19 +1204,24 @@ describe("renderHelp", () => {
1202
1204
 
1203
1205
  it("README.md and COMPATIBILITY.md list exactly the commands", () => {
1204
1206
  const commands = new Set(
1205
- [...renderHelp().matchAll(/^ {2}([a-z]+(?: [a-z]+)?)(?= )/gm)].map(([, name]) => name),
1207
+ [...renderHelp().matchAll(/^ {2}([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)(?= )/gm)].map(
1208
+ ([, name]) => name,
1209
+ ),
1206
1210
  )
1207
1211
  expect(commands).toContain("judge answer")
1212
+ expect(commands).toContain("context-hook")
1208
1213
  const listed = (file: string, list: RegExp): Set<string> => {
1209
1214
  const doc = readFileSync(resolve(import.meta.dirname, file), "utf8")
1210
1215
  const match = doc.match(list)
1211
1216
  expect(match, file).not.toBeNull()
1212
- return new Set([...match![1]!.matchAll(/`([a-z]+(?: [a-z]+)?)`/g)].map(([, name]) => name!))
1217
+ return new Set(
1218
+ [...match![1]!.matchAll(/`([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)`/g)].map(([, name]) => name!),
1219
+ )
1213
1220
  }
1214
1221
  expect(
1215
1222
  listed("../../README.md", /The `gtd` bin's commands — ([\s\S]*?) — are\s+listed/),
1216
1223
  ).toEqual(commands)
1217
- expect(listed("../../COMPATIBILITY.md", /every command \(([\s\S]*?)\), option/)).toEqual(
1224
+ expect(listed("../../COMPATIBILITY.md", /every command \(([\s\S]*?)\),\s+option/)).toEqual(
1218
1225
  commands,
1219
1226
  )
1220
1227
  })