@pmelab/gtd 32.1.0 → 34.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 (36) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +66 -53
  3. package/dist/chunks/{reserved-2fV0mj14.mjs → reserved-BcJgt5_R.mjs} +79 -12
  4. package/dist/chunks/{tail-BynhCfuv.mjs → tail-CM_fa93v.mjs} +1 -1
  5. package/dist/testkit.mjs +2 -2
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +174 -40
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +2 -2
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +249 -48
  12. package/package.json +1 -1
  13. package/skills/authoring/SKILL.md +2 -2
  14. package/src/testkit/steps.ts +4 -0
  15. package/src/workflows/feature/acceptance-criteria.feature +8 -0
  16. package/src/workflows/feature/fast-path.feature +11 -35
  17. package/src/workflows/feature/feature.test.ts +9 -100
  18. package/src/workflows/feature/feature.ts +29 -16
  19. package/src/workflows/feature/flag-mode.feature +2 -91
  20. package/src/workflows/feature/naive-acceptance.feature +140 -0
  21. package/src/workflows/feature/one-step-land.feature +11 -34
  22. package/src/workflows/feature/quality-unresolved.feature +96 -0
  23. package/src/workflows/feature/refactor-absorbs.feature +17 -25
  24. package/src/workflows/feature/refactor-first.feature +11 -35
  25. package/src/workflows/feature/refactor-lens-models.feature +6 -1
  26. package/src/workflows/feature/refactor-review.feature +67 -360
  27. package/src/workflows/feature/refactor.ts +4 -3
  28. package/src/workflows/feature/risk-tier.feature +10 -0
  29. package/src/workflows/feature/seam-rejection.feature +20 -37
  30. package/src/workflows/feature/seam.feature +66 -113
  31. package/src/workflows/feature/seam.test.ts +173 -3
  32. package/src/workflows/feature/seam.ts +196 -31
  33. package/src/workflows/review/review-door.feature +108 -0
  34. package/src/workflows/shared/tail.ts +2 -2
  35. package/src/workflows/shared/text.test.ts +40 -0
  36. package/src/workflows/shared/text.ts +110 -16
@@ -1,3 +1,4 @@
1
+ import { posix } from "node:path";
1
2
  //#region \0rolldown/runtime.js
2
3
  var __defProp = Object.defineProperty;
3
4
  var __exportAll = (all, no_symbols) => {
@@ -17212,6 +17213,17 @@ carries it.
17212
17213
  What each change does next (then run \`gtd land\`):
17213
17214
  - **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
17214
17215
  `;
17216
+ const qualityUnresolvedMessage = () => `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
17217
+ holds findings. Nothing reviews the change after this, so the process
17218
+ will not end with them open.
17219
+
17220
+ Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
17221
+ dropping a finding is your call to make.
17222
+
17223
+ What each change does next (then run \`gtd land\`):
17224
+ - **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the process ends (**idle**).
17225
+ - **Anything else** — rests here again.
17226
+ `;
17215
17227
  const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
17216
17228
  \`.gtd/FEEDBACK.md\` holds the failing output.
17217
17229
 
@@ -17999,24 +18011,47 @@ Print the closing message and stop — this writes nothing itself.
17999
18011
  `;
18000
18012
  };
18001
18013
  const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
18014
+ const isRed = (retry) => "red" in retry;
18015
+ const isFault = (retry) => "fault" in retry;
18016
+ const pickUp = (hash) => `Pick it up — \`git diff --binary ${hash}^ ${hash} | git apply\``;
18002
18017
  const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
18003
18018
  the file, never from memory, and stop only when each one holds. They are
18004
18019
  settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
18005
- const naivePassPrompt = (rejection) => `${stateFileRules}
18020
+ const naiveRetry = (retry) => isFault(retry) ? `
18021
+ Your previous attempt's (\`${retry.hash}\`) tests fall short:
18022
+ ${retry.fault}
18023
+ ${pickUp(retry.hash)} — and fix its tests; do not start over.
18024
+ ` : isRed(retry) ? `
18025
+ Your previous attempt (\`${retry.red}\`) left the suite red; \`.gtd/FEEDBACK.md\`
18026
+ holds the output. ${pickUp(retry.red)}
18027
+ — and fix it until the suite is green; do not start over.
18028
+ ` : `
18029
+ Your previous attempt was rejected: the judge answered \`reject\` (p=${retry.p}),
18030
+ meaning the naive pass solved the task the wrong way — discard it and
18031
+ re-implement. It changed these files outside the expected set:
18032
+ ${listed(retry.strays)}
18033
+ Start over and choose a different approach; do not rebuild the same diff.
18034
+ `;
18035
+ const naivePassPrompt = (retry) => `${stateFileRules}
18006
18036
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18007
18037
  ${criteriaBinding(false)}
18008
18038
  - Implement them the quickest way that works — the naive version. Do not
18009
18039
  restructure, abstract or refactor around the change; a later step judges
18010
18040
  the shape of your diff and a reviewer makes it fit
18011
- - Add tests as you go, and leave \`.gtd/\` untouched
18041
+ - Work breadth-first: rough out every part of the change before finishing any
18042
+ one, so a run cut off midway still shows the whole shape
18043
+ - Write behaviour-level acceptance tests, at least one per criterion, each
18044
+ naming its ID in the test or scenario name (\`[AC-3] …\`). Test through code
18045
+ that existed before this pass — a public entry point, the CLI, an existing
18046
+ module's exports — never a file this pass adds: that is naive structure a
18047
+ later reset and refactor replaces. A pass that leaves a criterion's ID out
18048
+ of its tests, or whose test imports a file it added, is sent back
18049
+ - Run \`${env.testCommand}\` and finish only once it is green with every
18050
+ criterion's test passing: the next step runs it on your diff and sends a red
18051
+ pass back
18052
+ - Leave \`.gtd/\` untouched
18012
18053
  - Leave everything uncommitted and finish your turn — do not commit
18013
- ${rejection === void 0 ? "" : `
18014
- Your previous attempt was rejected: the judge answered \`reject\` (p=${rejection.p}),
18015
- meaning the naive pass solved the task the wrong way — discard it and
18016
- re-implement. It changed these files outside the expected set:
18017
- ${listed(rejection.strays)}
18018
- Start over and choose a different approach; do not rebuild the same diff.
18019
- `}`;
18054
+ ${retry === void 0 ? "" : naiveRetry(retry)}`;
18020
18055
  const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
18021
18056
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18022
18057
  ${criteriaBinding(true)}
@@ -18041,8 +18076,41 @@ const implementFixPrompt = () => `${stateFileRules}
18041
18076
  ${fixFeedbackPrompt}
18042
18077
  - Leave everything uncommitted and finish your turn — do not commit
18043
18078
  `;
18044
- const seamSteerMessage = (rejection) => `The naive pass was rejected three times and the retries are spent.
18045
- The last attempt changed these files outside the expected set:
18079
+ const steerChoices = (overrule) => `Two ways forward (then run \`gtd land\`):
18080
+ - **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
18081
+ pass runs again with a fresh retry budget
18082
+ - **Overrule** — ${overrule}
18083
+
18084
+ An edit together with an approval counts as steer. Only
18085
+ \`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
18086
+ branch unjudged.
18087
+ `;
18088
+ const seamSteerMessage = (retry) => isFault(retry) ? `The naive pass failed three times and the retries are spent. The last
18089
+ attempt's tests (\`git show ${retry.hash}\`) fall short:
18090
+ ${retry.fault}
18091
+
18092
+ ${steerChoices(`land a clean tree to go on with the last attempt as it is:
18093
+ \`seam.check\` runs its suite and the seam judge rules on its shape. Use it
18094
+ when the rule cannot fit this change (tests the globs do not see, or no
18095
+ entry point that existed before). Risk: a criterion may have no test that
18096
+ 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
18097
+ attempt left the suite red: \`.gtd/FEEDBACK.md\` holds the last run's output; \`git show ${retry.red}\` shows
18098
+ the last attempt.
18099
+
18100
+ Two ways forward (then run \`gtd land\`):
18101
+ - **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
18102
+ pass runs again with a fresh retry budget
18103
+ - **Overrule** — land a clean tree to go on with the last attempt as it is:
18104
+ the seam judge rules on its shape, and the implementation's health loop
18105
+ must turn the suite green. Risk: the criteria's tests never passed on the
18106
+ naive pass — the choice is recorded in history.
18107
+
18108
+ An edit together with an approval counts as steer. Only
18109
+ \`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
18110
+ branch unjudged.
18111
+ ` : seamRejectedMessage(retry);
18112
+ const seamRejectedMessage = (rejection) => `The naive pass failed three times and the retries are spent. The judge
18113
+ rejected the last attempt: it changed these files outside the expected set:
18046
18114
  ${listed(rejection.strays)}
18047
18115
 
18048
18116
  Two ways forward (then run \`gtd land\`):
@@ -38473,7 +38541,7 @@ const reviewedRefactor = async (shape, options = {}) => {
38473
38541
  };
38474
38542
  //#endregion
38475
38543
  //#region src/workflows/feature/seam.ts
38476
- /** Naive-pass retries after a `reject`; the next `reject` rests at `seam.steer`. */
38544
+ /** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
38477
38545
  const NAIVE_RETRIES = 2;
38478
38546
  /** A change to an existing file outside the allowlist is a stray even when the task needs it: the judge decides. */
38479
38547
  const judgeSeam = (tip) => judgeDiffShape("judge", {
@@ -38481,12 +38549,95 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
38481
38549
  allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
38482
38550
  exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
38483
38551
  });
38484
- const naivePass = (rejection) => agentWithSkills("naive", naivePassPrompt(rejection), {
38485
- label: rejection === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
38552
+ const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
38553
+ label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
38486
38554
  keep: true,
38487
38555
  model: coder(),
38488
38556
  system: builderSystem()
38489
38557
  });
38558
+ const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
38559
+ const EXTENSION = /\.[\w-]+$/;
38560
+ const ALIAS = /^(?:[@~]\/|#)/;
38561
+ /** The specifiers an added `path` answers to: itself, without its extension, and its directory for an `index`. */
38562
+ const namesOf = (path) => {
38563
+ const bare = path.replace(EXTENSION, "");
38564
+ return posix.basename(bare) === "index" ? [
38565
+ path,
38566
+ bare,
38567
+ posix.dirname(bare)
38568
+ ] : [path, bare];
38569
+ };
38570
+ /**
38571
+ * The repository paths `test` imports, each with and without an extension: a
38572
+ * relative specifier resolved from its directory, an aliased one (`@/`, `~/`,
38573
+ * `#`) as a path tail, anything else as written. Comments are skipped; a
38574
+ * computed import goes unseen.
38575
+ */
38576
+ const importsOf = (test) => [...(test.after ?? "").replace(COMMENT, "").matchAll(IMPORTED)].flatMap(([, , spec]) => {
38577
+ const relative = /^\.\.?(\/|$)/.test(spec);
38578
+ const tail = ALIAS.test(spec);
38579
+ const path = relative ? posix.join(posix.dirname(test.path), spec) : spec.replace(ALIAS, "");
38580
+ return [path, path.replace(EXTENSION, "")].map((p) => ({
38581
+ path: p,
38582
+ tail
38583
+ }));
38584
+ });
38585
+ const reaches = (imports, added) => namesOf(added).some((name) => imports.some(({ path, tail }) => name === path || tail && name.endsWith(`/${path}`)));
38586
+ const named = (id, text) => new RegExp(`(?<![\\w-])${id}(?!\\d)`).test(text);
38587
+ /** The lines `change` added: an ID an earlier change left in the file does not count. */
38588
+ const addedLines = (change) => {
38589
+ const before = new Set((change.before ?? "").split("\n"));
38590
+ return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
38591
+ };
38592
+ /**
38593
+ * Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
38594
+ * test line of the pass names, or a test importing a file the pass added —
38595
+ * naive structure a reset and refactor replaces, which the tests must survive.
38596
+ */
38597
+ const acceptanceFault = (file, tip) => {
38598
+ const ids = (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
38599
+ const tests = tip.changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
38600
+ const lines = tests.map(addedLines);
38601
+ const untested = ids.filter((id) => !lines.some((text) => named(id, text)));
38602
+ 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]}] …"`;
38603
+ const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
38604
+ const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
38605
+ const imports = importsOf(test);
38606
+ return added.filter((path) => reaches(imports, path)).map((path) => ` - ${test.path} → ${path}`);
38607
+ });
38608
+ 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")}`;
38609
+ };
38610
+ 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`;
38611
+ /**
38612
+ * Run the suite with `tip`'s diff applied, then take it out again; resolves
38613
+ * `true` when green. The pass's paths go back to HEAD before (an interrupted
38614
+ * run left them applied) and after; afterwards so does anything else the run
38615
+ * changed, judged against what was already dirty, which is left alone.
38616
+ */
38617
+ const naiveGreen = (tip) => {
38618
+ const paths = tip.changes.map((c) => restore(c.path));
38619
+ return check("check", [
38620
+ ...paths,
38621
+ "dirty=$(git -c core.quotePath=false ls-files -m -d -o --exclude-standard)",
38622
+ `if ! git diff --binary ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply; then`,
38623
+ " echo 'gtd: the kept naive pass no longer applies to HEAD; no test ran'",
38624
+ " exit 1",
38625
+ "fi",
38626
+ "(",
38627
+ env.testCommand ?? "",
38628
+ ")",
38629
+ "code=$?",
38630
+ ...paths,
38631
+ "git -c core.quotePath=false ls-files -m -d -o --exclude-standard | while IFS= read -r path; do",
38632
+ " printf '%s\\n' \"$dirty\" | grep -qxF -- \"$path\" && continue",
38633
+ " if git cat-file -e \"HEAD:$path\" 2>/dev/null; then git checkout -q HEAD -- \"$path\"; else rm -f -- \"$path\"; fi",
38634
+ "done",
38635
+ "exit \"$code\""
38636
+ ].join("\n"), {
38637
+ report: FEEDBACK,
38638
+ label: "Running the naive pass's tests"
38639
+ });
38640
+ };
38490
38641
  function implement(first, tier, amend) {
38491
38642
  const options = typeof first === "object" && first !== null ? first : {
38492
38643
  reference: first,
@@ -38513,36 +38664,76 @@ function implement(first, tier, amend) {
38513
38664
  await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
38514
38665
  });
38515
38666
  }
38516
- const decide = () => scope("seam", async () => {
38517
- let rejection;
38667
+ /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
38668
+ const steer = async (retry) => {
38669
+ await human("steer", {
38670
+ message: seamSteerMessage(retry),
38671
+ label: "Steer or overrule the naive pass",
38672
+ file: REQUIREMENTS,
38673
+ acceptClean: true
38674
+ });
38675
+ if (changes().length === 0) return false;
38676
+ requireCriteria(REQUIREMENTS);
38677
+ return true;
38678
+ };
38679
+ /** Rejects, test faults and red checks share one budget; a steer or an overrule refills it. */
38680
+ const budget = () => {
38518
38681
  let retries = 0;
38519
- for (;;) {
38520
- await naivePass(rejection);
38521
- const tip = kept("naive");
38522
- const shape = await judgeSeam(tip);
38523
- if (shape.choice === "land") return { reference: tip.hash };
38524
- if (shape.choice === "refactor-first") return {
38525
- reference: tip.hash,
38526
- shape
38527
- };
38528
- rejection = {
38529
- strays: shape.strays,
38530
- p: shape.judgment?.answers.shape?.p ?? 0
38531
- };
38682
+ return async (next) => {
38532
38683
  if (retries < NAIVE_RETRIES) {
38533
38684
  retries++;
38534
- continue;
38685
+ return {
38686
+ kind: "retry",
38687
+ retry: next
38688
+ };
38535
38689
  }
38536
- await human("steer", {
38537
- message: seamSteerMessage(rejection),
38538
- label: "Steer or overrule the rejected naive pass",
38539
- file: REQUIREMENTS,
38540
- acceptClean: true
38541
- });
38542
- if (changes().length === 0) return { reference: tip.hash };
38543
- requireCriteria(REQUIREMENTS);
38544
- rejection = void 0;
38690
+ const steered = await steer(next);
38545
38691
  retries = 0;
38692
+ if (read(".gtd/FEEDBACK.md") !== void 0) await run("clear", removeScript([FEEDBACK]), { label: "Clearing the red check" });
38693
+ return { kind: steered ? "steered" : "overruled" };
38694
+ };
38695
+ };
38696
+ /** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
38697
+ const vetted = async (tip, vetting) => {
38698
+ const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
38699
+ if (fault !== void 0) {
38700
+ const verdict = await vetting.again({
38701
+ fault,
38702
+ hash: tip.hash
38703
+ });
38704
+ if (verdict.kind !== "overruled") return verdict;
38705
+ vetting.waived = true;
38706
+ }
38707
+ if (await naiveGreen(tip)) return void 0;
38708
+ const verdict = await vetting.again({ red: tip.hash });
38709
+ return verdict.kind === "overruled" ? void 0 : verdict;
38710
+ };
38711
+ /** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
38712
+ const ruled = async (tip, again) => {
38713
+ const shape = await judgeSeam(tip);
38714
+ if (shape.choice === "land") return { reference: tip.hash };
38715
+ if (shape.choice === "refactor-first") return {
38716
+ reference: tip.hash,
38717
+ shape
38718
+ };
38719
+ const next = await again({
38720
+ strays: shape.strays,
38721
+ p: shape.judgment?.answers.shape?.p ?? 0
38722
+ });
38723
+ return next.kind === "overruled" ? { reference: tip.hash } : next;
38724
+ };
38725
+ const decide = () => scope("seam", async () => {
38726
+ const vetting = {
38727
+ again: budget(),
38728
+ waived: false
38729
+ };
38730
+ let retry;
38731
+ for (;;) {
38732
+ await naivePass(retry);
38733
+ const tip = kept("naive");
38734
+ const outcome = await vetted(tip, vetting) ?? await ruled(tip, vetting.again);
38735
+ if (!("kind" in outcome)) return outcome;
38736
+ retry = outcome.kind === "retry" ? outcome.retry : void 0;
38546
38737
  }
38547
38738
  });
38548
38739
  const seamBuild = async (base) => {
@@ -38580,7 +38771,7 @@ const unwind = () => {
38580
38771
  const commit = head$1();
38581
38772
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
38582
38773
  };
38583
- /** A judged naive pass (none with `flagMode` off), then implement and review until a review round signs off; design must have run since `firstBase`. */
38774
+ /** A judged naive pass (none with `flagMode` off), then implement and the quality lap; design must have run since `firstBase`. */
38584
38775
  const buildFeature = async (firstBase) => {
38585
38776
  const tier = riskTierOn() ? recordRisk() : void 0;
38586
38777
  const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
@@ -38591,18 +38782,31 @@ const buildFeature = async (firstBase) => {
38591
38782
  afterRefactor: seam.refactored
38592
38783
  });
38593
38784
  const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path)) ? firstBase : seam.base;
38594
- await featureFeedback(await buildTail(base, {
38785
+ await tail(settleQuality, {
38595
38786
  green: true,
38596
38787
  lensBase: base
38597
- }), base);
38788
+ });
38598
38789
  });
38599
38790
  };
38791
+ const settleQuality = async () => {
38792
+ let rested = false;
38793
+ while ((read(".gtd/QUALITY.md") ?? "").length > 0) {
38794
+ rested = true;
38795
+ await human("quality-unresolved", {
38796
+ message: qualityUnresolvedMessage(),
38797
+ label: "Quality findings left unresolved",
38798
+ file: QUALITY
38799
+ });
38800
+ }
38801
+ if (rested) await healthy(() => fixCheck());
38802
+ };
38600
38803
  /** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
38601
38804
  const planAndBuild = async (firstBase, stated) => {
38602
38805
  if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
38603
38806
  await buildFeature(firstBase);
38604
38807
  };
38605
- const featureFeedback = async (first, lensBase) => {
38808
+ /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
38809
+ const afterTail = async (first) => {
38606
38810
  let outcome = first;
38607
38811
  while (outcome.verdict === "feedback") {
38608
38812
  await reUnwind(outcome);
@@ -38613,13 +38817,10 @@ const featureFeedback = async (first, lensBase) => {
38613
38817
  });
38614
38818
  outcome = await buildTail(outcome.base, {
38615
38819
  green: true,
38616
- lap: "feedback",
38617
- lensBase
38820
+ lap: "feedback"
38618
38821
  });
38619
38822
  }
38620
38823
  };
38621
- /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
38622
- const afterTail = (outcome) => featureFeedback(outcome);
38623
38824
  /** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
38624
38825
  const ordinaryStart = async () => {
38625
38826
  await human("idle", {
@@ -38823,4 +39024,4 @@ var workflows_default = feature;
38823
39024
  */
38824
39025
  const doors = {};
38825
39026
  //#endregion
38826
- export { ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, 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, implement, 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 };
39027
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "32.1.0",
3
+ "version": "34.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": {
@@ -224,8 +224,8 @@ workflow.
224
224
 
225
225
  In the bundled default, `planAndBuild` runs the design phase, then
226
226
  `buildFeature`: the naive pass and its seam judge, then the implementation and
227
- review. Add a human sign-off before the first build (a review feedback lap
228
- re-runs design inside `buildFeature` and does not pass it again):
227
+ the quality lap, ending with no human review. Add a human sign-off before the
228
+ build:
229
229
 
230
230
  ```ts
231
231
  const planAndBuild = async (firstBase: string): Promise<void> => {
@@ -108,6 +108,10 @@ Given("a file {string} with:", (world: TestkitWorld, path: string, content: stri
108
108
  world.repo().write(path, withNewline(content))
109
109
  })
110
110
 
111
+ Given("the file {string} is deleted", (world: TestkitWorld, path: string) => {
112
+ world.repo().remove(path)
113
+ })
114
+
111
115
  Given("the working tree is committed as {string}", (world: TestkitWorld, message: string) => {
112
116
  world.repo().commit(message)
113
117
  })
@@ -86,6 +86,14 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
86
86
  - AC-1: `greeting` reads "Hello, world".
87
87
  """
88
88
  And step "design.gate.answer" changes nothing
89
+ And step "seam.check" changes nothing
90
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
91
+ """
92
+ it("[AC-1] holds", () => {})
93
+ it("[AC-2] holds", () => {})
94
+ it("[AC-3] holds", () => {})
95
+ it("[AC-7] holds", () => {})
96
+ """
89
97
  And step "seam.naive" writes "src/greeting.ts" with:
90
98
  """
91
99
  export const greeting = "Hello, world"
@@ -2,8 +2,8 @@
2
2
  Feature: a trivial change takes the same few beats at either risk tier, flag-free at low
3
3
 
4
4
  With GTD_FLAG_RISK_TIER on, a one-line copy fix goes
5
- from idle through the naive pass, the seam judge's land, implement and human
6
- review back to idle. Low tier walks exactly the steps high does; only the
5
+ from idle through the naive pass, the seam judge's land, implement and the
6
+ quality lap back to idle. Low tier walks exactly the steps high does; only the
7
7
  implementation prompt and the summary differ.
8
8
 
9
9
  Background:
@@ -27,18 +27,6 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
27
27
  And step "implement.health.check" changes nothing
28
28
  And step "implement.closing" runs its script
29
29
  And step "build.quality.correctness.reviewing" changes nothing
30
- And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
31
- """
32
- # Review: abc1234
33
-
34
- <!-- base: 0000000 -->
35
-
36
- ## greeting
37
-
38
- - [ ] ./src/greeting.ts#1-1
39
- typo fixed
40
- """
41
- And step "build.review.closing" runs its script
42
30
 
43
31
  Scenario Outline: a one-line fix runs idle to idle with the same beats at either tier
44
32
  Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
@@ -57,7 +45,12 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
57
45
  Then the process rests at "seam.naive"
58
46
  And the last commit subject is "gtd(human): design.gate.answer → seam.naive"
59
47
 
60
- Given step "seam.naive" writes "src/greeting.ts" with:
48
+ Given step "seam.check" changes nothing
49
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
50
+ """
51
+ it("[AC-1] holds", () => {})
52
+ """
53
+ And step "seam.naive" writes "src/greeting.ts" with:
61
54
  """
62
55
  export const greeting = "Hello, world"
63
56
  """
@@ -75,21 +68,6 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
75
68
  export const greeting = "Hello, world!"
76
69
  """
77
70
  When the process is driven
78
- Then the process rests at "build.review.await-review"
79
-
80
- Given a file ".gtd/REVIEW.md" with:
81
- """
82
- # Review: abc1234
83
-
84
- <!-- base: 0000000 -->
85
-
86
- ## greeting
87
-
88
- - [x] ./src/greeting.ts#1-1
89
- typo fixed
90
- """
91
- When gtd lands
92
- And the process is driven
93
71
  Then the process rests at "idle"
94
72
  And "src/greeting.ts" contains "Hello, world!"
95
73
  And the commit subjects end with:
@@ -99,15 +77,13 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
99
77
  gtd(check): start-gate.check → design.triage
100
78
  gtd(agent): design.triage → design.gate.answer
101
79
  gtd(human): design.gate.answer → seam.naive
102
- gtd(agent): seam.naive → seam.judge
80
+ gtd(agent): seam.naive → seam.check
81
+ gtd(check): seam.check → seam.judge
103
82
  gtd(judge): seam.judge → implement.building
104
83
  gtd(agent): implement.building → implement.health.check
105
84
  gtd(check): implement.health.check → implement.closing
106
85
  gtd(check): implement.closing → build.quality.correctness.reviewing
107
- gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
108
- gtd(agent): build.review.reviewing → build.review.await-review
109
- gtd(human): build.review.await-review → build.review.closing
110
- gtd(check): build.review.closing → idle
86
+ gtd(agent): build.quality.correctness.reviewing → idle
111
87
  """
112
88
  And the output of gtd "summary" <summary> "needs a human reviewer"
113
89