@pmelab/gtd 44.0.0 → 45.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +40 -9
  3. package/dist/chunks/{reserved-DJk_6-ju.mjs → reserved-ulcSkND6.mjs} +142 -5
  4. package/dist/chunks/{review-X7Jfz0dY.mjs → review-Da2ndnB1.mjs} +177 -7
  5. package/dist/chunks/{tail-5w5mxGTq.mjs → tail-BW030xz_.mjs} +1 -1
  6. package/dist/gtd.bundle.mjs +7 -5
  7. package/dist/testkit.mjs +7 -5
  8. package/dist/workflow-kit.mjs +22 -10
  9. package/dist/workflows/cleanup.mjs +2 -2
  10. package/dist/workflows/feature.mjs +3 -3
  11. package/dist/workflows/fix.mjs +2 -2
  12. package/dist/workflows/review.mjs +4 -15
  13. package/dist/workflows/rollout.mjs +1 -1
  14. package/dist/workflows/tests.mjs +193 -0
  15. package/dist/workflows.mjs +533 -40
  16. package/package.json +6 -1
  17. package/src/install/ConfigScaffold.ts +2 -1
  18. package/src/install/Install.ts +5 -4
  19. package/src/workflow-kit/review.test.ts +80 -2
  20. package/src/workflow-kit/review.ts +45 -11
  21. package/src/workflows/feature/review-answers.feature +12 -3
  22. package/src/workflows/feature/standards.feature +286 -0
  23. package/src/workflows/index.ts +2 -1
  24. package/src/workflows/review/review-door.feature +11 -2
  25. package/src/workflows/review/review.ts +3 -16
  26. package/src/workflows/shared/learn.test.ts +154 -0
  27. package/src/workflows/shared/learn.ts +206 -0
  28. package/src/workflows/shared/lenses.ts +5 -1
  29. package/src/workflows/shared/options.ts +22 -1
  30. package/src/workflows/shared/review.ts +29 -2
  31. package/src/workflows/shared/skills.test.ts +2 -0
  32. package/src/workflows/shared/skills.ts +3 -0
  33. package/src/workflows/shared/standards.test.ts +90 -0
  34. package/src/workflows/shared/standards.ts +156 -0
  35. package/src/workflows/shared/text.test.ts +58 -1
  36. package/src/workflows/shared/text.ts +33 -3
  37. package/src/workflows/shared/vars.ts +10 -0
  38. package/src/workflows/tests/index.ts +6 -0
  39. package/src/workflows/tests/tests-options.feature +52 -0
  40. package/src/workflows/tests/tests.feature +266 -0
  41. package/src/workflows/tests/tests.test.ts +51 -0
  42. package/src/workflows/tests/tests.ts +270 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "44.0.0",
3
+ "version": "45.1.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
@@ -564,6 +564,22 @@ noted in steps 3 and 4 below.
564
564
  `gtd judge answer` / conservative-default shape as step 3's own judged
565
565
  points.
566
566
 
567
+ **The review learns your taste.** Each won't-fix and each own answer is a
568
+ candidate standard. A judge (`build.review.standards.judge`) asks whether it
569
+ is reusable beyond this note; at `standardMinP` (`0.8`) or above it is kept
570
+ in `.gtd/STANDARDS.md` with no question asked, and the next review lists it
571
+ under `## Standards added`. Every later agent turn — the lenses, the naive
572
+ pass, the build, every fix — is handed the kept standards, so a finding you
573
+ rejected once is not raised again and a rule you wrote is followed, in this
574
+ process and every later one. One the judge does not confidently rule out
575
+ contradicting a standard already kept is not applied: it waits under
576
+ `## Undecided` and becomes a `## Standard conflict <id>` note in the review,
577
+ with its own three boxes — **keep the new one (drop the old)**, **keep the
578
+ old one (discard the new)**, or **your own wording**, which replaces both —
579
+ and the review does not sign off until it is answered. `.gtd/STANDARDS.md` is
580
+ committed and yours to edit; gtd rewrites it as one bullet per standard when
581
+ it learns.
582
+
567
583
  You never talk to it. Every exchange is a file in `.gtd/` that you edit in your
568
584
  own editor, and every answer you give is a commit. Your test suite is the gate
569
585
  throughout: a red baseline stops the process before it starts, and a red build
@@ -721,6 +737,21 @@ hand or leave it for cleanup.
721
737
 
722
738
  The delivery model these workflows implement: [Delivery model](SDLC.md).
723
739
 
740
+ ### Reviving skipped tests
741
+
742
+ The opt-in `tests` workflow revisits tests nobody runs. Export `tests` from
743
+ `@pmelab/gtd/workflows/tests` and run `gtd door tests` (or
744
+ `gtd --workflow tests`). On a green baseline an agent sweeps the whole
745
+ repository — every skipped test plus the `test-audit` skill's findings — into
746
+ `.gtd/TESTS.md`, one section per test file. Each file is then rewritten in its
747
+ own turn until nothing in it is skipped and the suite is green. A test the agent
748
+ calls obsolete stays skipped and is proposed for deletion: the review in
749
+ `.gtd/REVIEW.md` gives each proposal `delete it`, `keep it skipped` or
750
+ `_your answer_`, and refuses sign-off until each holds one tick. An agent turn
751
+ that deletes a test file is refused. Skip detection is a factory option with
752
+ JavaScript/TypeScript and cucumber defaults; see
753
+ [Reviving skipped tests](docs/configuration.md#reviving-skipped-tests).
754
+
724
755
  ## Checking a range for stray files
725
756
 
726
757
  ```sh
@@ -859,15 +890,15 @@ approvals as `approvals` (`[]` when none), and `gtd history show` prints each as
859
890
  gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
860
891
  [SemVer promise](https://github.com/pmelab/gtd/blob/main/COMPATIBILITY.md).
861
892
 
862
- | Import | What it is |
863
- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
864
- | `@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`. |
865
- | `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
866
- | `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`) and the review-note readers. |
867
- | `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. |
868
- | `@pmelab/gtd/workflows` | Every shipped workflow and all its parts in one module; also resolvable as `@pmelab/gtd/workflow`. |
869
- | `@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. |
870
- | `@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. |
893
+ | Import | What it is |
894
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
895
+ | `@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`. |
896
+ | `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
897
+ | `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`) and the review-note readers. |
898
+ | `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. |
899
+ | `@pmelab/gtd/workflows` | Every shipped workflow and all its parts in one module; also resolvable as `@pmelab/gtd/workflow`. |
900
+ | `@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. |
901
+ | `@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. |
871
902
 
872
903
  Types come from the TypeScript sources the package ships. Configs never need an
873
904
  install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
@@ -113,6 +113,108 @@ const requireFlag = (workflow, registryPath, name, defaultOn) => {
113
113
  if (entry.defaultOn !== defaultOn) return refuse(defaultOn ? `gtd: ${workflow}: flag "${name}" is not default-on — roll it out first` : `gtd: ${workflow}: flag "${name}" is already default-on`);
114
114
  };
115
115
  //#endregion
116
+ //#region src/workflows/shared/standards.ts
117
+ const STANDARDS = ".gtd/STANDARDS.md";
118
+ const UNDECIDED = "Undecided";
119
+ const oneLine = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim();
120
+ const ITEM = /^(?:[-*+]|\d+[.)])\s+(.*)$/;
121
+ const CONTRADICTS = /^\s+[-*+]\s+contradicts:\s*(.*)$/i;
122
+ const wrapped = (item, line) => {
123
+ const olds = item.olds;
124
+ if (olds !== void 0 && olds.length > 0) olds[olds.length - 1] += ` ${line.trim()}`;
125
+ else item.text += ` ${line.trim()}`;
126
+ };
127
+ /** What tells two standards apart: not the emphasis or spacing a formatter may rewrite. */
128
+ const standardKey = (text) => oneLine(text.replace(/[*_`\\]/g, "")).toLowerCase();
129
+ const toStandards = (items) => {
130
+ const seen = /* @__PURE__ */ new Set();
131
+ const fresh = (text) => {
132
+ const key = standardKey(text);
133
+ if (key === "" || seen.has(key)) return false;
134
+ seen.add(key);
135
+ return true;
136
+ };
137
+ const decided = items.filter((i) => i.olds === void 0).map((i) => oneLine(i.text));
138
+ const undecided = items.flatMap((i) => i.olds === void 0 ? [] : [{
139
+ text: oneLine(i.text),
140
+ olds: i.olds.map(oneLine).filter((o) => o !== "")
141
+ }]);
142
+ return {
143
+ decided: decided.filter(fresh),
144
+ undecided: undecided.filter((c) => fresh(c.text))
145
+ };
146
+ };
147
+ /**
148
+ * Every top-level bullet is one standard, those under `## Undecided` waiting
149
+ * on the human; a hand-edited file may group the rest under any heading.
150
+ */
151
+ const parseStandards = (text) => {
152
+ const items = [];
153
+ let pending = false;
154
+ let last;
155
+ for (const line of text.split("\n")) {
156
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
157
+ const item = ITEM.exec(line);
158
+ const old = CONTRADICTS.exec(line);
159
+ if (heading !== null) {
160
+ pending = heading[1].length > 1 && heading[2].trim() === UNDECIDED;
161
+ last = void 0;
162
+ } else if (item !== null) {
163
+ last = pending ? {
164
+ text: item[1],
165
+ olds: []
166
+ } : { text: item[1] };
167
+ items.push(last);
168
+ } else if (old !== null && last?.olds !== void 0) last.olds.push(old[1]);
169
+ else if (/^\s+\S/.test(line) && last !== void 0) wrapped(last, line);
170
+ else if (line.trim() !== "") last = void 0;
171
+ }
172
+ return toStandards(items);
173
+ };
174
+ const renderStandards = ({ decided, undecided }) => {
175
+ return `${[
176
+ "# Standards",
177
+ "Kept from your review answers for every agent turn. One standard per bullet.",
178
+ ...decided.length === 0 ? [] : [decided.map((s) => `- ${s}`).join("\n")],
179
+ ...undecided.length === 0 ? [] : [`## ${UNDECIDED}`, undecided.map((c) => [`- ${c.text}`, ...c.olds.map((o) => ` - contradicts: ${o}`)].join("\n")).join("\n")]
180
+ ].join("\n\n")}\n`;
181
+ };
182
+ const hash = (raw) => {
183
+ const text = standardKey(raw);
184
+ let h = 2166136261;
185
+ for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
186
+ return (h >>> 0).toString(16).padStart(8, "0");
187
+ };
188
+ const CONFLICT_TITLE = /^Standard conflict ([0-9a-f]{8})$/;
189
+ const conflictTitle = (c) => `Standard conflict ${hash(c.text)}`;
190
+ const conflictFor = (standards, title) => {
191
+ const id = CONFLICT_TITLE.exec(title)?.[1];
192
+ return id === void 0 ? void 0 : standards.undecided.find((c) => hash(c.text) === id);
193
+ };
194
+ const isConflictTitle = (title) => CONFLICT_TITLE.test(title);
195
+ const KEEP_NEW = "keep the new one (drop the old)";
196
+ const KEEP_OLD = "keep the old one (discard the new)";
197
+ /** A conflict's chunk body; its three boxes read as fix / won't fix / own answer by position. */
198
+ const conflictBody = (c) => [...c.olds.length === 0 ? [`A standard learned from your answers contradicted ones another answer since replaced in \`${STANDARDS}\`. Keep it, discard it, or reword it.`, `New: ${c.text}`] : [
199
+ `A standard learned from your answers contradicts ${c.olds.length === 1 ? "one" : "some"} already in \`${STANDARDS}\`. Your own answer replaces both with its wording.`,
200
+ `New: ${c.text}`,
201
+ `Old:\n\n${c.olds.map((o) => `- ${o}`).join("\n")}`
202
+ ], `${ANSWER_LEAD}\n\n- [ ] ${KEEP_NEW}\n- [ ] ${KEEP_OLD}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`].join("\n\n");
203
+ /** What every agent turn is told while `.gtd/STANDARDS.md` holds decided standards. */
204
+ const standardsBlock = (text) => {
205
+ const { decided } = parseStandards(text ?? "");
206
+ if (decided.length === 0) return "";
207
+ return `## Standards
208
+
209
+ \`${STANDARDS}\` holds the standards the human set in earlier reviews.
210
+ Follow every one in what you write. Reviewing, never raise a finding a
211
+ standard rules out — a \`Won't fix:\` standard is a finding the human
212
+ rejected, so never raise it or one like it again — and raise breaking a
213
+ standard as a finding.
214
+
215
+ ${decided.map((s) => `- ${s}`).join("\n")}`;
216
+ };
217
+ //#endregion
116
218
  //#region src/workflows/shared/vars.ts
117
219
  /** The refactor guard's fixed list, and `seamAllow`'s default minus `.gtd/**`. */
118
220
  const testFileGlobs = [
@@ -124,6 +226,11 @@ const testFileGlobs = [
124
226
  "**/__tests__/**"
125
227
  ];
126
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
+ };
127
234
  /** Process settings. */
128
235
  const defaults = {
129
236
  reviewBase: "",
@@ -132,6 +239,7 @@ const defaults = {
132
239
  reviewNoteActionable: "0.7",
133
240
  reviewSeverityMinP: "0.7",
134
241
  qualityTriageMinP: "0.7",
242
+ standardMinP: "0.8",
135
243
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
136
244
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
137
245
  flagMode: "true",
@@ -391,8 +499,11 @@ const withSkills = (skills, prompt) => {
391
499
  };
392
500
  /** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
393
501
  const agentWithSkills = (name, prompt, options = {}) => {
394
- const access = accessPreamble(accessFor(name, options.file));
395
- const body = access === "" ? prompt : `${access}\n\n${prompt}`;
502
+ const body = [
503
+ accessPreamble(accessFor(name, options.file)),
504
+ prompt,
505
+ standardsBlock(read(STANDARDS))
506
+ ].filter((part) => part !== "").join("\n\n");
396
507
  return agent(name, withSkills(skillsFor(name).join(", "), body), options);
397
508
  };
398
509
  const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
@@ -432,6 +543,7 @@ const SUMMARY_TITLES = [
432
543
  "Weakened scenarios",
433
544
  "Fixed from your notes",
434
545
  "Fixed automatically",
546
+ "Standards added",
435
547
  "Dropped as noise",
436
548
  "Nothing to note"
437
549
  ];
@@ -451,7 +563,9 @@ const composedReview = (base, report) => {
451
563
  return name;
452
564
  };
453
565
  const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
454
- const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
566
+ const answerBoxes = ([fix, wontFix] = [ANSWER_FIX, ANSWER_WONT_FIX]) => `${ANSWER_LEAD}\n\n- [ ] ${fix}\n- [ ] ${wontFix}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
567
+ const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
568
+ const added = report.standards?.added ?? [];
455
569
  const notes = report.notes.map((n) => ({
456
570
  ...n,
457
571
  title: unique(n.title)
@@ -460,8 +574,10 @@ const composedReview = (base, report) => {
460
574
  const chunks = [
461
575
  ...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.`)],
462
576
  ...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
463
- ...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}`)),
577
+ ...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)}`)),
464
579
  ...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
580
+ ...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
465
581
  ...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
466
582
  ];
467
583
  const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
@@ -1765,6 +1881,7 @@ const ShippedOptions = Struct({
1765
1881
  reviewNoteActionable: setting,
1766
1882
  reviewSeverityMinP: setting,
1767
1883
  qualityTriageMinP: setting,
1884
+ standardMinP: setting,
1768
1885
  refactorFlagPattern: setting,
1769
1886
  seamAllow: setting,
1770
1887
  sliceMaxCriteria: setting,
@@ -1827,6 +1944,24 @@ const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions,
1827
1944
  base: void 0
1828
1945
  };
1829
1946
  });
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
+ }));
1830
1965
  //#endregion
1831
1966
  //#region src/workflows/shared/skills.ts
1832
1967
  const skills = (vars) => {
@@ -1842,6 +1977,8 @@ const skills = (vars) => {
1842
1977
  design: ["spec-driven-development", "planning-and-task-breakdown"],
1843
1978
  notes: ["incremental-implementation", "code-simplification"],
1844
1979
  feedback: ["test-driven-development", "incremental-implementation"],
1980
+ tests: ["test-audit", "test-driven-development"],
1981
+ "tests.health": ["debugging-and-error-recovery"],
1845
1982
  ...build,
1846
1983
  ...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
1847
1984
  refactor: ["ponytail"],
@@ -1874,4 +2011,4 @@ const steering = {
1874
2011
  [REVIEW]: "review"
1875
2012
  };
1876
2013
  //#endregion
1877
- export { naiveApplyFailure as $, reviewMissing as A, registryEntry as At, coverageMessage as B, coder as C, unwindFailure as Ct, fixQuality as D, flagMentions as Dt, fixNits as E, testFileGlobs as Et, buildReviewTriageMessage as F, designSystem as G, coveringPrompt as H, builderSystem as I, fixReviewNotesPrompt as J, designTriagePrompt as K, clearCriteriaMessage as L, reviewing as M, agentWithSkills as N, fixRisks as O, flagReferences as Ot, buildReviewCollectingPrompt as P, implementFixPrompt as Q, clearCriteriaQuestion as R, awaitReview as S, unwindFailedMessage as St, fixCheck as T, envDefaults as Tt, criterionLine as U, coverageQuestion as V, designGateAnswerMessage as W, idleMessage as X, handOffAwaitReviewMessage as Y, implementBuildingPrompt as Z, REFACTOR_NOTE as _, seamByHandMessage as _t, flagModeOn as a, qualityUnresolvedMessage as at, SLICES as b, startGateBlockedMessage as bt, baseline as c, refactorFixPrompt as ct, guarded as d, reviewEditNotesCapture as dt, naivePassPrompt as et, healthy as f, reviewEditsCapture as ft, REFACTOR as g, reviewerSystem as gt, QUALITY as h, reviewNotesCapture as ht, skills as i, qualityTriageMessage as it, reviewQuality as j, requireFlag as jt, planner as k, flipProblems as kt, escalation as l, refactorNotePrompt as lt, HANDOFF as m, reviewGateBlockedMessage as mt, summary as n, oversizedMessage as nt, flagShipped as o, refactorBuildingPrompt as ot, FEEDBACK as p, reviewFeedbackFixPrompt as pt, finisherSystem as q, access as r, qualityFindingsFile as rt, shipped as s, refactorDroppedRefusal as st, steering as t, normaliseCriteriaScript as tt, gate as u, refactorRules as ut, REQUIREMENTS as v, seamSteerMessage as vt, collecting as w, defaults as wt, answerReviewQuestions as x, uncoveredMessage as xt, REVIEW as y, slicingPrompt as yt, composedReview as z };
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 };
@@ -1,7 +1,159 @@
1
- import { answered, changesSince, head, judge, numeric, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
1
+ import { answered, changesSince, head, judge, numeric, read, removeScript, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
2
2
  import { removedAssertions, review } from "../workflow-kit.mjs";
3
- import { A as reviewMissing, E as fixNits, F as buildReviewTriageMessage, O as fixRisks, S as awaitReview, T as fixCheck, d as guarded, dt as reviewEditNotesCapture, f as healthy, ft as reviewEditsCapture, h as QUALITY, ht as reviewNotesCapture, w as collecting, x as answerReviewQuestions, y as REVIEW, z as composedReview } from "./reserved-DJk_6-ju.mjs";
4
- import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-5w5mxGTq.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-ulcSkND6.mjs";
4
+ import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-BW030xz_.mjs";
5
+ //#region src/workflows/shared/learn.ts
6
+ const SUMMARY_MAX = 200;
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*/;
8
+ const summary = (body) => {
9
+ const text = oneLine(body).replace(BANNER, "");
10
+ return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
11
+ };
12
+ /** The standard a won't-fix or an own answer would make; a fix states none. */
13
+ const candidateOf = (d) => {
14
+ const anchor = oneLine(d.anchor);
15
+ if (d.choice === "wontfix") {
16
+ const why = summary(d.body);
17
+ return [{
18
+ decision: d,
19
+ text: `Won't fix: ${anchor}${why === "" ? "" : ` — ${why}`}`
20
+ }];
21
+ }
22
+ if (d.choice === "answer") return [{
23
+ decision: d,
24
+ text: `${oneLine(d.text)} (re: ${anchor})`
25
+ }];
26
+ return [];
27
+ };
28
+ const without = (list, drop) => {
29
+ const keys = new Set(drop.map(standardKey));
30
+ return list.filter((s) => !keys.has(standardKey(s)));
31
+ };
32
+ const adding = (list, text) => list.some((s) => standardKey(s) === standardKey(text)) ? [...list] : [...list, text];
33
+ /**
34
+ * Keep the new one, keep the old one, or replace both with the human's own
35
+ * wording. Another open conflict stops naming a replaced standard: the human
36
+ * never judged it against the replacement, so keeping it must not drop that.
37
+ */
38
+ const resolve = (standards, d) => {
39
+ const c = conflictFor(standards, d.anchor);
40
+ if (c === void 0) return standards;
41
+ const others = standards.undecided.filter((u) => u !== c);
42
+ if (d.choice === "wontfix") return {
43
+ decided: standards.decided,
44
+ undecided: others
45
+ };
46
+ const kept = d.choice === "fix" ? c.text : oneLine(d.text);
47
+ const undecided = others.map((u) => ({
48
+ text: u.text,
49
+ olds: without(u.olds, c.olds)
50
+ }));
51
+ return {
52
+ decided: adding(without(standards.decided, c.olds), kept),
53
+ undecided
54
+ };
55
+ };
56
+ const contradicts = (i, id, other) => ({
57
+ id: `standard-${i}.contradicts-${id}`,
58
+ primitive: "noul",
59
+ instructions: `Does the "Standard:" line of evidence "standard-${i}" contradict ${other}?`,
60
+ criteria: "yes: following one breaks the other. no: both can be followed — they are compatible or unrelated."
61
+ });
62
+ const questionsFor = (i, anchor, decided) => [
63
+ {
64
+ id: `standard-${i}`,
65
+ primitive: "noul",
66
+ instructions: `Is the human's answer to review note "${anchor}" (evidence "standard-${i}") a reusable standard for this codebase — a rule later changes and reviews should follow — rather than a call about this one note? Its "Standard:" line is the wording gtd would keep.`,
67
+ criteria: "yes: it states a rule or preference that applies beyond this note. no: it is specific to this note, a one-off call, or unclear."
68
+ },
69
+ ...decided.map((_, k) => contradicts(i, `${k + 1}`, `kept standard ${k + 1} (evidence "kept-${k + 1}")`)),
70
+ ...Array.from({ length: i - 1 }, (_, j) => contradicts(i, `new-${j + 1}`, `the "Standard:" line of evidence "standard-${j + 1}"`))
71
+ ];
72
+ const evidenceOf = (c) => [
73
+ `Note: ${c.decision.anchor}`,
74
+ `Reviewer's text: ${c.decision.body}`,
75
+ `Human's answer: ${c.decision.choice === "wontfix" ? "won't fix" : c.decision.text}`,
76
+ `Standard: ${c.text}`
77
+ ].join("\n");
78
+ /**
79
+ * One judge rest over every candidate. Only a confident `yes` on uncut
80
+ * evidence makes a standard; a contradiction the judge does not confidently
81
+ * rule out goes to the human instead of overwriting what they decided before.
82
+ * A cut kept standard leaves the round unlearned: raise `judgeBudgetBytes`.
83
+ */
84
+ const judged = async (candidates, decided, minP) => {
85
+ const { answers, truncated } = await judge("review.standards.judge", {
86
+ questions: candidates.flatMap((c, i) => questionsFor(i + 1, c.decision.anchor, decided)),
87
+ evidence: Object.fromEntries([...candidates.map((c, i) => [`standard-${i + 1}`, evidenceOf(c)]), ...decided.map((s, k) => [`kept-${k + 1}`, s])]),
88
+ message: "Judging whether your answers are standards to keep: run `gtd judge run`, or land without a verdict, which keeps none.",
89
+ label: "Learning your standards"
90
+ });
91
+ const ruledOut = (i, id) => answered(answers[`standard-${i}.contradicts-${id}`], "no", minP);
92
+ const added = [];
93
+ const raised = [];
94
+ candidates.forEach((c, n) => {
95
+ const i = n + 1;
96
+ if (truncated.includes(`standard-${i}`)) return;
97
+ if (decided.some((_, k) => truncated.includes(`kept-${k + 1}`))) return;
98
+ if (!answered(answers[`standard-${i}`], "yes", minP)) return;
99
+ const olds = [...decided.filter((_, k) => !ruledOut(i, `${k + 1}`)), ...added.filter((a) => !ruledOut(i, `new-${a.i}`)).map((a) => a.text)];
100
+ if (olds.length === 0) added.push({
101
+ i,
102
+ text: c.text
103
+ });
104
+ else raised.push({
105
+ text: c.text,
106
+ olds
107
+ });
108
+ });
109
+ return {
110
+ added: added.map((a) => a.text),
111
+ raised
112
+ };
113
+ };
114
+ /**
115
+ * Settles every answer to a standard conflict, then learns standards from
116
+ * the won't-fix ticks and own answers: kept in `.gtd/STANDARDS.md`, a
117
+ * contradicting one as a new conflict note in `.gtd/REVIEW.md`.
118
+ */
119
+ const learnStandards = async (decisions) => {
120
+ const before = parseStandards(read(".gtd/STANDARDS.md") ?? "");
121
+ const rest = decisions.filter((d) => !isConflictTitle(d.anchor));
122
+ const settled = decisions.filter((d) => isConflictTitle(d.anchor)).reduce(resolve, before);
123
+ const known = new Set([...settled.decided, ...settled.undecided.map((c) => c.text)].map(standardKey));
124
+ const candidates = rest.flatMap(candidateOf).filter((c) => {
125
+ const key = standardKey(c.text);
126
+ if (known.has(key)) return false;
127
+ known.add(key);
128
+ return true;
129
+ });
130
+ const minP = numeric(vars.standardMinP, Infinity);
131
+ const { added, raised } = candidates.length === 0 || !Number.isFinite(minP) ? {
132
+ added: [],
133
+ raised: []
134
+ } : await judged(candidates, settled.decided, minP);
135
+ const after = {
136
+ decided: [...settled.decided, ...added],
137
+ undecided: [...settled.undecided, ...raised]
138
+ };
139
+ if (renderStandards(after) !== renderStandards(before)) await run("review.standards.recording", writeScript(STANDARDS, renderStandards(after)), { label: "Recording your standards" });
140
+ if (raised.length > 0) {
141
+ const chunks = raised.map((c) => `## ${conflictTitle(c)}\n\n${conflictBody(c)}`);
142
+ await run("review.standards.raising", writeScript(REVIEW, `${(read(".gtd/REVIEW.md") ?? "").trimEnd()}\n\n${chunks.join("\n\n")}\n`), {
143
+ label: "Asking about a contradicting standard",
144
+ file: REVIEW
145
+ });
146
+ }
147
+ };
148
+ const settlesStandard = (d) => isConflictTitle(d.anchor);
149
+ const standardsPending = () => parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided.length > 0;
150
+ const standardsAddedSince = (base) => {
151
+ const change = changesSince(base).get(STANDARDS);
152
+ if (change === void 0) return [];
153
+ const was = new Set(parseStandards(change.before ?? "").decided);
154
+ return parseStandards(change.after ?? "").decided.filter((s) => !was.has(s));
155
+ };
156
+ //#endregion
5
157
  //#region src/workflows/shared/review.ts
6
158
  const VERDICTS = [
7
159
  "edit",
@@ -132,7 +284,11 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
132
284
  ...report,
133
285
  weakened: removedAssertions(changesSince(base)),
134
286
  nitFix,
135
- answered
287
+ answered,
288
+ standards: {
289
+ added: standardsAddedSince(base),
290
+ undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
291
+ }
136
292
  }), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
137
293
  label: "Composing the review",
138
294
  file: REVIEW,
@@ -147,7 +303,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
147
303
  * praise dropped.
148
304
  */
149
305
  const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
150
- const { guard, guardRests, fixAccess, rules } = options;
306
+ const { guard, guardRests, fixAccess, rules, awaitMessage } = options;
151
307
  const feedback = options.feedback ?? {
152
308
  file: ".gtd/REQUIREMENTS.md",
153
309
  collecting,
@@ -172,11 +328,14 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
172
328
  guardRests,
173
329
  rules
174
330
  }),
175
- awaitReview: (b) => awaitReview(b),
331
+ awaitReview: (b) => awaitReview(b, awaitMessage?.(b)),
176
332
  reviewMissing,
177
333
  collecting: feedback.collecting,
178
334
  triage: triage(severities),
179
335
  decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
336
+ settles: settlesStandard,
337
+ learn: learnStandards,
338
+ pending: standardsPending,
180
339
  answerQuestions: answerReviewQuestions,
181
340
  fixNits: async (notes) => {
182
341
  await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
@@ -229,6 +388,17 @@ const callForm = (first, second, [third, fourth, ...extra]) => {
229
388
  if (typeof second === "string") return legacyForm(first, second, third, fourth);
230
389
  if (typeof first === "string") return currentForm(first, second, third, fourth);
231
390
  };
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
+ };
232
402
  /**
233
403
  * Undo the human's review-round code edits. A path changed since the human
234
404
  * left it is not overwritten; requireRevert names it instead.
@@ -246,4 +416,4 @@ const reUnwind = async (feedback) => {
246
416
  requireRevert(edited, base);
247
417
  };
248
418
  //#endregion
249
- export { reUnwind as i, humanReview as n, isCode as r, buildTail as t };
419
+ export { reUnwind as a, isCode as i, fixFeedback as n, humanReview as r, buildTail as t };
@@ -1,6 +1,6 @@
1
1
  import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
2
2
  import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { A as reviewMissing, D as fixQuality, M as reviewing, O as fixRisks, S as awaitReview, T as fixCheck, Y as handOffAwaitReviewMessage, d as guarded, f as healthy, h as QUALITY, it as qualityTriageMessage, j as reviewQuality, l as escalation, rt as qualityFindingsFile, y as REVIEW } from "./reserved-DJk_6-ju.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-ulcSkND6.mjs";
4
4
  //#region src/workflows/shared/lenses.ts
5
5
  const NO_FINDINGS = {
6
6
  notes: [],
@@ -21606,10 +21606,11 @@ 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) 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
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
21613
21614
  \`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
21614
21615
  missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
21615
21616
 
@@ -21722,7 +21723,8 @@ const CONFIG_SCAFFOLD = `// The workflows gtd runs here: gtd's shipped feature,
21722
21723
  // its own doors: \`gtd door review [base]\` and \`gtd door fix\`. Export \`rollout\`
21723
21724
  // from \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
21724
21725
  // \`@pmelab/gtd/workflows/cleanup\` to add \`gtd door rollout <flag>\` and
21725
- // \`gtd door cleanup <flag>\`.
21726
+ // \`gtd door cleanup <flag>\`, and \`tests\` from \`@pmelab/gtd/workflows/tests\`
21727
+ // to add \`gtd door tests\`.
21726
21728
  export {
21727
21729
  feature as default,
21728
21730
  feature,
package/dist/testkit.mjs CHANGED
@@ -6643,10 +6643,11 @@ 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) 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
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
6650
6651
  \`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
6651
6652
  missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
6652
6653
 
@@ -6759,7 +6760,8 @@ const CONFIG_SCAFFOLD = `// The workflows gtd runs here: gtd's shipped feature,
6759
6760
  // its own doors: \`gtd door review [base]\` and \`gtd door fix\`. Export \`rollout\`
6760
6761
  // from \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
6761
6762
  // \`@pmelab/gtd/workflows/cleanup\` to add \`gtd door rollout <flag>\` and
6762
- // \`gtd door cleanup <flag>\`.
6763
+ // \`gtd door cleanup <flag>\`, and \`tests\` from \`@pmelab/gtd/workflows/tests\`
6764
+ // to add \`gtd door tests\`.
6763
6765
  export {
6764
6766
  feature as default,
6765
6767
  feature,