@pmelab/gtd 45.2.0 → 47.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 (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -30
  3. package/dist/chunks/{reserved-FWrjy7zo.mjs → reserved-DF1HD6jL.mjs} +42 -48
  4. package/dist/chunks/{review-Dhl4vvDe.mjs → review-Bv40kZu9.mjs} +9 -23
  5. package/dist/chunks/{tail-B-Es6Hwj.mjs → tail-D89fU_wf.mjs} +68 -39
  6. package/dist/gtd.bundle.mjs +5 -7
  7. package/dist/testkit.mjs +5 -7
  8. package/dist/workflows/cleanup.mjs +2 -2
  9. package/dist/workflows/feature.mjs +26 -22
  10. package/dist/workflows/fix.mjs +2 -2
  11. package/dist/workflows/review.mjs +14 -4
  12. package/dist/workflows/rollout.mjs +1 -1
  13. package/dist/workflows.mjs +270 -429
  14. package/package.json +1 -6
  15. package/src/install/ConfigScaffold.ts +1 -2
  16. package/src/install/Install.ts +4 -5
  17. package/src/workflows/cleanup/cleanup.test.ts +11 -9
  18. package/src/workflows/feature/feature.ts +38 -28
  19. package/src/workflows/feature/lens-review.feature +3 -1
  20. package/src/workflows/feature/slice-findings.feature +198 -0
  21. package/src/workflows/fix/fix-lens-origin.feature +101 -0
  22. package/src/workflows/fix/fix-quality.feature +131 -0
  23. package/src/workflows/index.ts +1 -3
  24. package/src/workflows/review/review.ts +15 -3
  25. package/src/workflows/shared/lenses.ts +30 -9
  26. package/src/workflows/shared/options.ts +1 -21
  27. package/src/workflows/shared/review.ts +5 -27
  28. package/src/workflows/shared/skills.test.ts +0 -2
  29. package/src/workflows/shared/skills.ts +0 -3
  30. package/src/workflows/shared/tail.ts +74 -57
  31. package/src/workflows/shared/text.ts +86 -33
  32. package/src/workflows/shared/vars.ts +0 -8
  33. package/dist/workflows/tests.mjs +0 -193
  34. package/src/workflows/feature/quality-unresolved.feature +0 -121
  35. package/src/workflows/tests/index.ts +0 -6
  36. package/src/workflows/tests/tests-options.feature +0 -52
  37. package/src/workflows/tests/tests.feature +0 -266
  38. package/src/workflows/tests/tests.test.ts +0 -51
  39. package/src/workflows/tests/tests.ts +0 -270
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "45.2.0",
3
+ "version": "47.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
@@ -504,12 +504,12 @@ noted in steps 3 and 4 below.
504
504
  unsure verdict (below `qualityTriageMinP`, `0.7`) is `design`; mechanical
505
505
  findings the fix turns leave become notes too. This lap is where code quality
506
506
  is looked at, and every round pays for it. It never replaces step 4 — your
507
- review stays the final gate, and nothing here skips it. Between
508
- [coarse slices](#coarse-slices) no review follows, so there the fix turn
509
- fixes every finding, and those the fix cap leaves rest at
510
- `build.quality-unresolved` until you delete or empty `.gtd/QUALITY.md`; the
511
- suite then runs on any hand edits before the next slice. The `fix` door
512
- (below) repairs a red baseline through the untriaged lap.
507
+ review stays the final gate, and nothing here skips it. Every
508
+ [coarse slice](docs/configuration.md#coarse-slices) runs the same triaged lap
509
+ and never rests for its findings: its notes are carried to the review after
510
+ the last slice, each naming its slice and lens, beside every slice's fixes.
511
+ The `fix` and `cleanup` doors triage their lap too, and hand its notes to
512
+ their own review.
513
513
 
514
514
  A red suite that keeps failing past a few fix attempts escalates instead of
515
515
  retrying forever: an agent turn reads the failing output and writes
@@ -741,21 +741,6 @@ hand or leave it for cleanup.
741
741
 
742
742
  The delivery model these workflows implement: [Delivery model](SDLC.md).
743
743
 
744
- ### Reviving skipped tests
745
-
746
- The opt-in `tests` workflow revisits tests nobody runs. Export `tests` from
747
- `@pmelab/gtd/workflows/tests` and run `gtd door tests` (or
748
- `gtd --workflow tests`). On a green baseline an agent sweeps the whole
749
- repository — every skipped test plus the `test-audit` skill's findings — into
750
- `.gtd/TESTS.md`, one section per test file. Each file is then rewritten in its
751
- own turn until nothing in it is skipped and the suite is green. A test the agent
752
- calls obsolete stays skipped and is proposed for deletion: the review in
753
- `.gtd/REVIEW.md` gives each proposal `delete it`, `keep it skipped` or
754
- `_your answer_`, and refuses sign-off until each holds one tick. An agent turn
755
- that deletes a test file is refused. Skip detection is a factory option with
756
- JavaScript/TypeScript and cucumber defaults; see
757
- [Reviving skipped tests](docs/configuration.md#reviving-skipped-tests).
758
-
759
744
  ## Checking a range for stray files
760
745
 
761
746
  ```sh
@@ -894,15 +879,15 @@ approvals as `approvals` (`[]` when none), and `gtd history show` prints each as
894
879
  gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
895
880
  [SemVer promise](https://github.com/pmelab/gtd/blob/main/COMPATIBILITY.md).
896
881
 
897
- | Import | What it is |
898
- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
899
- | `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), `scope` and `slice` (named spans of a process), workflow factories, `defineConfig` and `FLOWS_PROTOCOL`. |
900
- | `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
901
- | `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`) and the review-note readers. |
902
- | `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. |
903
- | `@pmelab/gtd/workflows` | Every shipped workflow and all its parts in one module; also resolvable as `@pmelab/gtd/workflow`. |
904
- | `@pmelab/gtd/workflows/feature`, `@pmelab/gtd/workflows/review`, `@pmelab/gtd/workflows/fix`, `@pmelab/gtd/workflows/rollout`, `@pmelab/gtd/workflows/cleanup`, `@pmelab/gtd/workflows/tests` | One shipped workflow each: its factory as the default export, its parts, and the reserved exports (`defaults`, `envDefaults`, `summary`, `steering`, `skills`, `access`). `gtd install` imports these. |
905
- | `@pmelab/gtd/testkit` | Runs any workflow against the real engine, in an in-memory or a real repository. Needs `vitest` and `quickpickle`, optional peer dependencies. |
882
+ | Import | What it is |
883
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
884
+ | `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), `scope` and `slice` (named spans of a process), workflow factories, `defineConfig` and `FLOWS_PROTOCOL`. |
885
+ | `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
886
+ | `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`) and the review-note readers. |
887
+ | `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. |
888
+ | `@pmelab/gtd/workflows` | Every shipped workflow and all its parts in one module; also resolvable as `@pmelab/gtd/workflow`. |
889
+ | `@pmelab/gtd/workflows/feature`, `@pmelab/gtd/workflows/review`, `@pmelab/gtd/workflows/fix`, `@pmelab/gtd/workflows/rollout`, `@pmelab/gtd/workflows/cleanup` | One shipped workflow each: its factory as the default export, its parts, and the reserved exports (`defaults`, `envDefaults`, `summary`, `steering`, `skills`, `access`). `gtd install` imports these. |
890
+ | `@pmelab/gtd/testkit` | Runs any workflow against the real engine, in an in-memory or a real repository. Needs `vitest` and `quickpickle`, optional peer dependencies. |
906
891
 
907
892
  Types come from the TypeScript sources the package ships. Configs never need an
908
893
  install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
@@ -1,6 +1,6 @@
1
1
  import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, mark, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
2
2
  import { d as ANSWER_FIX, f as ANSWER_LEAD, p as ANSWER_WONT_FIX, v as FREE_TEXT_PLACEHOLDER } from "./steering-D7hWeMgE.mjs";
3
- import { escalation as escalation$1, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
3
+ import { gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
4
4
  import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$, s as Number$ } from "./Schema-C6fLlQzx.mjs";
5
5
  //#region src/workflows/shared/registry.ts
6
6
  const FILL = "";
@@ -226,11 +226,6 @@ const testFileGlobs = [
226
226
  "**/__tests__/**"
227
227
  ];
228
228
  const defaultFlagRegistry = "src/flags/index.ts";
229
- /** The `tests` workflow's own process settings: where it looks for skipped tests, and how a skip reads. */
230
- const testsDefaults = {
231
- testFiles: testFileGlobs.join(", "),
232
- skipPattern: "^\\s*(?:(?:describe|context|suite|it|test|specify)\\.(?:skip|todo)\\b|x(?:describe|context|it|test|specify)\\s*\\(|(?:@\\S+\\s+)*@(?:skip|ignore)(?:\\s|$))"
233
- };
234
229
  /** Process settings. */
235
230
  const defaults = {
236
231
  reviewBase: "",
@@ -549,6 +544,20 @@ const SUMMARY_TITLES = [
549
544
  ];
550
545
  const unboxed = (body) => body.replace(/^(\s*(?:>\s*)*(?:[-*+]|\d+[.)])\s+)\[[ xX]\]\s?/gm, "$1");
551
546
  const short = (hash) => hash.slice(0, 7);
547
+ /** `slice 2, lens tests`; empty when neither is known. */
548
+ const originOf = (f) => [...f.slice === void 0 ? [] : [`slice ${f.slice}`], ...f.lens === void 0 ? [] : [`lens ${f.lens}`]].join(", ");
549
+ const originLine = (f) => {
550
+ const origin = originOf(f);
551
+ return origin === "" ? "" : `_From ${origin}._\n\n`;
552
+ };
553
+ const withOrigin = (f, rest) => [rest, originOf(f)].filter((s) => s !== "").join(", ");
554
+ const noteBody = (n) => {
555
+ const body = unboxed(n.body) || "_No detail given._";
556
+ if (n.unresolved === true) return `_The fixer left this unresolved._\n\n${body}`;
557
+ if (n.unclear === true) return `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${body}`;
558
+ return body;
559
+ };
560
+ const carriedOnly = (f) => f.slice === void 0 ? {} : f;
552
561
  /**
553
562
  * \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
554
563
  * answer boxes, then the fixed and noise lists. Chunk titles are made unique —
@@ -563,7 +572,7 @@ const composedReview = (base, report) => {
563
572
  return name;
564
573
  };
565
574
  const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
566
- const answerBoxes = ([fix, wontFix] = [ANSWER_FIX, ANSWER_WONT_FIX]) => `${ANSWER_LEAD}\n\n- [ ] ${fix}\n- [ ] ${wontFix}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
575
+ const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
567
576
  const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
568
577
  const added = report.standards?.added ?? [];
569
578
  const notes = report.notes.map((n) => ({
@@ -575,25 +584,14 @@ const composedReview = (base, report) => {
575
584
  ...report.nitFix === void 0 ? [] : [chunk("Fixed from your notes", `Your nits and fix-as-suggested notes were worked on in ${short(report.nitFix)}; the notes you answered are left out — check the diff.`)],
576
585
  ...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
577
586
  ...conflicts,
578
- ...notes.map((n) => chunk(n.title, `${n.unresolved === true ? `_The fixer left this unresolved._\n\n${unboxed(n.body)}` : n.unclear === true ? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}` : unboxed(n.body) || "_No detail given._"}\n\n${answerBoxes(n.answers)}`)),
579
- ...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
587
+ ...notes.map((n) => chunk(n.title, `${originLine(carriedOnly(n))}${noteBody(n)}\n\n${answerBoxes}`)),
588
+ ...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => fixedLine(f, carriedOnly(f))).join("\n"))],
580
589
  ...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
581
- ...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
590
+ ...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => noiseLine(f, carriedOnly(f))).join("\n"))]
582
591
  ];
583
592
  const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
584
593
  return `# Review: ${short(base)}\n\n<!-- base: ${base} -->\n\n${body.join("\n\n")}\n`;
585
594
  };
586
- const qualityUnresolvedMessage = () => `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
587
- holds findings. Nothing after this fixes them, so the process will not
588
- go on with them open.
589
-
590
- Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
591
- dropping a finding is your call to make.
592
-
593
- What each change does next (then run \`gtd land\`):
594
- - **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the next slice is built, or after the last one you review the change (**build.review.reviewing**).
595
- - **Anything else** — rests here again.
596
- `;
597
595
  const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
598
596
  \`.gtd/FEEDBACK.md\` holds the failing output.
599
597
 
@@ -1259,6 +1257,28 @@ const lapRange = {
1259
1257
  feedback: "it starts at the previous review's boundary, so it covers only what is new since the previous review.",
1260
1258
  refactor: "it covers the whole refactor since it started, on every round. A refactor may update an existing test's call sites only: give any test-file hunk its own chunk, and say so if it changes a test, an assertion or an expected value."
1261
1259
  };
1260
+ const fixedLine = (f, origin = f) => `- ${f.title} (${withOrigin(origin, short(f.commit))})`;
1261
+ const noiseLine = (f, origin = f) => `- ${f.title} (${withOrigin(origin, `p=${f.p}`)})`;
1262
+ /**
1263
+ * `review` with the lap's notes appended as prose chunks — no pointer, no
1264
+ * answer boxes — then what it fixed and dropped. A title the review already
1265
+ * holds gets a numbered suffix, as notes are told apart by title.
1266
+ */
1267
+ const withLapFindings = (review, { notes, fixed, noise }) => {
1268
+ const used = new Set(review.split("\n").filter((line) => line.startsWith("## ")).map((line) => line.slice(3).trim()));
1269
+ const unique = (title) => {
1270
+ let name = title;
1271
+ for (let n = 2; used.has(name); n++) name = `${title} (${n})`;
1272
+ used.add(name);
1273
+ return name;
1274
+ };
1275
+ const chunks = [
1276
+ ...notes.map((n) => `## ${unique(n.title)}\n\n${originLine(n)}${noteBody(n)}`),
1277
+ ...fixed.length === 0 ? [] : [`## ${unique("Fixed automatically")}\n\n${fixed.map((f) => fixedLine(f)).join("\n")}`],
1278
+ ...noise.length === 0 ? [] : [`## ${unique("Dropped as noise")}\n\n${noise.map((f) => noiseLine(f)).join("\n")}`]
1279
+ ];
1280
+ return chunks.length === 0 ? review : `${review.trimEnd()}\n\n${chunks.join("\n\n")}\n`;
1281
+ };
1262
1282
  const buildReviewReviewingPrompt = (base, lap, carry, removed = []) => `${styleBlock}
1263
1283
 
1264
1284
  ${styleFormatContract}
@@ -1803,12 +1823,6 @@ const guardedSteps = (guard, describeGuard, rests = true, rules) => {
1803
1823
  exhausted: guarded(steps.exhausted, rests ? guard : void 0)
1804
1824
  };
1805
1825
  };
1806
- /**
1807
- * Hand a red suite that repeated attempts could not fix to a person: an
1808
- * agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
1809
- * on, the human is told the budget is spent instead.
1810
- */
1811
- const escalation = (count, guard, guardRests = true, rules) => escalation$1(count, guardedSteps(guard, void 0, guardRests, rules));
1812
1826
  /** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
1813
1827
  const retryQuestion = (comparable) => comparable ? {
1814
1828
  id: "verdict",
@@ -1944,24 +1958,6 @@ const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions,
1944
1958
  base: void 0
1945
1959
  };
1946
1960
  });
1947
- const TestsOptions = Struct({
1948
- ...ShippedOptions.fields,
1949
- testFiles: setting,
1950
- skipPattern: setting
1951
- });
1952
- const testsShipped = (flow) => defineWorkflow(standardSchemaV1(TestsOptions, {
1953
- onExcessProperty: "error",
1954
- errors: "all"
1955
- }), ({ contextLimit, ...options }) => ({
1956
- flow: limited(flow, contextLimit),
1957
- defaults: over({
1958
- ...defaults,
1959
- ...testsDefaults
1960
- }, options),
1961
- envDefaults: over(envDefaults, options),
1962
- doors: { tests: {} },
1963
- base: void 0
1964
- }));
1965
1961
  //#endregion
1966
1962
  //#region src/workflows/shared/skills.ts
1967
1963
  const skills = (vars) => {
@@ -1977,8 +1973,6 @@ const skills = (vars) => {
1977
1973
  design: ["spec-driven-development", "planning-and-task-breakdown"],
1978
1974
  notes: ["incremental-implementation", "code-simplification"],
1979
1975
  feedback: ["test-driven-development", "incremental-implementation"],
1980
- tests: ["test-audit", "test-driven-development"],
1981
- "tests.health": ["debugging-and-error-recovery"],
1982
1976
  ...build,
1983
1977
  ...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
1984
1978
  refactor: ["ponytail"],
@@ -2011,4 +2005,4 @@ const steering = {
2011
2005
  [REVIEW]: "review"
2012
2006
  };
2013
2007
  //#endregion
2014
- export { implementFixPrompt as $, planner as A, conflictBody as At, composedReview as B, flipProblems as Bt, awaitReview as C, unwindFailedMessage as Ct, fixNits as D, testFileGlobs as Dt, fixCheck as E, envDefaults as Et, buildReviewCollectingPrompt as F, parseStandards as Ft, designGateAnswerMessage as G, coverageQuestion as H, requireFlag as Ht, buildReviewTriageMessage as I, renderStandards as It, finisherSystem as J, designSystem as K, builderSystem as L, standardKey as Lt, reviewQuality as M, conflictTitle as Mt, reviewing as N, isConflictTitle as Nt, fixQuality as O, testsDefaults as Ot, agentWithSkills as P, oneLine as Pt, implementBuildingPrompt as Q, clearCriteriaMessage as R, flagMentions as Rt, answerReviewQuestions as S, uncoveredMessage as St, collecting as T, defaults as Tt, coveringPrompt as U, coverageMessage as V, registryEntry as Vt, criterionLine as W, handOffAwaitReviewMessage as X, fixReviewNotesPrompt as Y, idleMessage as Z, REFACTOR as _, reviewerSystem as _t, flagModeOn as a, qualityTriageMessage as at, REVIEW as b, slicingPrompt as bt, testsShipped as c, refactorDroppedRefusal as ct, gate as d, refactorRules as dt, naiveApplyFailure as et, guarded as f, reviewEditNotesCapture as ft, QUALITY as g, reviewNotesCapture as gt, HANDOFF as h, reviewGateBlockedMessage as ht, skills as i, qualityFindingsFile as it, reviewMissing as j, conflictFor as jt, fixRisks as k, STANDARDS as kt, baseline as l, refactorFixPrompt as lt, FEEDBACK as m, reviewFeedbackFixPrompt as mt, summary as n, normaliseCriteriaScript as nt, flagShipped as o, qualityUnresolvedMessage as ot, healthy as p, reviewEditsCapture as pt, designTriagePrompt as q, access as r, oversizedMessage as rt, shipped as s, refactorBuildingPrompt as st, steering as t, naivePassPrompt as tt, escalation as u, refactorNotePrompt as ut, REFACTOR_NOTE as v, seamByHandMessage as vt, coder as w, unwindFailure as wt, SLICES as x, startGateBlockedMessage as xt, REQUIREMENTS as y, seamSteerMessage as yt, clearCriteriaQuestion as z, flagReferences as zt };
2008
+ export { naivePassPrompt as $, reviewQuality as A, isConflictTitle as At, coverageQuestion as B, collecting as C, defaults as Ct, fixRisks as D, conflictBody as Dt, fixQuality as E, STANDARDS as Et, builderSystem as F, flagMentions as Ft, designTriagePrompt as G, criterionLine as H, clearCriteriaMessage as I, flagReferences as It, handOffAwaitReviewMessage as J, finisherSystem as K, clearCriteriaQuestion as L, flipProblems as Lt, agentWithSkills as M, parseStandards as Mt, buildReviewCollectingPrompt as N, renderStandards as Nt, planner as O, conflictFor as Ot, buildReviewTriageMessage as P, standardKey as Pt, naiveApplyFailure as Q, composedReview as R, registryEntry as Rt, coder as S, withLapFindings as St, fixNits as T, testFileGlobs as Tt, designGateAnswerMessage as U, coveringPrompt as V, designSystem as W, implementBuildingPrompt as X, idleMessage as Y, implementFixPrompt as Z, REQUIREMENTS as _, slicingPrompt as _t, flagModeOn as a, refactorDroppedRefusal as at, answerReviewQuestions as b, unwindFailedMessage as bt, baseline as c, refactorRules as ct, healthy as d, reviewFeedbackFixPrompt as dt, normaliseCriteriaScript as et, FEEDBACK as f, reviewGateBlockedMessage as ft, REFACTOR_NOTE as g, seamSteerMessage as gt, REFACTOR as h, seamByHandMessage as ht, skills as i, refactorBuildingPrompt as it, reviewing as j, oneLine as jt, reviewMissing as k, conflictTitle as kt, gate as l, reviewEditNotesCapture as lt, QUALITY as m, reviewerSystem as mt, summary as n, qualityFindingsFile as nt, flagShipped as o, refactorFixPrompt as ot, HANDOFF as p, reviewNotesCapture as pt, fixReviewNotesPrompt as q, access as r, qualityTriageMessage as rt, shipped as s, refactorNotePrompt as st, steering as t, oversizedMessage as tt, guarded as u, reviewEditsCapture as ut, REVIEW as v, startGateBlockedMessage as vt, fixCheck as w, envDefaults as wt, awaitReview as x, unwindFailure as xt, SLICES as y, uncoveredMessage as yt, coverageMessage as z, requireFlag as zt };
@@ -1,7 +1,7 @@
1
- import { answered, changesSince, head, judge, numeric, read, removeScript, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
1
+ import { answered, changesSince, head, judge, numeric, read, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
2
2
  import { removedAssertions, review } from "../workflow-kit.mjs";
3
- import { At as conflictBody, B as composedReview, C as awaitReview, D as fixNits, E as fixCheck, Ft as parseStandards, I as buildReviewTriageMessage, It as renderStandards, J as finisherSystem, Lt as standardKey, Mt as conflictTitle, Nt as isConflictTitle, P as agentWithSkills, Pt as oneLine, S as answerReviewQuestions, T as collecting, b as REVIEW, f as guarded, ft as reviewEditNotesCapture, g as QUALITY, gt as reviewNotesCapture, j as reviewMissing, jt as conflictFor, k as fixRisks, kt as STANDARDS, mt as reviewFeedbackFixPrompt, p as healthy, pt as reviewEditsCapture, w as coder, y as REQUIREMENTS } from "./reserved-FWrjy7zo.mjs";
4
- import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-B-Es6Hwj.mjs";
3
+ import { At as isConflictTitle, C as collecting, D as fixRisks, Dt as conflictBody, Et as STANDARDS, Mt as parseStandards, Nt as renderStandards, Ot as conflictFor, P as buildReviewTriageMessage, Pt as standardKey, R as composedReview, T as fixNits, b as answerReviewQuestions, d as healthy, jt as oneLine, k as reviewMissing, kt as conflictTitle, lt as reviewEditNotesCapture, pt as reviewNotesCapture, u as guarded, ut as reviewEditsCapture, v as REVIEW, w as fixCheck, x as awaitReview } from "./reserved-DF1HD6jL.mjs";
4
+ import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-D89fU_wf.mjs";
5
5
  //#region src/workflows/shared/learn.ts
6
6
  const SUMMARY_MAX = 200;
7
7
  const BANNER = /^_(?:The fixer left this unresolved\.|A fix turn removed this while renaming another finding — check whether it was fixed\.|No detail given\.)_\s*/;
@@ -276,7 +276,7 @@ const triage = (severities) => async (notes) => {
276
276
  const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
277
277
  /**
278
278
  * gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
279
- * turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
279
+ * turn. A re-review
280
280
  * after fixed nits restores the doc as committed at `carry`, answers kept;
281
281
  * a fresh one leaves out the notes the human already answered.
282
282
  */
@@ -289,7 +289,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
289
289
  added: standardsAddedSince(base),
290
290
  undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
291
291
  }
292
- }), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
292
+ })) : showScript(carry, REVIEW, REVIEW), {
293
293
  label: "Composing the review",
294
294
  file: REVIEW,
295
295
  base
@@ -303,7 +303,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
303
303
  * praise dropped.
304
304
  */
305
305
  const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
306
- const { guard, guardRests, fixAccess, rules, awaitMessage } = options;
306
+ const { guard, guardRests, fixAccess, rules } = options;
307
307
  const feedback = options.feedback ?? {
308
308
  file: ".gtd/REQUIREMENTS.md",
309
309
  collecting,
@@ -328,7 +328,7 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
328
328
  guardRests,
329
329
  rules
330
330
  }),
331
- awaitReview: (b) => awaitReview(b, awaitMessage?.(b)),
331
+ awaitReview,
332
332
  reviewMissing,
333
333
  collecting: feedback.collecting,
334
334
  triage: triage(severities),
@@ -371,10 +371,7 @@ function buildTail(first, second, ...rest) {
371
371
  return tail((escalations, report) => humanReview(base, escalations, {
372
372
  ...form,
373
373
  report
374
- }), {
375
- ...form,
376
- triage: true
377
- });
374
+ }), form);
378
375
  }
379
376
  const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
380
377
  const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
@@ -388,17 +385,6 @@ const callForm = (first, second, [third, fourth, ...extra]) => {
388
385
  if (typeof second === "string") return legacyForm(first, second, third, fourth);
389
386
  if (typeof first === "string") return currentForm(first, second, third, fourth);
390
387
  };
391
- /** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
392
- const fixFeedback = async () => {
393
- await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
394
- label: "Fixing your review feedback",
395
- file: REQUIREMENTS,
396
- model: coder(),
397
- system: finisherSystem(),
398
- allowEmpty: true
399
- });
400
- await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
401
- };
402
388
  /**
403
389
  * Undo the human's review-round code edits. A path changed since the human
404
390
  * left it is not overwritten; requireRevert names it instead.
@@ -416,4 +402,4 @@ const reUnwind = async (feedback) => {
416
402
  requireRevert(edited, base);
417
403
  };
418
404
  //#endregion
419
- export { reUnwind as a, isCode as i, fixFeedback as n, humanReview as r, buildTail as t };
405
+ export { reUnwind as i, humanReview as n, isCode as r, buildTail as t };
@@ -1,12 +1,24 @@
1
1
  import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
2
- import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { C as awaitReview, E as fixCheck, M as reviewQuality, N as reviewing, O as fixQuality, X as handOffAwaitReviewMessage, at as qualityTriageMessage, b as REVIEW, f as guarded, g as QUALITY, it as qualityFindingsFile, j as reviewMissing, k as fixRisks, p as healthy, u as escalation } from "./reserved-FWrjy7zo.mjs";
2
+ import { lensEntriesOf, lensOf, qualityLap, reviewToHandOff } from "../workflow-kit.mjs";
3
+ import { A as reviewQuality, D as fixRisks, E as fixQuality, J as handOffAwaitReviewMessage, St as withLapFindings, d as healthy, j as reviewing, k as reviewMissing, m as QUALITY, nt as qualityFindingsFile, rt as qualityTriageMessage, u as guarded, v as REVIEW, w as fixCheck, x as awaitReview } from "./reserved-DF1HD6jL.mjs";
4
4
  //#region src/workflows/shared/lenses.ts
5
5
  const NO_FINDINGS = {
6
6
  notes: [],
7
7
  fixed: [],
8
8
  noise: []
9
9
  };
10
+ /** `reports` as one, each finding labelled with the slice it came from, in slice order. */
11
+ const joinReports = (reports) => {
12
+ const tag = (slice, fs) => fs.map((f) => ({
13
+ ...f,
14
+ slice
15
+ }));
16
+ return {
17
+ notes: reports.flatMap((r) => tag(r.slice, r.report.notes)),
18
+ fixed: reports.flatMap((r) => tag(r.slice, r.report.fixed)),
19
+ noise: reports.flatMap((r) => tag(r.slice, r.report.noise))
20
+ };
21
+ };
10
22
  const UNTITLED = "Quality findings";
11
23
  const below = (section) => section.split("\n").slice(1).join("\n").trim();
12
24
  const strayProse = (text, sections) => sections.reduce((rest, body) => rest.replace(body, "\n"), text).split("\n").filter((line) => !/^#\s/.test(line)).join("\n").trim();
@@ -86,7 +98,14 @@ const classify = async (findings) => {
86
98
  */
87
99
  const triageFindings = async (options) => {
88
100
  const { escalations, guard, guardRests, rules } = options;
89
- const findings = findingsOf(read(".gtd/QUALITY.md") ?? "");
101
+ const withLens = (f) => {
102
+ const lens = options.lensOf?.(f);
103
+ return lens === void 0 ? f : {
104
+ ...f,
105
+ lens
106
+ };
107
+ };
108
+ const findings = findingsOf(read(".gtd/QUALITY.md") ?? "").map(withLens);
90
109
  if (findings.length === 0) return NO_FINDINGS;
91
110
  const { mechanical, noise, design } = await classify(findings);
92
111
  const sorted = qualityFindingsFile(mechanical);
@@ -103,7 +122,10 @@ const triageFindings = async (options) => {
103
122
  commit
104
123
  })));
105
124
  unclear.push(...turn.unclear);
106
- open = turn.open;
125
+ open = turn.open.map((f) => ({
126
+ ...mechanical.find((m) => m.title === f.title),
127
+ ...f
128
+ }));
107
129
  }
108
130
  if (mechanical.length > 0) await healthy(() => fixCheck(rules), {
109
131
  escalations,
@@ -131,22 +153,15 @@ const triageFindings = async (options) => {
131
153
  //#region src/workflows/shared/tail.ts
132
154
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
133
155
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
134
- /**
135
- * One review turn per lens over the whole change, each appending every
136
- * finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
137
- * has any.
138
- */
139
- const qualityLap = (guard, lenses, base) => qualityLap$1(lenses ?? lensEntriesOf(vars.qualityReviews ?? ""), guarded((entry) => {
156
+ const lensLap = (guard, lenses, base, seen) => qualityLap(lenses ?? lensEntriesOf(vars.qualityReviews ?? ""), guarded(async (entry) => {
140
157
  const { lens, model } = lensOf(entry);
141
- return reviewQuality(lens, base, model);
158
+ await reviewQuality(lens, base, model);
159
+ seen(lens, findingsOf(read(".gtd/QUALITY.md") ?? ""));
142
160
  }, guard), QUALITY);
143
- /** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
144
- const fixQualityFindings = (escalations, guard, guardRests = true, rules) => fixFindings({
145
- fix: guarded(() => fixQuality(rules), guard),
146
- findings: QUALITY,
147
- cap: 3,
148
- escalate: () => escalation(escalations, guard, guardRests, rules)
149
- });
161
+ const closeQuality = async () => {
162
+ if (read(".gtd/QUALITY.md") === void 0) return;
163
+ await run("quality.closing", removeScript([QUALITY]), { label: "Clearing quality findings" });
164
+ };
150
165
  /** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
151
166
  const closeReview = (round) => run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
152
167
  label: "Closing the review",
@@ -183,7 +198,7 @@ const runTail = (finish, options) => scope("build", async () => {
183
198
  const guardRests = options.guardRests === void 0 ? true : options.guardRests;
184
199
  const escalations = { rounds: 0 };
185
200
  const fix = () => fixCheck(rules);
186
- const repair = async () => {
201
+ if (options.fixFirst) {
187
202
  await guarded(fix, guard)();
188
203
  await healthy(fix, {
189
204
  fixesSoFar: 1,
@@ -192,38 +207,52 @@ const runTail = (finish, options) => scope("build", async () => {
192
207
  guardRests,
193
208
  rules
194
209
  });
195
- };
196
- if (options.fixFirst) await repair();
197
- else if (!options.green) await healthy(fix, {
210
+ } else if (!options.green) await healthy(fix, {
198
211
  escalations,
199
212
  guard,
200
213
  guardRests,
201
214
  rules
202
215
  });
203
- const lap = await qualityLap(guard, lenses, lensBase);
204
- if (options.triage) return finish(escalations, lap === "findings" ? await triageFindings({
205
- escalations,
206
- guard,
207
- guardRests,
208
- rules
209
- }) : NO_FINDINGS);
210
- if (lap === "findings") if (await fixQualityFindings(escalations, guard, guardRests, rules)) await healthy(fix, {
216
+ const byTitle = [];
217
+ const lap = await lensLap(guard, lenses, lensBase, (lens, findings) => {
218
+ const before = byTitle.map((f) => f.title);
219
+ for (const { title } of findings) {
220
+ const at = before.indexOf(title);
221
+ if (at >= 0) before.splice(at, 1);
222
+ else byTitle.push({
223
+ title,
224
+ lens
225
+ });
226
+ }
227
+ });
228
+ const lensOf = (f) => {
229
+ const at = byTitle.findIndex((e) => e.title === f.title);
230
+ return at < 0 ? void 0 : byTitle.splice(at, 1)[0].lens;
231
+ };
232
+ const report = lap === "findings" ? await triageFindings({
211
233
  escalations,
212
234
  guard,
213
235
  guardRests,
214
- rules
215
- });
216
- else await repair();
217
- return finish(escalations, NO_FINDINGS);
236
+ rules,
237
+ lensOf
238
+ }) : NO_FINDINGS;
239
+ await closeQuality();
240
+ return finish(escalations, report);
218
241
  });
219
242
  /**
220
243
  * The fix workflow's human review since `base`: a sign-off closes
221
244
  * `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
222
245
  * `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
223
246
  */
224
- const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
247
+ const handOffReview = (base, escalations, guard, report = NO_FINDINGS) => reviewToHandOff(base, {
225
248
  file: REVIEW,
226
- reviewing: guarded((b, carry) => reviewing(b, carry), guard),
249
+ reviewing: guarded(async (b, carry) => {
250
+ await reviewing(b, carry);
251
+ const review = read(REVIEW);
252
+ if (review === void 0) return;
253
+ const full = withLapFindings(review, report);
254
+ if (full !== review) await run("review.carrying", writeScript(REVIEW, full), { label: "Adding the quality lap's findings" });
255
+ }, guard),
227
256
  fixRisks: guarded(fixRisks, guard),
228
257
  keepGreen: () => healthy(fixCheck, {
229
258
  escalations,
@@ -237,10 +266,10 @@ const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
237
266
  unparsed: "gtd land: review-doc: your edit to .gtd/REVIEW.md contains no note fix can read — restore the deleted chunk, or write the note as a line under its entry.",
238
267
  deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
239
268
  });
240
- /** The tail the fix workflow runs: ends at the hand-off review since `base`. */
241
- const handOffTail = (fixFirst, base, guard) => tail((escalations) => handOffReview(base, escalations, guard), {
269
+ /** The tail the fix workflow runs: ends at the hand-off review since `base`, which gets what the lap left. */
270
+ const handOffTail = (fixFirst, base, guard) => tail((escalations, report) => handOffReview(base, escalations, guard, report), {
242
271
  fixFirst,
243
272
  guard
244
273
  });
245
274
  //#endregion
246
- export { refusePendingHandOff as a, isOptionalObject as i, closeReview as n, tail as o, handOffTail as r, NO_FINDINGS as s, REVIEW_ORIG as t };
275
+ export { refusePendingHandOff as a, joinReports as c, isOptionalObject as i, closeReview as n, tail as o, handOffTail as r, NO_FINDINGS as s, REVIEW_ORIG as t };
@@ -21606,11 +21606,10 @@ rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
21606
21606
  straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
21607
21607
  \`gtd doors\` lists the doors a repository offers (those of the workflows its
21608
21608
  config exports — the bundled \`fix\` and \`review [base]\`, plus the opt-in
21609
- \`rollout <flag>\` (turn a release flag default-on), \`cleanup <flag>\`
21610
- (remove it) and \`tests\` (revive skipped tests), which a config adds by
21611
- exporting \`rollout\` from \`@pmelab/gtd/workflows/rollout\`, \`cleanup\` from
21612
- \`@pmelab/gtd/workflows/cleanup\` and \`tests\` from
21613
- \`@pmelab/gtd/workflows/tests\` — plus any it declares). Refuses with
21609
+ \`rollout <flag>\` (turn a release flag default-on) and \`cleanup <flag>\`
21610
+ (remove it), which a config adds by exporting \`rollout\` from
21611
+ \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
21612
+ \`@pmelab/gtd/workflows/cleanup\` — plus any it declares). Refuses with
21614
21613
  \`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
21615
21614
  missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
21616
21615
 
@@ -21723,8 +21722,7 @@ const CONFIG_SCAFFOLD = `// The workflows gtd runs here: gtd's shipped feature,
21723
21722
  // its own doors: \`gtd door review [base]\` and \`gtd door fix\`. Export \`rollout\`
21724
21723
  // from \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
21725
21724
  // \`@pmelab/gtd/workflows/cleanup\` to add \`gtd door rollout <flag>\` and
21726
- // \`gtd door cleanup <flag>\`, and \`tests\` from \`@pmelab/gtd/workflows/tests\`
21727
- // to add \`gtd door tests\`.
21725
+ // \`gtd door cleanup <flag>\`.
21728
21726
  export {
21729
21727
  feature as default,
21730
21728
  feature,
package/dist/testkit.mjs CHANGED
@@ -6643,11 +6643,10 @@ rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
6643
6643
  straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
6644
6644
  \`gtd doors\` lists the doors a repository offers (those of the workflows its
6645
6645
  config exports — the bundled \`fix\` and \`review [base]\`, plus the opt-in
6646
- \`rollout <flag>\` (turn a release flag default-on), \`cleanup <flag>\`
6647
- (remove it) and \`tests\` (revive skipped tests), which a config adds by
6648
- exporting \`rollout\` from \`@pmelab/gtd/workflows/rollout\`, \`cleanup\` from
6649
- \`@pmelab/gtd/workflows/cleanup\` and \`tests\` from
6650
- \`@pmelab/gtd/workflows/tests\` — plus any it declares). Refuses with
6646
+ \`rollout <flag>\` (turn a release flag default-on) and \`cleanup <flag>\`
6647
+ (remove it), which a config adds by exporting \`rollout\` from
6648
+ \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
6649
+ \`@pmelab/gtd/workflows/cleanup\` — plus any it declares). Refuses with
6651
6650
  \`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
6652
6651
  missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
6653
6652
 
@@ -6760,8 +6759,7 @@ const CONFIG_SCAFFOLD = `// The workflows gtd runs here: gtd's shipped feature,
6760
6759
  // its own doors: \`gtd door review [base]\` and \`gtd door fix\`. Export \`rollout\`
6761
6760
  // from \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
6762
6761
  // \`@pmelab/gtd/workflows/cleanup\` to add \`gtd door rollout <flag>\` and
6763
- // \`gtd door cleanup <flag>\`, and \`tests\` from \`@pmelab/gtd/workflows/tests\`
6764
- // to add \`gtd door tests\`.
6762
+ // \`gtd door cleanup <flag>\`.
6765
6763
  export {
6766
6764
  feature as default,
6767
6765
  feature,
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { Et as envDefaults, Ht as requireFlag, Tt as defaults, Vt as registryEntry, i as skills, n as summary, o as flagShipped, r as access, t as steering, w as coder, zt as flagReferences } from "../chunks/reserved-FWrjy7zo.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-B-Es6Hwj.mjs";
2
+ import { Ct as defaults, It as flagReferences, Rt as registryEntry, S as coder, i as skills, n as summary, o as flagShipped, r as access, t as steering, wt as envDefaults, zt as requireFlag } from "../chunks/reserved-DF1HD6jL.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-D89fU_wf.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 () => {