@pmelab/gtd 41.0.0 → 43.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 (73) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +107 -53
  3. package/claude/hooks/sketch.ts +1 -1
  4. package/dist/chunks/{reserved-Ch4LLrEj.mjs → reserved-COEMFEpy.mjs} +84 -51
  5. package/dist/chunks/{steering-DVILanVf.mjs → steering-D4LUQ-av.mjs} +95 -12
  6. package/dist/chunks/tail-C_xn8U7U.mjs +246 -0
  7. package/dist/flows.mjs +11 -1
  8. package/dist/gtd.bundle.mjs +498 -30
  9. package/dist/steering.mjs +2 -2
  10. package/dist/testkit.mjs +460 -28
  11. package/dist/web/main.js +42 -13
  12. package/dist/workflow-kit.mjs +61 -25
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +66 -57
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +2 -2
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +446 -141
  19. package/package.json +1 -1
  20. package/src/cli/Cli.test.ts +33 -1
  21. package/src/cli/Cli.ts +70 -6
  22. package/src/cli/History.ts +15 -0
  23. package/src/cli/Judges.test.ts +373 -0
  24. package/src/cli/Judges.ts +624 -0
  25. package/src/cli/Metrics.ts +3 -16
  26. package/src/cli/Program.test.ts +3 -1
  27. package/src/cli/Program.ts +9 -0
  28. package/src/flows/scripts.ts +15 -0
  29. package/src/steering/index.ts +12 -1
  30. package/src/steering/review.test.ts +109 -20
  31. package/src/steering/review.ts +160 -9
  32. package/src/testkit/EmittedScriptRecognizer.ts +0 -14
  33. package/src/testkit/FlowScripts.test.ts +17 -0
  34. package/src/testkit/FlowScripts.ts +15 -0
  35. package/src/testkit/steps.ts +31 -0
  36. package/src/workflow-kit/review.test.ts +170 -26
  37. package/src/workflow-kit/review.ts +95 -19
  38. package/src/workflows/feature/coverage.feature +3 -3
  39. package/src/workflows/feature/fast-path.feature +2 -2
  40. package/src/workflows/feature/feature.test.ts +2 -37
  41. package/src/workflows/feature/feature.ts +12 -14
  42. package/src/workflows/feature/flag-mode.feature +2 -2
  43. package/src/workflows/feature/lens-review.feature +246 -0
  44. package/src/workflows/feature/one-step-land.feature +2 -2
  45. package/src/workflows/feature/quality-unresolved.feature +47 -32
  46. package/src/workflows/feature/refactor-absorbs.feature +3 -3
  47. package/src/workflows/feature/refactor-first.feature +5 -5
  48. package/src/workflows/feature/refactor-rebuild.feature +7 -7
  49. package/src/workflows/feature/refactor-review.feature +19 -100
  50. package/src/workflows/feature/refactor.test.ts +12 -27
  51. package/src/workflows/feature/refactor.ts +0 -1
  52. package/src/workflows/feature/review-answers.feature +179 -0
  53. package/src/workflows/feature/review-gate.feature +6 -6
  54. package/src/workflows/feature/review.test.ts +33 -77
  55. package/src/workflows/feature/review.ts +79 -47
  56. package/src/workflows/feature/seam-rejection.feature +2 -2
  57. package/src/workflows/feature/seam.feature +2 -2
  58. package/src/workflows/feature/seam.test.ts +1 -1
  59. package/src/workflows/feature/slicing.feature +6 -6
  60. package/src/workflows/feature/steps.test.ts +0 -12
  61. package/src/workflows/feature/steps.ts +3 -10
  62. package/src/workflows/index.ts +1 -0
  63. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
  64. package/src/workflows/shared/access.test.ts +2 -11
  65. package/src/workflows/shared/access.ts +0 -2
  66. package/src/workflows/shared/lenses.test.ts +39 -0
  67. package/src/workflows/shared/lenses.ts +176 -0
  68. package/src/workflows/shared/options.ts +1 -0
  69. package/src/workflows/shared/tail.ts +19 -4
  70. package/src/workflows/shared/text.test.ts +106 -88
  71. package/src/workflows/shared/text.ts +164 -53
  72. package/src/workflows/shared/vars.ts +1 -0
  73. package/dist/chunks/tail-lY1UmJrU.mjs +0 -112
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "41.0.0",
3
+ "version": "43.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
@@ -339,10 +339,10 @@ Then, in any repository:
339
339
  ```
340
340
 
341
341
  That starts a process from your requirements and drives it until it needs you.
342
- The requirements go to the top of `.gtd/TODO.md`; deferred review notes already
343
- there stay. A long, multi-line requirements paste still starts a process; if
344
- Claude Code never expanded the paste, gtd says it could not read it, and you add
345
- the requirements to the top of `.gtd/TODO.md` and run `/gtd`. `/gtd fix`,
342
+ The requirements go to the top of `.gtd/TODO.md`, above whatever it already
343
+ holds. A long, multi-line requirements paste still starts a process; if Claude
344
+ Code never expanded the paste, gtd says it could not read it, and you add the
345
+ requirements to the top of `.gtd/TODO.md` and run `/gtd`. `/gtd fix`,
346
346
  `/gtd review [base]`, `/gtd rollout <flag>` and `/gtd cleanup <flag>` (where the
347
347
  config exports them) take the doors described below; text whose word count does
348
348
  not fit a door's args (bare `/gtd rollout` included) starts a process from that
@@ -490,14 +490,22 @@ noted in steps 3 and 4 below.
490
490
  `spec-challenge` run on the planner model (`smart`), the other three on the
491
491
  coder model (`base`); a `qualityReviews` entry `<lens>=<model>` picks a
492
492
  lens's own. Each traces the change from its own angle and records every
493
- finding, blocking or not; one fix turn then fixes ALL of them once, with no
494
- re-review after the fix. A clean turn means approval only when that lens
495
- found nothing at all. This lap is where code quality is looked at, and every
496
- round pays for it. It never replaces step 4 — your review stays the final
497
- gate, and nothing here skips it. Findings the fix cap leaves in
498
- `.gtd/QUALITY.md` rest at `build.quality-unresolved` until you delete or
499
- empty the file; the suite then runs on any hand edits before the review. The
500
- `fix` door (below) repairs a red baseline through this same lap.
493
+ finding, blocking or not, as its own `## ` chunk of `.gtd/QUALITY.md`. A
494
+ clean turn means approval only when that lens found nothing at all. The
495
+ lenses are the only agentic review: a judge (`build.quality.triage`) then
496
+ classifies each finding — `mechanical` ones are fixed by the fix turn
497
+ (`build.fix.quality.fixing`, up to three turns), `noise` is dropped with its
498
+ `p` recorded as `Gtd-Mark: noise=<id> p=<p> <title>`, and `design` ones
499
+ (questioning a criterion or the approach) become notes for your review. An
500
+ unsure verdict (below `qualityTriageMinP`, `0.7`) is `design`; mechanical
501
+ findings the fix turns leave become notes too. This lap is where code quality
502
+ is looked at, and every round pays for it. It never replaces step 4 — your
503
+ review stays the final gate, and nothing here skips it. Between
504
+ [coarse slices](#coarse-slices) no review follows, so there the fix turn
505
+ fixes every finding, and those the fix cap leaves rest at
506
+ `build.quality-unresolved` until you delete or empty `.gtd/QUALITY.md`; the
507
+ suite then runs on any hand edits before the next slice. The `fix` door
508
+ (below) repairs a red baseline through the untriaged lap.
501
509
 
502
510
  A red suite that keeps failing past a few fix attempts escalates instead of
503
511
  retrying forever: an agent turn reads the failing output and writes
@@ -512,39 +520,43 @@ noted in steps 3 and 4 below.
512
520
  `Gtd-Mark: flaky=<check>` (see
513
521
  [Re-check before a person](docs/configuration.md#re-check-before-a-person)).
514
522
 
515
- 4. **You review.** After the last slice's quality lap you get a review document
516
- over the whole change, listing what changed and what to look at (on
517
- `refactor-first` the refactor slice gets one of its own first, before the
518
- feature is built; see [the naive pass](#the-naive-pass)). Before you see it,
519
- an automatic risk-fix pass (`build.review.fix.risks.fixing`) runs. Any risk
520
- the reviewer names (a note opening with `Risk:`) is fixed first, the suite
521
- kept green, and the review rewritten — once per review round, so a risk the
522
- rewrite still names reaches you unfixed; risk: a fix lands with no check that
523
- the risk was real. Tick the boxes to approve, or write what is wrong.
524
- Approving ends the process (at the refactor review, it moves on to the
525
- feature); feedback is judged note by note, each as `edit`, `question`, `nit`
526
- or `praise`. An `edit` sends the process back to step 2 for re-derived
527
- requirements — the built criteria keep their IDs (reworded in place when
528
- their behaviour changes) and new behaviour gets new ones — then the
529
- implementation is amended to meet them, with no new naive pass or seam judge,
530
- every criterion's coverage is judged again, the quality lap runs, and you
531
- review again. At the refactor review an `edit` re-derives the refactor
532
- instead. A `question` is answered inline under your note and the process
533
- stops at the review again — no new plan. A `nit` is fixed in one batch, the
534
- suite must go green (a red one gets fix turns first), then a fresh review of
535
- the change stops at the review again. `praise` is dropped; a round of only
536
- praise signs off. When a round mixes them, questions are answered and nits
537
- fixed first, then the edits are planned — risk: the planning lap may redo nit
538
- fixes it touches.
523
+ 4. **You review.** After the last slice's quality lap gtd writes the review
524
+ document itself (`build.review.composing`; no reviewer turn runs): one `## `
525
+ chunk per design note the lap left, each ending in three tick boxes — **fix
526
+ as suggested**, **won't fix**, or **your own answer** written over
527
+ `_your answer_` — then, for reference, no tick boxes, no answer needed — what
528
+ was **fixed automatically** (with the fixing commit) and what was **dropped
529
+ as noise**. On `refactor-first` the refactor slice gets one of its own first,
530
+ before the feature is built; see [the naive pass](#the-naive-pass). Review
531
+ the diff yourself (`gtd base` prints its base). **Every note needs exactly
532
+ one tick before `gtd land` accepts the review** (`answer-completeness`; an
533
+ open `- H:` thread waiting on the agent is the one exception). Fix as
534
+ suggested joins the nit batch below, unjudged; won't fix drops the note and
535
+ records it as `Gtd-Mark: wontfix=<title>` on the gate's commit; your own
536
+ answer is judged like a note. A note you answered is left out of the fresh
537
+ review after a nit fix; a round that also answered a question restores the
538
+ review instead, your ticks kept. Land with every note answered and nothing
539
+ else changed to approve, or write what is wrong — disagreeing with a fix or a
540
+ drop means writing a note of your own. Approving ends the process (at the
541
+ refactor review, it moves on to the feature); feedback is judged note by
542
+ note, each as `edit`, `question`, `nit` or `praise`. An `edit` sends the
543
+ process back to step 2 for re-derived requirements — the built criteria keep
544
+ their IDs (reworded in place when their behaviour changes) and new behaviour
545
+ gets new ones — then the implementation is amended to meet them, with no new
546
+ naive pass or seam judge, every criterion's coverage is judged again, the
547
+ quality lap runs, and you review again. At the refactor review an `edit`
548
+ re-derives the refactor instead. A `question` is answered inline under your
549
+ note and the process stops at the review again — no new plan. A `nit` is
550
+ fixed in one batch, the suite must go green (a red one gets fix turns first),
551
+ then a fresh review of the change stops at the review again. `praise` is
552
+ dropped; a round of only praise signs off. When a round mixes them, questions
553
+ are answered and nits fixed first, then the edits are planned — risk: the
554
+ planning lap may redo nit fixes it touches.
539
555
 
540
556
  Each `edit` note also gets a severity tier from four judge questions
541
- (security, reversibility, blast radius, new concept). `critical` and `should`
542
- notes are re-planned this round; `could` notes are deferred. A leading `!` on
543
- a note forces `critical` and skips the judge. Deferral appends to the
544
- committed `.gtd/TODO.md`; it never starts a process on its own, and it ships
545
- in the branch/PR unless you delete it. Landing clean at the gate after a
546
- deferral signs off without the deferred notes; the gate lists them. To pull
547
- one in, edit its line in `.gtd/TODO.md` when you start a process.
557
+ (security, reversibility, blast radius, new concept). Every tier is
558
+ re-planned this round, `critical` first, then `should`, then `could`; nothing
559
+ is deferred. A leading `!` on a note forces `critical` and skips the judge.
548
560
  `reviewSeverityMinP` sets the confidence floor (see
549
561
  [configuration](docs/configuration.md)). A hand-edit to code always plans a
550
562
  lap. Only a confident non-`edit` verdict skips the replan — a note whose
@@ -584,14 +596,16 @@ gtd door review <commitish>
584
596
  ```
585
597
 
586
598
  starts a pure review of everything from `<commitish>` to HEAD. It goes straight
587
- to step 4, with no planning and no building. Sign off and it closes. Leave notes
588
- and the process ends with `.gtd/REVIEW.md` (and the reviewer's original,
589
- `.gtd/REVIEW.orig.md`) left for `gtd door fix`. Until `fix` consumes them,
590
- starting `feature` or `review` is refused; remove both files to abandon the
591
- hand-off. `gtd door fix` fixes those notes and every `Risk:` in one turn, then
592
- asks for your review of the fix. With no review waiting, it repairs a red
593
- baseline as its own reviewed commit instead. `gtd doors` lists the doors of the
594
- workflows your `gtd.config.ts` exports, plus any it adds.
599
+ to the quality lap and a review, with no planning and no building; unlike
600
+ `feature`, its lap fixes every finding and a reviewer turn
601
+ (`build.review.reviewing`) writes `.gtd/REVIEW.md`, risks and all. Sign off and
602
+ it closes. Leave notes and the process ends with `.gtd/REVIEW.md` (and the
603
+ reviewer's original, `.gtd/REVIEW.orig.md`) left for `gtd door fix`. Until `fix`
604
+ consumes them, starting `feature` or `review` is refused; remove both files to
605
+ abandon the hand-off. `gtd door fix` fixes those notes and every `Risk:` in one
606
+ turn, then asks for your review of the fix. With no review waiting, it repairs a
607
+ red baseline as its own reviewed commit instead. `gtd doors` lists the doors of
608
+ the workflows your `gtd.config.ts` exports, plus any it adds.
595
609
  `gtd --workflow <name>` starts any workflow by name.
596
610
 
597
611
  The workflow itself is a plain async TypeScript function, and the
@@ -730,6 +744,7 @@ full trail with `gtd history fetch | sh`, then `gtd history show <ref>`.
730
744
  gtd history list # one line per kept process
731
745
  gtd history show <ref> # replay its steps, judge verdicts and approvals
732
746
  gtd history metrics [ref...] # how the kept processes went, per process and in total
747
+ gtd history judges [ref...] # how often each judge's verdicts were overturned
733
748
  gtd history fetch | sh # pull every kept ref from origin
734
749
  ```
735
750
 
@@ -762,6 +777,45 @@ process kept from that branch. A rate is `null` without a decided slice. Each
762
777
  process names its `workflow` (`null` for the default one); the total spans every
763
778
  ref measured, so name the refs to leave another workflow's processes out.
764
779
 
780
+ `gtd history judges` calibrates three of the bundled `feature` workflow's judges
781
+ from the `Gtd-Judge` verdicts the kept refs recorded. It never replays and asks
782
+ no model. A verdict is **overturned** when later history contradicts it:
783
+
784
+ - **retry** (`health.judge`) — an `identical` verdict, when the next fix turn's
785
+ check went green anyway (no outcome when a person edited code in between)
786
+ - **triage** (`quality.triage`) — a `mechanical` or `noise` verdict the review
787
+ listed under _Fixed automatically_ or _Dropped as noise_, when the human
788
+ edited that finding's line there or wrote under it
789
+ - **clear** (`design.clear`) — a `yes` that skipped design (landed on
790
+ `design.normalise`), when a later human design step in the same process
791
+ changed the acceptance criteria
792
+
793
+ Any other verdict has no outcome. The report counts verdicts, scored verdicts
794
+ and overturns per judge and per `p` bucket (`0.8-0.9: 1/2` — one of two
795
+ overturned). `--var <threshold>=<value>` re-decides every past verdict under
796
+ another threshold and prints the decisions that would change, against the
797
+ threshold each process recorded (a process that recorded none is left out):
798
+
799
+ ```sh
800
+ gtd history judges --var qualityTriageMinP=0.9
801
+ # what-if qualityTriageMinP=0.9: 7 noise drops become notes (2 overturned)
802
+ ```
803
+
804
+ The thresholds are `judgeIdenticalMinP` (retry) and `qualityTriageMinP`
805
+ (triage). The judge's evidence-budget cut is not recorded, so a cut verdict is
806
+ scored and re-decides as if it was whole. A retry verdict on an empty report is
807
+ not scored, as the flow drops it. `--json` prints
808
+ `{ processes, total, whatIf }`, each process and the total with its `judges`.
809
+ `--jsonl` prints one line per verdict instead — `process`, `judge`, `commit`,
810
+ `step`, `question`, `state` (the evidence the tree still shows: the finding, the
811
+ two check reports, the sketch without its numbered criteria), `answer`, `p` and
812
+ `outcome` (`overturned`, `held` or `null`) — ready to fine-tune a local model
813
+ on:
814
+
815
+ ```sh
816
+ gtd history judges --jsonl > judges.jsonl
817
+ ```
818
+
765
819
  `gtd history fetch` only prints a fetch script; pipe it into `sh` to run it.
766
820
  Beyond that one ship push, refs are not pushed or fetched by default. The ref
767
821
  holds every step commit, including steering files and code a later step removed,
@@ -818,8 +872,8 @@ install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
818
872
  The `gtd` bin's commands — `next`, `land`, `abandon`, `restore`, `validate`,
819
873
  `check`, `uncheck`, `install`, `summary`, `base`, `door`, `doors`, `slices`,
820
874
  `version`, `help`, `history list`, `history show`, `history metrics`,
821
- `history fetch`, `strays`, and the tools `ui`, `lsp`, `loop`, `context-hook`,
822
- `judge` (`judge answer`, `judge run`) — are listed in the
875
+ `history judges`, `history fetch`, `strays`, and the tools `ui`, `lsp`, `loop`,
876
+ `context-hook`, `judge` (`judge answer`, `judge run`) — are listed in the
823
877
  [CLI reference](https://github.com/pmelab/gtd/blob/main/docs/cli.md).
824
878
  `gtd slices` lists the spans a flow's `slice()` calls mark. It refuses (exit 1)
825
879
  when HEAD neither closes nor sits inside a process, or when today's config
@@ -1,5 +1,5 @@
1
1
  // New requirements go above what the steering file already holds, untouched:
2
- // a deferred review note the start diff alters reads as the human's edit.
2
+ // anything already there that the start diff alters reads as the human's edit.
3
3
  export function withRequirements(existing: string, requirements: string): string {
4
4
  const head = `${requirements.trimEnd()}\n`
5
5
  return existing.trim() ? `${head}\n${existing}` : head
@@ -1,4 +1,5 @@
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
+ import { d as ANSWER_FIX, f as ANSWER_LEAD, p as ANSWER_WONT_FIX, v as FREE_TEXT_PLACEHOLDER } from "./steering-D4LUQ-av.mjs";
2
3
  import { escalation as escalation$1, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
3
4
  import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$, s as Number$ } from "./Schema-C6fLlQzx.mjs";
4
5
  //#region src/workflows/shared/registry.ts
@@ -130,6 +131,7 @@ const defaults = {
130
131
  judgeIdenticalMinP: "0.7",
131
132
  reviewNoteActionable: "0.7",
132
133
  reviewSeverityMinP: "0.7",
134
+ qualityTriageMinP: "0.7",
133
135
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
134
136
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
135
137
  flagMode: "true",
@@ -420,6 +422,51 @@ carries it.
420
422
  What each change does next (then run \`gtd land\`):
421
423
  - **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
422
424
  `;
425
+ const qualityTriageMessage = () => `Judging each quality finding. Run \`gtd judge answer\` and pipe a
426
+ \`mechanical\`/\`noise\`/\`design\` verdict per finding ID — or land without a
427
+ verdict, which hands every finding to the review as a note.
428
+ `;
429
+ /** \`.gtd/QUALITY.md\` holding only \`findings\`, one \`## \` chunk each. */
430
+ const qualityFindingsFile = (findings) => `# Quality\n\n${findings.map((f) => `## ${f.title}\n\n${f.body}`.trim()).join("\n\n")}\n`;
431
+ const SUMMARY_TITLES = [
432
+ "Weakened scenarios",
433
+ "Fixed from your notes",
434
+ "Fixed automatically",
435
+ "Dropped as noise",
436
+ "Nothing to note"
437
+ ];
438
+ const unboxed = (body) => body.replace(/^(\s*(?:>\s*)*(?:[-*+]|\d+[.)])\s+)\[[ xX]\]\s?/gm, "$1");
439
+ const short = (hash) => hash.slice(0, 7);
440
+ /**
441
+ * \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
442
+ * answer boxes, then the fixed and noise lists. Chunk titles are made unique —
443
+ * notes and answers are matched to their chunk by title.
444
+ */
445
+ const composedReview = (base, report) => {
446
+ const used = new Set(SUMMARY_TITLES);
447
+ const unique = (title) => {
448
+ let name = title;
449
+ for (let n = 2; used.has(name); n++) name = `${title} (${n})`;
450
+ used.add(name);
451
+ return name;
452
+ };
453
+ 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}`;
455
+ const notes = report.notes.map((n) => ({
456
+ ...n,
457
+ title: unique(n.title)
458
+ })).filter((n) => report.answered?.has(n.title) !== true);
459
+ const weakened = (report.weakened ?? []).flatMap(({ path, lines }) => lines.map((a) => `- ${path} — ${a.scenario} — ${a.line}`));
460
+ const chunks = [
461
+ ...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
+ ...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}`)),
464
+ ...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
465
+ ...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
466
+ ];
467
+ const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
468
+ return `# Review: ${short(base)}\n\n<!-- base: ${base} -->\n\n${body.join("\n\n")}\n`;
469
+ };
423
470
  const qualityUnresolvedMessage = () => `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
424
471
  holds findings. Nothing after this fixes them, so the process will not
425
472
  go on with them open.
@@ -496,10 +543,6 @@ ${stateFileRules}
496
543
  ${footnoteFoldIn}${codeThreadReplies()}
497
544
  - The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
498
545
  — no other files for notes or output
499
- - Lines of a \`## Deferred review notes\` section of \`.gtd/TODO.md\`
500
- that the start diff does not touch are parked by review, never folded;
501
- the human's edits to such a line (or its removal) are input like any
502
- other part of the start diff
503
546
  - \`.gtd/TODO.md\` is the likely home of the sketch that started
504
547
  this process — the human's input, folded into the concerns
505
548
  below like any other part of the start diff; never a state
@@ -728,8 +771,10 @@ const finisherSystem = () => `${finisherPersona}
728
771
  ${agentConduct}`;
729
772
  const buildFixQualityPrompt = (rules) => `${stateFileRules}
730
773
  - Read \`.gtd/QUALITY.md\` — one \`## \` chunk per finding a quality
731
- dimension wrote. Merge duplicate findings across dimensions
732
- FIRST, then fix every finding in every chunk, blocking or not
774
+ dimension wrote. Fix every finding in every chunk, blocking or not;
775
+ where dimensions repeat one, fix the shared cause once
776
+ - Delete each finding's \`## \` chunk once it is resolved; never
777
+ rename, merge or reword a chunk you leave
733
778
  - When findings conflict, missing test signal beats line count —
734
779
  a test is never deleted to satisfy a simplification finding
735
780
  - Delete \`.gtd/QUALITY.md\` once every finding is resolved
@@ -772,9 +817,9 @@ const buildQualityReviewingPrompt = (lens, brief, base = start()) => `${stateFil
772
817
  behaviour removed — decide that by reasoning, never by running a
773
818
  mutation-testing tool. A test that pins a bug is a finding, not
774
819
  praise
775
- - APPEND every finding you have, blocking or not, as a \`## \`
776
- chunk to \`.gtd/QUALITY.md\` — never overwrite what an earlier
777
- dimension already wrote there
820
+ - APPEND every finding you have, blocking or not, as its own
821
+ \`## <one-line title>\` chunk to \`.gtd/QUALITY.md\` — one finding per
822
+ chunk, never overwrite what an earlier dimension already wrote there
778
823
  - Write nothing only when this lens found nothing at all — then a
779
824
  clean turn IS this dimension's approval
780
825
  ${brief ? `
@@ -786,12 +831,16 @@ ${brief}
786
831
  const reviewerSystem = () => `${reviewerPersona}
787
832
 
788
833
  ${agentConduct}`;
789
- const buildReviewAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\` holds the review record for the process — one
790
- \`- [ ]\` checkbox per reviewable item, grouped into chunks. Tick a box
791
- (\`- [x]\`) as you review each hunk; ticking only records that you've read
792
- it, it is not sign-off. Ticks are read-progress only: landing clears
793
- every box back to \`- [ ]\` on disk and nothing records which hunks
794
- you read — there is no persisted trail of it.
834
+ const buildReviewAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\` holds what the quality lenses left for you: one chunk
835
+ per finding that needs your judgement — it questions a criterion or the
836
+ approach, or the fixer could not resolve it — then, for reference only, what
837
+ was **fixed automatically** (with the fixing commit) and what was **dropped
838
+ as noise**. Each finding chunk ends in three boxes — tick exactly one:
839
+ \`fix as suggested\` (fixed in the nit batch below), \`won't fix\` (dropped,
840
+ recorded in the commit), or write your own answer over \`_your answer_\` and
841
+ tick it (judged like a note). Every landing here is refused until each
842
+ finding holds exactly one tick. The fixed and noise lists need no answer;
843
+ disagreeing with a fix or a drop means writing a note of your own.
795
844
 
796
845
  Review the diff yourself, with whatever tool you like — gtd checks
797
846
  nothing out and touches no ref; \`gtd base\` prints this same hash any
@@ -801,9 +850,9 @@ working tree:
801
850
  git diff ${base}
802
851
  When you've been through the whole diff, run \`gtd land\`:
803
852
 
804
- - **Sign off** — leave no comment — no note in
805
- \`.gtd/REVIEW.md\`, no code edit — to close the process,
806
- whatever the boxes say. Every turn commit stays on the
853
+ - **Sign off** — answer every finding, leave no other comment — no note
854
+ in \`.gtd/REVIEW.md\`, no code edit — to close the process.
855
+ Every turn commit stays on the
807
856
  branch; run \`gtd summary\` afterward for a closing-message
808
857
  prompt.
809
858
  - **Ask a question** — start a thread (\`- H: <question>\`) on a
@@ -949,27 +998,6 @@ const wrapItem = (item) => {
949
998
  } else line = line === "" ? word : `${line} ${word}`;
950
999
  return [...out, line];
951
1000
  };
952
- /** Appends one section of `notes` to `.gtd/TODO.md`, wrapped the way oxfmt writes it; an existing file is only ever appended to. */
953
- const deferNotesScript = (notes, round) => {
954
- return `mkdir -p .gtd
955
- if [ -s .gtd/TODO.md ]; then
956
- [ -z "$(tail -c1 .gtd/TODO.md)" ] || printf '\\n' >> .gtd/TODO.md
957
- printf '\\n' >> .gtd/TODO.md
958
- fi
959
- printf '%s\\n' ${[
960
- `## Deferred review notes (${round.slice(0, 7)})`,
961
- "",
962
- ...notes.flatMap((n) => wrapItem(`- ${noteHead(n)}: ${n.text.replace(/\s+/g, " ")}`))
963
- ].map(quote).join(" ")} >> .gtd/TODO.md
964
- `;
965
- };
966
- /** The gate right after a deferral: landing clean signs off without these notes. */
967
- const buildReviewAwaitAfterDeferMessage = (base, deferred) => `${buildReviewAwaitReviewMessage(base)}
968
- Deferred to \`.gtd/TODO.md\` just now, NOT acted on in this process — landing
969
- clean signs off without them:
970
-
971
- ${deferred.map((n) => `- ${noteHead(n)}: ${n.text.replace(/\s*\n\s*/g, " ")}`).join("\n")}
972
- `;
973
1001
  const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
974
1002
  footnote, or prose under a chunk) with one verdict: \`edit\` (a change
975
1003
  request), \`question\`, \`nit\` (a small fix needing no re-plan) or
@@ -979,16 +1007,18 @@ default; a skipped judgment never dismisses a note).
979
1007
 
980
1008
  Also judge each note's severity, one id per note: \`note-<n>.security\`,
981
1009
  \`note-<n>.reversibility\`, \`note-<n>.blastRadius\` and
982
- \`note-<n>.newConcept\`.
1010
+ \`note-<n>.newConcept\`. An own answer under a finding's boxes is judged
1011
+ the same way, keyed \`answer-<n>\`.
983
1012
  `;
984
1013
  const closedFold = (round, target) => round === "edits" ? `- Fold every hand-edit and every note the human left that round into
985
- \`${target}\`
1014
+ \`${target}\` — a note ticked \`fix as suggested\` or answered in its
1015
+ own words included; one ticked \`won't fix\` is dropped
986
1016
  ` : round === "notes" ? `- Fold each note that asks for a change into \`${target}\`
987
1017
  ` : `- Fold each \`edit\` note the capture lists, no other note, and every
988
1018
  hand-edit, into \`${target}\`
989
1019
  `;
990
1020
  /** `refactor` retargets the prompt at `.gtd/REFACTOR.md` and fences it to the strays. A concern outside the fence is not folded; if nothing at all is written the landing is refused, but a partly buildable round drops the rest silently. */
991
- const buildReviewCollectingPrompt = (capture, refactor) => {
1021
+ const buildReviewCollectingPrompt = (capture, refactor, round) => {
992
1022
  const target = refactor ? ".gtd/REFACTOR.md" : ".gtd/REQUIREMENTS.md";
993
1023
  const fence = refactor ? `- The next refactor may change only these paths, plus an existing test
994
1024
  file's call sites, and never a flag read:
@@ -1063,12 +1093,16 @@ ${refactor ? refactor.round === "reply" ? `- This turn replies inside \`.gtd/REV
1063
1093
  ${closedFold(refactor.round, target)}` : `- This turn writes \`${target}\` (the folded concerns) and
1064
1094
  replies inside \`.gtd/REVIEW.md\` (thread replies only; the file stays
1065
1095
  in the tree) — you classify, you do not build
1066
- - Fold every concluded thread, ticked answer and hand-edit — and each
1067
- \`edit\` note the capture lists, no other note — into
1068
- \`${target}\` and delete the folded threads from
1069
- \`.gtd/REVIEW.md\`; append one \`- A:\` reply to each thread
1070
- whose last entry is a \`- H:\` question
1071
- `}- When the capture names an answering commit, carry each answered
1096
+ - Fold every concluded thread and hand-edit — and each \`edit\` note
1097
+ the capture lists, no other note — into \`${target}\` and
1098
+ delete the folded threads from \`.gtd/REVIEW.md\`; append one \`- A:\`
1099
+ reply to each thread whose last entry is a \`- H:\` question
1100
+ ${round === "edits" ? `- A hand-edit round routes no answer box itself: fold each note
1101
+ ticked \`fix as suggested\` or answered in its own words, too; drop one
1102
+ ticked \`won't fix\`
1103
+ ` : `- Never fold a note's ticked answer box the capture does not list: gtd
1104
+ routes those itself
1105
+ `}`}- When the capture names an answering commit, carry each answered
1072
1106
  question into \`${target}\`'s \`## Answered Questions\` as
1073
1107
  \`### <the note>\` plus its answer — the lap replaces \`.gtd/REVIEW.md\`
1074
1108
  - Keep capture order, and carry each note's \`(tier: reason)\`
@@ -1691,6 +1725,7 @@ const ShippedOptions = Struct({
1691
1725
  judgeIdenticalMinP: setting,
1692
1726
  reviewNoteActionable: setting,
1693
1727
  reviewSeverityMinP: setting,
1728
+ qualityTriageMinP: setting,
1694
1729
  refactorFlagPattern: setting,
1695
1730
  seamAllow: setting,
1696
1731
  sliceMaxCriteria: setting,
@@ -1785,11 +1820,9 @@ const skills = (vars) => {
1785
1820
  const access = (vars) => ({
1786
1821
  design: { write: [] },
1787
1822
  "build.review": { write: [".gtd/REVIEW.md"] },
1788
- "build.review.defer": { write: [".gtd/TODO.md"] },
1789
1823
  "build.review.fix.nits": {},
1790
1824
  "build.review.fix.risks": {},
1791
1825
  "refactor.build.review": { write: [".gtd/REVIEW.md"] },
1792
- "refactor.build.review.defer": { write: [".gtd/TODO.md"] },
1793
1826
  ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
1794
1827
  ...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), () => ({ write: [] }))
1795
1828
  });
@@ -1801,4 +1834,4 @@ const steering = {
1801
1834
  [REVIEW]: "review"
1802
1835
  };
1803
1836
  //#endregion
1804
- export { naiveApplyFailure as $, agentWithSkills as A, coverageQuestion as B, fixCheck as C, testFileGlobs as Ct, reviewMissing as D, registryEntry as Dt, planner as E, flipProblems as Et, buildReviewTriageMessage as F, designSystem as G, criterionLine as H, builderSystem as I, fixReviewNotesPrompt as J, designTriagePrompt as K, clearCriteriaMessage as L, buildReviewAwaitAfterDeferMessage as M, buildReviewCollectingPrompt as N, reviewQuality as O, requireFlag as Ot, buildReviewFixNitsPrompt as P, implementFixPrompt as Q, clearCriteriaQuestion as R, coder as S, envDefaults as St, fixRisks as T, flagReferences as Tt, deferNotesScript as U, coveringPrompt as V, designGateAnswerMessage as W, idleMessage as X, handOffAwaitReviewMessage as Y, implementBuildingPrompt as Z, REFACTOR_NOTE as _, startGateBlockedMessage as _t, flagModeOn as a, refactorDroppedRefusal as at, SLICES as b, unwindFailure as bt, baseline as c, refactorRules as ct, guarded as d, reviewGateBlockedMessage as dt, naivePassPrompt as et, healthy as f, reviewNotesCapture as ft, REFACTOR as g, slicingPrompt as gt, QUALITY as h, seamSteerMessage as ht, skills as i, refactorBuildingPrompt as it, buildReviewAnswerQuestionsPrompt as j, reviewing as k, escalation as l, reviewEditNotesCapture as lt, HANDOFF as m, seamByHandMessage as mt, summary as n, oversizedMessage as nt, flagShipped as o, refactorFixPrompt as ot, FEEDBACK as p, reviewerSystem as pt, finisherSystem as q, access as r, qualityUnresolvedMessage as rt, shipped as s, refactorNotePrompt as st, steering as t, normaliseCriteriaScript as tt, gate as u, reviewEditsCapture as ut, REQUIREMENTS as v, uncoveredMessage as vt, fixQuality as w, flagMentions as wt, awaitReview as x, defaults as xt, REVIEW as y, unwindFailedMessage as yt, coverageMessage as z };
1837
+ export { naivePassPrompt as $, agentWithSkills as A, coverageQuestion as B, fixCheck as C, envDefaults as Ct, reviewMissing as D, flipProblems as Dt, planner as E, flagReferences as Et, builderSystem as F, designTriagePrompt as G, criterionLine as H, clearCriteriaMessage as I, handOffAwaitReviewMessage as J, finisherSystem as K, clearCriteriaQuestion as L, buildReviewCollectingPrompt as M, buildReviewFixNitsPrompt as N, reviewQuality as O, registryEntry as Ot, buildReviewTriageMessage as P, naiveApplyFailure as Q, composedReview as R, coder as S, defaults as St, fixRisks as T, flagMentions as Tt, designGateAnswerMessage as U, coveringPrompt as V, designSystem as W, implementBuildingPrompt as X, idleMessage as Y, implementFixPrompt as Z, REFACTOR_NOTE as _, slicingPrompt as _t, flagModeOn as a, refactorBuildingPrompt as at, SLICES as b, unwindFailedMessage as bt, baseline as c, refactorNotePrompt as ct, guarded as d, reviewEditsCapture as dt, normaliseCriteriaScript as et, healthy as f, reviewGateBlockedMessage as ft, REFACTOR as g, seamSteerMessage as gt, QUALITY as h, seamByHandMessage as ht, skills as i, qualityUnresolvedMessage as it, buildReviewAnswerQuestionsPrompt as j, reviewing as k, requireFlag as kt, escalation as l, refactorRules as lt, HANDOFF as m, reviewerSystem as mt, summary as n, qualityFindingsFile as nt, flagShipped as o, refactorDroppedRefusal as ot, FEEDBACK as p, reviewNotesCapture as pt, fixReviewNotesPrompt as q, access as r, qualityTriageMessage as rt, shipped as s, refactorFixPrompt as st, steering as t, oversizedMessage as tt, gate as u, reviewEditNotesCapture as ut, REQUIREMENTS as v, startGateBlockedMessage as vt, fixQuality as w, testFileGlobs as wt, awaitReview as x, unwindFailure as xt, REVIEW as y, uncoveredMessage as yt, coverageMessage as z };