@pmelab/gtd 46.0.0 → 48.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 (34) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +21 -19
  3. package/dist/chunks/{reserved-BENPy6IJ.mjs → reserved-DF1HD6jL.mjs} +41 -22
  4. package/dist/chunks/{review-DcbgUwr7.mjs → review-Bv40kZu9.mjs} +5 -8
  5. package/dist/chunks/{tail-CDNMZ-2E.mjs → tail-D89fU_wf.mjs} +68 -39
  6. package/dist/gtd.bundle.mjs +203 -867
  7. package/dist/testkit.mjs +89 -53
  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 +3 -3
  12. package/dist/workflows/rollout.mjs +1 -1
  13. package/dist/workflows.mjs +257 -205
  14. package/package.json +1 -1
  15. package/src/cli/Cli.test.ts +0 -2
  16. package/src/cli/Cli.ts +0 -15
  17. package/src/cli/Program.test.ts +0 -1
  18. package/src/cli/Program.ts +0 -8
  19. package/src/cli/ToolCommands.ts +1 -2
  20. package/src/install/Install.ts +89 -35
  21. package/src/testkit/Layers.ts +0 -1
  22. package/src/wire/ContextLimit.ts +1 -1
  23. package/src/workflows/cleanup/cleanup.test.ts +11 -9
  24. package/src/workflows/feature/feature.ts +38 -28
  25. package/src/workflows/feature/lens-review.feature +3 -1
  26. package/src/workflows/feature/slice-findings.feature +198 -0
  27. package/src/workflows/fix/fix-lens-origin.feature +101 -0
  28. package/src/workflows/fix/fix-quality.feature +131 -0
  29. package/src/workflows/index.ts +0 -1
  30. package/src/workflows/shared/lenses.ts +30 -5
  31. package/src/workflows/shared/review.ts +3 -8
  32. package/src/workflows/shared/tail.ts +74 -57
  33. package/src/workflows/shared/text.ts +85 -29
  34. package/src/workflows/feature/quality-unresolved.feature +0 -121
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "46.0.0",
3
+ "version": "48.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
@@ -302,17 +302,19 @@ this with your environment. See
302
302
  [Driving the loop](https://github.com/pmelab/gtd/blob/main/docs/driver.md) for
303
303
  the full protocol.
304
304
 
305
- Or skip writing one: **`gtd loop`** is that loop, shipped with gtd. It drives
306
- `claude` (or `pi`) until the process rests on you, logs every turn to the file
307
- `gtd next --json=log` names, and maps the workflow's `smart`/`base` model hints
308
- to real model names through per-machine settings — see
309
- [`gtd loop`](https://github.com/pmelab/gtd/blob/main/docs/driver.md#the-bundled-driver-gtd-loop).
305
+ Or skip writing one: the
306
+ [complete minimal driver](https://github.com/pmelab/gtd/blob/main/docs/driver.md#a-complete-minimal-driver)
307
+ is a paste to own outright. It drives `claude` until the process rests on you,
308
+ logs every turn to the file `gtd next --json=log` names, records each turn's
309
+ cost on its landing, answers judge gates through `gtd judge run`, and maps the
310
+ workflow's `smart`/`base` model hints to real model names through
311
+ `GTD_LOOP_MODEL_SMART`/`GTD_LOOP_MODEL_BASE`.
310
312
 
311
313
  Both it and the Claude Code mod stop an agent turn that outgrows its context:
312
314
  past 60% of the model's window the agent is asked to wrap up with a handoff
313
- note, past 70% (or when the context is about to be compacted) the turn stops and
314
- nothing lands. A step's `context` option, or the bundled workflows'
315
- `contextLimit` option, moves the two limits — see
315
+ note, past 70% after a tool call (or when the context is about to be compacted)
316
+ the turn stops and nothing lands. A step's `context` option, or the bundled
317
+ workflows' `contextLimit` option, moves the two limits — see
316
318
  [Context limits](https://github.com/pmelab/gtd/blob/main/docs/driver.md#context-limits).
317
319
  In `feature`, a naive pass, refactor or build turn that wrapped up that way is a
318
320
  snapshot: a fresh turn picks up from it with the handoff note and the criteria
@@ -478,10 +480,10 @@ noted in steps 3 and 4 below.
478
480
  - After you review: is each of your notes an edit, a question, a nit or
479
481
  praise?
480
482
 
481
- gtd itself never calls a judge: `gtd loop`, the Claude Code mod and the
482
- reference driver answer these through `gtd judge run` (auto selection: jev
483
- when `TYPESAFE_API_KEY` is set, else `llm` via `claude`, default model
484
- sonnet, while the Claude Code mod's own judge subagent stays on `haiku` —
483
+ gtd itself never calls a judge: the Claude Code mod and the reference driver
484
+ answer these through `gtd judge run` (auto selection: jev when
485
+ `TYPESAFE_API_KEY` is set, else `llm` via `claude`, default model sonnet,
486
+ while the Claude Code mod's own judge subagent stays on `haiku` —
485
487
  `--model <name>` overrides); if that fails it shows you the message and
486
488
  stops. **The `llm` provider's `p` is self-reported by the model, not a
487
489
  measured probability, so a confidently wrong verdict can clear the 0.7 floor
@@ -504,12 +506,12 @@ noted in steps 3 and 4 below.
504
506
  unsure verdict (below `qualityTriageMinP`, `0.7`) is `design`; mechanical
505
507
  findings the fix turns leave become notes too. This lap is where code quality
506
508
  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.
509
+ review stays the final gate, and nothing here skips it. Every
510
+ [coarse slice](docs/configuration.md#coarse-slices) runs the same triaged lap
511
+ and never rests for its findings: its notes are carried to the review after
512
+ the last slice, each naming its slice and lens, beside every slice's fixes.
513
+ The `fix` and `cleanup` doors triage their lap too, and hand its notes to
514
+ their own review.
513
515
 
514
516
  A red suite that keeps failing past a few fix attempts escalates instead of
515
517
  retrying forever: an agent turn reads the failing output and writes
@@ -896,7 +898,7 @@ install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
896
898
  The `gtd` bin's commands — `next`, `land`, `abandon`, `restore`, `validate`,
897
899
  `check`, `uncheck`, `install`, `summary`, `base`, `door`, `doors`, `slices`,
898
900
  `version`, `help`, `history list`, `history show`, `history metrics`,
899
- `history judges`, `history fetch`, `strays`, and the tools `ui`, `lsp`, `loop`,
901
+ `history judges`, `history fetch`, `strays`, and the tools `ui`, `lsp`,
900
902
  `context-hook`, `judge` (`judge answer`, `judge run`) — are listed in the
901
903
  [CLI reference](https://github.com/pmelab/gtd/blob/main/docs/cli.md).
902
904
  `gtd slices` lists the spans a flow's `slice()` calls mark. It refuses (exit 1)
@@ -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 = "";
@@ -544,6 +544,20 @@ const SUMMARY_TITLES = [
544
544
  ];
545
545
  const unboxed = (body) => body.replace(/^(\s*(?:>\s*)*(?:[-*+]|\d+[.)])\s+)\[[ xX]\]\s?/gm, "$1");
546
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;
547
561
  /**
548
562
  * \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
549
563
  * answer boxes, then the fixed and noise lists. Chunk titles are made unique —
@@ -570,25 +584,14 @@ const composedReview = (base, report) => {
570
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.`)],
571
585
  ...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
572
586
  ...conflicts,
573
- ...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}`)),
574
- ...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"))],
575
589
  ...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
576
- ...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"))]
577
591
  ];
578
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.")];
579
593
  return `# Review: ${short(base)}\n\n<!-- base: ${base} -->\n\n${body.join("\n\n")}\n`;
580
594
  };
581
- const qualityUnresolvedMessage = () => `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
582
- holds findings. Nothing after this fixes them, so the process will not
583
- go on with them open.
584
-
585
- Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
586
- dropping a finding is your call to make.
587
-
588
- What each change does next (then run \`gtd land\`):
589
- - **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**).
590
- - **Anything else** — rests here again.
591
- `;
592
595
  const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
593
596
  \`.gtd/FEEDBACK.md\` holds the failing output.
594
597
 
@@ -1254,6 +1257,28 @@ const lapRange = {
1254
1257
  feedback: "it starts at the previous review's boundary, so it covers only what is new since the previous review.",
1255
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."
1256
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
+ };
1257
1282
  const buildReviewReviewingPrompt = (base, lap, carry, removed = []) => `${styleBlock}
1258
1283
 
1259
1284
  ${styleFormatContract}
@@ -1798,12 +1823,6 @@ const guardedSteps = (guard, describeGuard, rests = true, rules) => {
1798
1823
  exhausted: guarded(steps.exhausted, rests ? guard : void 0)
1799
1824
  };
1800
1825
  };
1801
- /**
1802
- * Hand a red suite that repeated attempts could not fix to a person: an
1803
- * agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
1804
- * on, the human is told the budget is spent instead.
1805
- */
1806
- const escalation = (count, guard, guardRests = true, rules) => escalation$1(count, guardedSteps(guard, void 0, guardRests, rules));
1807
1826
  /** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
1808
1827
  const retryQuestion = (comparable) => comparable ? {
1809
1828
  id: "verdict",
@@ -1986,4 +2005,4 @@ const steering = {
1986
2005
  [REVIEW]: "review"
1987
2006
  };
1988
2007
  //#endregion
1989
- export { naiveApplyFailure as $, reviewMissing as A, conflictTitle as At, coverageMessage as B, requireFlag as Bt, coder as C, unwindFailure as Ct, fixQuality as D, STANDARDS as Dt, fixNits as E, testFileGlobs as Et, buildReviewTriageMessage as F, standardKey as Ft, designSystem as G, coveringPrompt as H, builderSystem as I, flagMentions as It, fixReviewNotesPrompt as J, designTriagePrompt as K, clearCriteriaMessage as L, flagReferences as Lt, reviewing as M, oneLine as Mt, agentWithSkills as N, parseStandards as Nt, fixRisks as O, conflictBody as Ot, buildReviewCollectingPrompt as P, renderStandards as Pt, implementFixPrompt as Q, clearCriteriaQuestion as R, flipProblems as Rt, 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, isConflictTitle as jt, planner as k, conflictFor 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, registryEntry 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
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 { A as reviewMissing, At as conflictTitle, Dt as STANDARDS, E as fixNits, F as buildReviewTriageMessage, Ft as standardKey, Mt as oneLine, Nt as parseStandards, O as fixRisks, Ot as conflictBody, Pt as renderStandards, S as awaitReview, T as fixCheck, d as guarded, dt as reviewEditNotesCapture, f as healthy, ft as reviewEditsCapture, h as QUALITY, ht as reviewNotesCapture, jt as isConflictTitle, kt as conflictFor, w as collecting, x as answerReviewQuestions, y as REVIEW, z as composedReview } from "./reserved-BENPy6IJ.mjs";
4
- import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-CDNMZ-2E.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
@@ -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, {
@@ -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 { 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-BENPy6IJ.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 };