@pmelab/gtd 24.3.0 → 25.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 (74) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +28 -22
  3. package/dist/chunks/{reserved-DJwU5C4z.mjs → reserved-6pAgFGgB.mjs} +119 -135
  4. package/dist/chunks/{steering-D-PvRC5W.mjs → steering-DVILanVf.mjs} +16 -5
  5. package/dist/flows.mjs +9 -1
  6. package/dist/gtd.bundle.mjs +780 -813
  7. package/dist/steering.mjs +2 -2
  8. package/dist/testkit.mjs +666 -161
  9. package/dist/workflow-kit.mjs +55 -96
  10. package/dist/workflows/feature.mjs +171 -76
  11. package/dist/workflows/fix.mjs +2 -2
  12. package/dist/workflows/review.mjs +1 -1
  13. package/dist/workflows.mjs +353 -310
  14. package/package.json +1 -2
  15. package/src/cli/Program.test.ts +3 -8
  16. package/src/cli/Program.ts +1 -8
  17. package/src/edge/Edge.test.ts +5 -2
  18. package/src/edge/Edge.ts +3 -4
  19. package/src/emit/index.ts +1 -0
  20. package/src/flows/scripts.ts +6 -0
  21. package/src/platform/Git.test.ts +6 -9
  22. package/src/replay/Replay.ts +2 -1
  23. package/src/steering/FORMATS.md +1 -1
  24. package/src/steering/SteeringFormat.test.ts +9 -1
  25. package/src/steering/SteeringFormat.ts +7 -0
  26. package/src/steering/contract.test.ts +3 -0
  27. package/src/steering/index.ts +1 -0
  28. package/src/steering/review.test.ts +30 -0
  29. package/src/steering/review.ts +8 -2
  30. package/src/testkit/EmittedScriptRecognizer.test.ts +29 -30
  31. package/src/testkit/EmittedScriptRecognizer.ts +148 -51
  32. package/src/testkit/EmittedScripts.test.ts +166 -0
  33. package/src/testkit/FlowScripts.test.ts +242 -0
  34. package/src/testkit/FlowScripts.ts +326 -0
  35. package/src/testkit/GitTiers.ts +140 -49
  36. package/src/testkit/InMemRepo.ts +35 -2
  37. package/src/testkit/Layers.ts +10 -2
  38. package/src/testkit/TestRepo.ts +4 -1
  39. package/src/testkit/index.ts +1 -1
  40. package/src/workflow/discovery.test.ts +147 -0
  41. package/src/workflow/discovery.ts +5 -0
  42. package/src/workflow/fingerprint.test.ts +20 -4
  43. package/src/workflow/fingerprint.ts +8 -9
  44. package/src/workflow/index.ts +1 -6
  45. package/src/workflow/load.test.ts +5 -19
  46. package/src/workflow/load.ts +94 -10
  47. package/src/workflow-kit/review.test.ts +84 -0
  48. package/src/workflow-kit/review.ts +9 -6
  49. package/src/workflow-kit/scenarios.test.ts +116 -0
  50. package/src/workflow-kit/scenarios.ts +53 -113
  51. package/src/workflows/feature/feature.ts +2 -5
  52. package/src/workflows/feature/packages.test.ts +17 -34
  53. package/src/workflows/feature/packages.ts +13 -52
  54. package/src/workflows/feature/planning.ts +1 -1
  55. package/src/workflows/feature/review.test.ts +233 -54
  56. package/src/workflows/feature/review.ts +178 -41
  57. package/src/workflows/feature/steps.test.ts +12 -0
  58. package/src/workflows/feature/steps.ts +8 -1
  59. package/src/workflows/index.ts +0 -2
  60. package/src/workflows/shared/access.test.ts +4 -2
  61. package/src/workflows/shared/access.ts +1 -0
  62. package/src/workflows/shared/health.test.ts +1 -21
  63. package/src/workflows/shared/health.ts +4 -39
  64. package/src/workflows/shared/options.test.ts +8 -0
  65. package/src/workflows/shared/options.ts +1 -1
  66. package/src/workflows/shared/steps.ts +18 -11
  67. package/src/workflows/shared/tail.test.ts +1 -41
  68. package/src/workflows/shared/tail.ts +17 -41
  69. package/src/workflows/shared/text.fixture.ts +1 -0
  70. package/src/workflows/shared/text.test.ts +129 -41
  71. package/src/workflows/shared/text.ts +111 -75
  72. package/src/workflows/shared/vars.ts +1 -1
  73. package/src/workflows/shared/scenarios.test.ts +0 -256
  74. package/src/workflows/shared/scenarios.ts +0 -35
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "24.3.0",
3
+ "version": "25.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
@@ -44,12 +44,6 @@ the read side to its agent. `read` is not a security boundary unless your driver
44
44
  enforces it at the OS level. See
45
45
  [Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#file-access).
46
46
 
47
- > **`fastTestCommand` is required, with no fallback to `testCommand`.** Set it
48
- > (everything but e2e) as a workflow option in `gtd.config.ts`
49
- > (`feature({ fastTestCommand: "npm run test:fast" })`) or as
50
- > `GTD_FASTTESTCOMMAND`; gtd stops at the start gate, writing `.gtd/SETUP.md`,
51
- > until it is.
52
-
53
47
  > **A repository's `gtd.config.ts` is code, and gtd runs it.** A custom workflow
54
48
  > is a TypeScript module, and every gtd command that looks at workflow state —
55
49
  > `gtd next`, `gtd lsp`, `gtd door` and `gtd doors` included, not just
@@ -400,20 +394,21 @@ noted in steps 3 and 4 below.
400
394
  questions and all. Every plan gets the technical pass: its document has four
401
395
  sections, in order — `## Interfaces`, `## Call Stacks`, `## E2E Scenarios`,
402
396
  `## Unit Tests` — behind a leading `## Open Questions` when there are any.
403
- `## E2E Scenarios` is never empty: it holds the scenarios, or, when nothing
397
+ `## E2E Scenarios` is never empty: it names each user-visible behaviour in
398
+ plain words (the builder writes the scenario itself), or, when nothing
404
399
  user-visible changes, the line `No e2e change.` with a one-line reason.
405
400
 
406
- 3. **You wait.** The work is split into packages and built one at a time, each
407
- one starting from the unit tests it declares (a build turn missing one is
408
- refused), checked against the fast suite and fixed until it passes before
409
- moving on. A package that rewords a frozen `.feature` step stops at a wording
410
- gate: accept the change, or reject it and the original is restored. After the
411
- last package a full run (e2e included) has its own fix loop before the
412
- quality lap. Two points along the process are judged rather than always
413
- asking you outright — each stops and hands you a verdict to make
414
- (`gtd judge answer`, or land with a clean tree to accept the conservative
415
- default, which never skips work;
416
- `gtd judge run --provider fixed --answers <path>` — or the
401
+ 3. **You wait.** The work is split into vertical slices and built one at a time,
402
+ each one starting from the unit and e2e tests it declares (a build turn
403
+ missing one is refused), checked against the full suite (`testCommand`) and
404
+ fixed until it passes before moving on — so keep that suite fast; gtd has no
405
+ lighter one. Scenario wording is not frozen: you sign the scenarios off once,
406
+ in the review's `## Scenarios` chunk, where any assertion an existing
407
+ scenario lost is marked `Weakened:`. After the last package the quality lap
408
+ runs. Two points along the process are judged rather than always asking you
409
+ outright — each stops and hands you a verdict to make (`gtd judge answer`, or
410
+ land with a clean tree to accept the conservative default, which never skips
411
+ work; `gtd judge run --provider fixed --answers <path>` — or the
417
412
  `GTD_JUDGE_ANSWERS` env var, inline JSON — answers one from a file, piped
418
413
  between `gtd judge --json` and `gtd judge answer`;
419
414
  `gtd judge run --provider jev` asks TypeSafe's Jev instead, with the key in
@@ -475,10 +470,21 @@ noted in steps 3 and 4 below.
475
470
  turns first), then a fresh review of the change stops at the review again.
476
471
  `praise` is dropped; a round of only praise signs off. When a round mixes
477
472
  them, questions are answered and nits fixed first, then the edits are planned
478
- — risk: the planning lap may redo nit fixes it touches. A hand-edit to code
479
- always plans a lap. Only a confident non-`edit` verdict skips the replan — a
480
- note whose evidence was cut, or that got no verdict, counts as `edit`. The
481
- same `gtd judge answer` / conservative-default shape as step 3's own judged
473
+ — risk: the planning lap may redo nit fixes it touches.
474
+
475
+ Each `edit` note also gets a severity tier from four judge questions
476
+ (security, reversibility, blast radius, new concept). `critical` and `should`
477
+ notes are re-planned this round; `could` notes are deferred. A leading `!` on
478
+ a note forces `critical` and skips the judge. Deferral appends to the
479
+ committed `.gtd/TODO.md`; it never starts a process on its own, and it ships
480
+ in the branch/PR unless you delete it. Landing clean at the gate after a
481
+ deferral signs off without the deferred notes; the gate lists them. To pull
482
+ one in, edit its line in `.gtd/TODO.md` when you start a process.
483
+ `reviewSeverityMinP` sets the confidence floor (see
484
+ [configuration](docs/configuration.md)). A hand-edit to code always plans a
485
+ lap. Only a confident non-`edit` verdict skips the replan — a note whose
486
+ evidence was cut, or that got no verdict, counts as `edit`. The same
487
+ `gtd judge answer` / conservative-default shape as step 3's own judged
482
488
  points.
483
489
 
484
490
  You never talk to it. Every exchange is a file in `.gtd/` that you edit in your
@@ -1,7 +1,6 @@
1
- import { accessFor, agent, answered, check, codeThreads, defineWorkflow, env, head, human, judge, numeric, quote, read, refuse, removeScript, run, scope, skillsFor, start, vars } from "../flows.mjs";
2
- import { escalation as escalation$1, fixFindings, gate as gate$1, healthy as healthy$1, holdWording as holdWording$1, lensScopes, lensesOf, qualityLap as qualityLap$1, reviewToHandOff, untilSettled } from "../workflow-kit.mjs";
1
+ import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, env, head, human, judge, numeric, quote, read, refuse, removeScript, run, scope, showScript, skillsFor, start, vars } from "../flows.mjs";
2
+ import { escalation as escalation$1, fixFindings, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, qualityLap as qualityLap$1, removedAssertions, reviewToHandOff } from "../workflow-kit.mjs";
3
3
  import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct } from "./Schema-B5yK7QrT.mjs";
4
- import { dirname } from "node:path";
5
4
  //#region src/workflows/shared/prose.ts
6
5
  const skillsPreamble = `- Load whatever's listed here that your harness actually has installed,
7
6
  skip anything it doesn't — silently, never stopping or asking about a
@@ -310,6 +309,10 @@ ${stateFileRules}
310
309
  ${footnoteFoldIn}${codeThreadReplies()}
311
310
  - The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
312
311
  — no other files for notes or output
312
+ - Lines of a \`## Deferred review notes\` section of \`.gtd/TODO.md\`
313
+ that the start diff does not touch are parked by review, never folded;
314
+ the human's edits to such a line (or its removal) are input like any
315
+ other part of the start diff
313
316
  - \`.gtd/TODO.md\` is the likely home of the sketch that started
314
317
  this process — the human's input, folded into the concerns
315
318
  below like any other part of the start diff; never a state
@@ -453,7 +456,8 @@ ${footnoteFoldIn}${codeThreadReplies()}
453
456
  - Every open point here is TECHNICAL — triage already resolved
454
457
  the product ones, one phase earlier. PERMISSIVE: answer it
455
458
  yourself unless you genuinely cannot defend a default; a wrong
456
- technical call is still caught by the full run and the quality lap
459
+ technical call is still caught by each package's suite run and the
460
+ quality lap
457
461
  - The narrow exception: a simplification that drops something
458
462
  \`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
459
463
  silent default
@@ -465,11 +469,13 @@ ${footnoteFoldIn}${codeThreadReplies()}
465
469
  only, as fenced TypeScript (or the repository's language)
466
470
  3. \`## Call Stacks\` — seam level, entry point → module
467
471
  boundary; one line per hop, never per line of code
468
- 4. \`## E2E Scenarios\` — Gherkin in fenced \`gherkin\` blocks
469
- (whatever e2e framework the repository uses), each preceded
470
- by a line \`- e2e: <path>\`. With no user-visible
471
- behaviour change, the literal line \`No e2e change.\` plus a
472
- one-line reason — never empty or missing
472
+ 4. \`## E2E Scenarios\` — one line per user-visible behaviour,
473
+ \`- e2e: <path> — <behaviour> (covers <concern>)\`, in plain
474
+ words: the builder writes the scenario in the repository's
475
+ own e2e framework, so never spell out Gherkin here. With no
476
+ user-visible behaviour change, the literal line
477
+ \`No e2e change.\` plus a one-line reason — never empty or
478
+ missing
473
479
  5. \`## Unit Tests\` — one \`### <interface>\` per entry of
474
480
  \`## Interfaces\`, each test a line
475
481
  \`- unit: <path> — <behaviour> (covers <concern>)\`;
@@ -495,35 +501,31 @@ const architectureDecomposePrompt = () => `${styleBlock}
495
501
  ${stateFileRules}
496
502
  - The only state files this turn touches are the package files
497
503
  under \`.gtd/packages/\` — no other files for notes or output.
498
- Never delete \`.gtd/ARCHITECTURE.md\`: the build tail's full run
499
- still needs it, and package files may reference it
504
+ Never delete \`.gtd/ARCHITECTURE.md\`: package files may
505
+ reference it, and it is removed with the last package
500
506
  - Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
501
507
  conversation, otherwise read it (the converged technical
502
508
  plan). It already lists the concerns, interfaces, e2e scenarios
503
509
  and unit tests — this turn groups them into packages. A
504
510
  \`## Merged Concerns\` heading there records merges, never a
505
511
  concern of its own: write no package file for it
506
- - A package is a consecutive run of \`## Unit Tests\` entries that go
507
- green together on the fast suite, at most ~8 unit-test entries
508
- each. Keep the merge rule: concerns whose footprints centre on the
509
- same files stay in one package unless the later one only consumes
510
- an interface the earlier one creates. Prefer fewer, larger
511
- packages. Order them so each stays green on the fast suite —
512
- interface-introducing packages first
512
+ - A package is one concern's slice, and each must leave the full
513
+ suite green — every package boundary costs a full run, so write
514
+ as few as the concerns allow. Keep the merge rule: concerns whose
515
+ footprints centre on the same files stay in one package unless
516
+ the later one only consumes an interface the earlier one creates.
517
+ Order them interface-introducing packages first
513
518
  - Write the packages under \`.gtd/packages/\`, each carrying its
514
519
  requirement(s) — both, independently, if merged — its tasks, and
515
520
  each task's acceptance criteria as \`- [ ]\` checkboxes and
516
521
  relevant paths:
517
- - \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
518
- \`## E2E Scenarios\` as tests in the repository's own e2e
519
- framework and leaves them red. Write no such file when
520
- \`## E2E Scenarios\` says "No e2e change."
521
- - \`01-…\` onward — the unit-test packages. A requirement no test
522
- covers (docs, README, deletions, config) becomes a chore
523
- package, or a task of one; chore packages declare no tests
524
- - the last numbered package is the wiring package: CLI entry,
525
- workflow composition, config. A red e2e after it is a bug, not
526
- missing work
522
+ - \`01-…\` onward — one vertical slice each: the unit tests,
523
+ the code, the wiring (CLI entry, workflow composition, config)
524
+ and every \`## E2E Scenarios\` entry its concern covers, so the
525
+ slice ends with its scenarios passing. A requirement no test
526
+ covers (docs, README, deletions, config) is a task of the slice
527
+ it belongs to; only one no slice owns becomes a chore package,
528
+ which declares no tests
527
529
  - Every package with tests has a \`## Tests\` section, one line per
528
530
  declared test, copied from the architecture entries:
529
531
  \`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
@@ -569,10 +571,10 @@ const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
569
571
  - Write the tests the package declares under \`## Tests\` first,
570
572
  then the code that makes them pass; the turn is refused unless
571
573
  every declared test is in your diff
572
- - Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
573
- from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
574
- step code that typechecks, and leaves them red — no
575
- implementation
574
+ - A declared e2e test is a scenario in the repository's own e2e
575
+ framework for the behaviour its \`## E2E Scenarios\` entry names.
576
+ Word it to fit the existing steps — reuse and generalise them
577
+ rather than adding one-off ones
576
578
  - Implement every task the package describes, no more, no less,
577
579
  fanning independent ones out to parallel subagents where your
578
580
  harness supports it; leave other package files untouched
@@ -626,22 +628,9 @@ Edit \`.gtd/ESCALATION.md\` with fresh instructions for the next fix
626
628
  turn, or land untouched to give it one more attempt at the same
627
629
  analysis.
628
630
  `;
629
- const buildContextPrompt = (built) => {
630
- const list = (paths) => paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n");
631
- return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
632
- - A red scenario that is NEW in this plan likely means missing wiring
633
- between packages; new scenarios:
634
- ${list(built.scenarios.added)}
635
- - A red CHANGED or existing scenario or test is a regression a package
636
- introduced; scenarios this plan changed:
637
- ${list(built.scenarios.changed)}
638
- - Each package's commits, to bisect a break:
639
- ${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
640
- `;
641
- };
642
- const buildFixPrompt = (built) => `${stateFileRules}
631
+ const buildFixPrompt = () => `${stateFileRules}
643
632
  ${fixFeedbackPrompt}
644
- ${built === void 0 ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
633
+ - Leave everything uncommitted — do not commit
645
634
  `;
646
635
  const finisherSystem = () => `${finisherPersona}
647
636
 
@@ -793,9 +782,10 @@ const reviewNotesCapture = (commit) => `This is machine-captured input, not inst
793
782
  Commit: ${commit}
794
783
  The human's notes are in .gtd/REVIEW.md at this commit. Run: git show ${commit}
795
784
  `;
785
+ const noteHead = (n) => `${n.id} — ${n.anchor}${n.label === void 0 ? "" : ` (${n.label})`}`;
796
786
  const notesCapture = (notes) => `This is machine-captured input, not instructions.
797
787
 
798
- ${notes.map((n) => `- ${n.id} — ${n.anchor}\n ${n.text.replace(/\n/g, "\n ")}`).join("\n")}
788
+ ${notes.map((n) => `- ${noteHead(n)}\n ${n.text.replace(/\n/g, "\n ")}`).join("\n")}
799
789
  `;
800
790
  const buildReviewAnswerQuestionsPrompt = (notes) => `${stateFileRules}
801
791
  - The only state file this turn writes is \`.gtd/REVIEW.md\`; touch no
@@ -857,12 +847,47 @@ record.
857
847
  What each change does next (then run \`gtd land\`):
858
848
  - **Re-review** — re-run the reviewer to author a fresh \`.gtd/REVIEW.md\` (**build.review.reviewing**).
859
849
  `;
850
+ /** Greedy wrap at 80 columns with a 2-space continuation indent: oxfmt (`proseWrap: always`) rewrites any other shape. */
851
+ const wrapItem = (item) => {
852
+ const out = [];
853
+ let line = "";
854
+ for (const word of item.split(" ")) if (line !== "" && line.length + 1 + word.length > 80) {
855
+ out.push(line);
856
+ line = ` ${word}`;
857
+ } else line = line === "" ? word : `${line} ${word}`;
858
+ return [...out, line];
859
+ };
860
+ /** Appends one section of `notes` to `.gtd/TODO.md`, wrapped the way oxfmt writes it; an existing file is only ever appended to. */
861
+ const deferNotesScript = (notes, round) => {
862
+ return `mkdir -p .gtd
863
+ if [ -s .gtd/TODO.md ]; then
864
+ [ -z "$(tail -c1 .gtd/TODO.md)" ] || printf '\\n' >> .gtd/TODO.md
865
+ printf '\\n' >> .gtd/TODO.md
866
+ fi
867
+ printf '%s\\n' ${[
868
+ `## Deferred review notes (${round.slice(0, 7)})`,
869
+ "",
870
+ ...notes.flatMap((n) => wrapItem(`- ${noteHead(n)}: ${n.text.replace(/\s+/g, " ")}`))
871
+ ].map(quote).join(" ")} >> .gtd/TODO.md
872
+ `;
873
+ };
874
+ /** The gate right after a deferral: landing clean signs off without these notes. */
875
+ const buildReviewAwaitAfterDeferMessage = (base, deferred) => `${buildReviewAwaitReviewMessage(base)}
876
+ Deferred to \`.gtd/TODO.md\` just now, NOT acted on in this process — landing
877
+ clean signs off without them:
878
+
879
+ ${deferred.map((n) => `- ${noteHead(n)}: ${n.text.replace(/\s*\n\s*/g, " ")}`).join("\n")}
880
+ `;
860
881
  const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
861
882
  footnote, or prose under a chunk) with one verdict: \`edit\` (a change
862
883
  request), \`question\`, \`nit\` (a small fix needing no re-plan) or
863
884
  \`praise\`. Run \`gtd judge answer\` and pipe a choice per note — or land
864
885
  untouched, which treats every note as \`edit\` (the conservative
865
886
  default; a skipped judgment never dismisses a note).
887
+
888
+ Also judge each note's severity, one id per note: \`note-<n>.security\`,
889
+ \`note-<n>.reversibility\`, \`note-<n>.blastRadius\` and
890
+ \`note-<n>.newConcept\`.
866
891
  `;
867
892
  const buildReviewCollectingPrompt = (capture) => `${styleBlock}
868
893
 
@@ -883,6 +908,8 @@ ${footnoteFoldIn}${codeThreadReplies()}
883
908
  - When the capture names an answering commit, carry each answered
884
909
  question into \`.gtd/REQUIREMENTS.md\`'s \`## Answered Questions\` as
885
910
  \`### <the note>\` plus its answer — the lap replaces \`.gtd/REVIEW.md\`
911
+ - Keep capture order, and carry each note's \`(tier: reason)\`
912
+ label into the concern folded from it
886
913
  - Nit notes were already fixed: never re-raise a fixed nit as a concern
887
914
  - Finish by running \`gtd check review .gtd/REVIEW.md\` and fix
888
915
  what it reports
@@ -924,7 +951,18 @@ actionability, and never dismiss a real note or edit as approval.
924
951
  - If not: finish without writing anything — changing nothing is
925
952
  the sign-off
926
953
  `;
927
- const buildReviewReviewingPrompt = (base, carry) => `${styleBlock}
954
+ const scenariosPrompt = (removed) => `- When a scenario in a \`.feature\` file was added or changed since the
955
+ base, the first chunk is \`## Scenarios\`: one pointer per such
956
+ scenario, its note saying in plain words what behaviour it now pins
957
+ down — the human signs off the scenarios here, nowhere else
958
+ ${removed.length === 0 ? "" : `- These assertions of scenarios that existed at the base are gone:
959
+ ${removed.map(({ path, lines }) => lines.map((a) => ` ${path} — ${a.scenario} — ${a.line}\n`).join("")).join("")} Open the note of each scenario that lost one with \`Weakened:\`,
960
+ naming the lost line and whether \`.gtd/REQUIREMENTS.md\` asked
961
+ for it. A scenario that no longer exists has no pointer of its own:
962
+ put its \`Weakened:\` note on a pointer of the chunk covering its
963
+ removal
964
+ `}`;
965
+ const buildReviewReviewingPrompt = (base, carry, removed = []) => `${styleBlock}
928
966
 
929
967
  ${styleFormatContract}
930
968
 
@@ -966,7 +1004,7 @@ review the changes:
966
1004
  A note sitting entirely on the line(s) beneath the pointer is
967
1005
  also valid. Either way, the note must never start with a bare \`./path\` token
968
1006
  — that parses as a second pointer, not a note
969
- No diff is given — read the changes yourself. The range runs
1007
+ ${scenariosPrompt(removed)}No diff is given — read the changes yourself. The range runs
970
1008
  from \`${base}\` to the working tree (committed turns
971
1009
  plus anything pending); on a feedback round that's the previous
972
1010
  review's boundary, so it covers only what's new.
@@ -1013,20 +1051,6 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
1013
1051
  Print the closing message and stop — this writes nothing itself.
1014
1052
  `;
1015
1053
  };
1016
- const scenarioWordingMessage = (at, drifted) => `The e2e scenario wording was frozen when package 0 landed, and the last
1017
- turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
1018
-
1019
- ${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
1020
-
1021
- To accept the change, land untouched.
1022
-
1023
- To reject it, run
1024
-
1025
- git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
1026
-
1027
- and land. Restoring only some of the files is a partial accept: every file
1028
- still differing from the frozen wording is accepted.
1029
- `;
1030
1054
  //#endregion
1031
1055
  //#region src/workflows/shared/steps.ts
1032
1056
  const FEEDBACK = ".gtd/FEEDBACK.md";
@@ -1037,7 +1061,7 @@ const REVIEW = ".gtd/REVIEW.md";
1037
1061
  const QUALITY = ".gtd/QUALITY.md";
1038
1062
  const planner = () => env.plannerModel ?? "";
1039
1063
  const coder = () => env.coderModel ?? "";
1040
- const fixCheck = (built) => agentWithSkills("fix", buildFixPrompt(built), {
1064
+ const fixCheck = () => agentWithSkills("fix", buildFixPrompt(), {
1041
1065
  label: "Fixing the check",
1042
1066
  file: FEEDBACK,
1043
1067
  model: coder(),
@@ -1093,7 +1117,7 @@ const fixQuality = () => agentWithSkills("fix.quality.fixing", buildFixQualityPr
1093
1117
  allowEmpty: true
1094
1118
  });
1095
1119
  /** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
1096
- const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry), {
1120
+ const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry, removedAssertions(changesSince(base))), {
1097
1121
  label: "Reviewing",
1098
1122
  file: REVIEW,
1099
1123
  mode: "review",
@@ -1109,37 +1133,24 @@ const fixRisks = (notes) => agentWithSkills("review.fix.risks.fixing", buildRevi
1109
1133
  system: reviewerSystem(),
1110
1134
  allowEmpty: true
1111
1135
  });
1112
- const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)) => human("review.await-review", {
1136
+ const awaitReview = (base, message = buildReviewAwaitReviewMessage(base), acceptClean = false) => human("review.await-review", {
1113
1137
  message,
1114
1138
  label: "Awaiting your review",
1115
1139
  file: REVIEW,
1116
1140
  mode: "review",
1117
- base
1141
+ base,
1142
+ ...acceptClean ? { acceptClean } : {}
1118
1143
  });
1119
1144
  /** The round at commit `round` left no `.gtd/REVIEW.md`. */
1120
1145
  const reviewMissing = (round) => human("review.review-missing", {
1121
1146
  message: buildReviewReviewMissingMessage(round),
1122
1147
  label: "Nothing to review"
1123
1148
  });
1124
- //#endregion
1125
- //#region src/workflows/shared/health.ts
1126
- /** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
1127
- const SETUP = ".gtd/SETUP.md";
1128
- const SETUP_TEXT = "The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it as a workflow option in `gtd.config.ts` or as GTD_FASTTESTCOMMAND.";
1129
- /** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
1130
- const fastSuiteGuard = () => (env.fastTestCommand ?? "").trim() === "" ? [
1131
- `mkdir -p ${quote(dirname(SETUP))}`,
1132
- `printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
1133
- `printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
1134
- "exit 0"
1135
- ] : [`rm -f ${quote(SETUP)}`];
1136
- const unconfigured = () => read(SETUP) !== void 0;
1137
1149
  /** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
1138
- const baseline = (name, label = "Checking the baseline") => untilSettled(() => check(name, env.testCommand ?? "", {
1150
+ const baseline = (name, label = "Checking the baseline") => check(name, env.testCommand ?? "", {
1139
1151
  report: FEEDBACK,
1140
- label,
1141
- preamble: fastSuiteGuard()
1142
- }), unconfigured);
1152
+ label
1153
+ });
1143
1154
  const escalationSteps = {
1144
1155
  limit: 2,
1145
1156
  describe: describeEscalation,
@@ -1177,22 +1188,18 @@ const sameFailure = async (previous, current) => {
1177
1188
  });
1178
1189
  return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
1179
1190
  };
1180
- const runSuite = (options) => {
1181
- const fast = options.suite === "fast";
1182
- return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
1183
- report: FEEDBACK,
1184
- label: "Running checks",
1185
- sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []],
1186
- ...fast ? { preamble: fastSuiteGuard() } : {}
1187
- });
1188
- };
1191
+ const runSuite = (options) => check("health.check", env.testCommand ?? "", {
1192
+ report: FEEDBACK,
1193
+ label: "Running checks",
1194
+ sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []]
1195
+ });
1189
1196
  /**
1190
1197
  * Loop until the suite is green: check, and on red run `fix` — escalating
1191
1198
  * first once `FIX_CAP` fixes are spent, or a judge calls the failure
1192
1199
  * identical to the previous round's.
1193
1200
  */
1194
1201
  const healthy = (fix, options = {}) => healthy$1(fix, {
1195
- check: options.suite === "fast" ? () => untilSettled(() => runSuite(options), unconfigured) : () => runSuite(options),
1202
+ check: () => runSuite(options),
1196
1203
  report: FEEDBACK,
1197
1204
  fixCap: 3,
1198
1205
  fixesSoFar: options.fixesSoFar ?? 0,
@@ -1214,12 +1221,12 @@ const defaults = {
1214
1221
  judgeBudgetBytes: "32768",
1215
1222
  judgeIdenticalMinP: "0.7",
1216
1223
  reviewNoteActionable: "0.7",
1224
+ reviewSeverityMinP: "0.7",
1217
1225
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
1218
1226
  };
1219
1227
  /** Environment settings. */
1220
1228
  const envDefaults = {
1221
1229
  testCommand: "npm test",
1222
- fastTestCommand: "",
1223
1230
  plannerModel: "smart",
1224
1231
  coderModel: "base"
1225
1232
  };
@@ -1231,9 +1238,9 @@ const ShippedOptions = Struct({
1231
1238
  judgeBudgetBytes: setting,
1232
1239
  judgeIdenticalMinP: setting,
1233
1240
  reviewNoteActionable: setting,
1241
+ reviewSeverityMinP: setting,
1234
1242
  qualityReviews: setting,
1235
1243
  testCommand: setting,
1236
- fastTestCommand: setting,
1237
1244
  plannerModel: setting,
1238
1245
  coderModel: setting
1239
1246
  });
@@ -1253,21 +1260,6 @@ const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOp
1253
1260
  base
1254
1261
  }));
1255
1262
  //#endregion
1256
- //#region src/workflows/shared/scenarios.ts
1257
- /** After a turn: stop at `scenario-wording` while wording drifted; accept moves the snapshot. */
1258
- const holdWording = (frozen) => holdWording$1(frozen, (at, drifted) => human("scenario-wording", {
1259
- message: scenarioWordingMessage(at, drifted),
1260
- label: "Scenario wording changed",
1261
- acceptClean: true,
1262
- base: at
1263
- }));
1264
- /** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
1265
- const guarded = (frozen, turn, within) => async () => {
1266
- await turn();
1267
- if (within === void 0) await holdWording(frozen);
1268
- else await scope(within, () => holdWording(frozen));
1269
- };
1270
- //#endregion
1271
1263
  //#region src/workflows/shared/tail.ts
1272
1264
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
1273
1265
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
@@ -1278,8 +1270,8 @@ const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
1278
1270
  */
1279
1271
  const qualityLap = () => qualityLap$1(lensesOf(vars.qualityReviews ?? ""), reviewQuality, QUALITY);
1280
1272
  /** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
1281
- const fixQualityFindings = (escalations, frozen) => fixFindings({
1282
- fix: guarded(frozen, fixQuality),
1273
+ const fixQualityFindings = (escalations) => fixFindings({
1274
+ fix: fixQuality,
1283
1275
  findings: QUALITY,
1284
1276
  cap: 3,
1285
1277
  escalate: () => escalation(escalations)
@@ -1295,34 +1287,25 @@ const refusePendingHandOff = () => {
1295
1287
  };
1296
1288
  /**
1297
1289
  * The build tail: fix (when entered red), keep green, the quality lap, then
1298
- * `finish` — the human review, as the calling workflow runs it.
1290
+ * `finish` — the human review, as the calling workflow runs it. `green`: the
1291
+ * caller just left the full suite green (the package loop), so no run opens it.
1299
1292
  */
1300
- const tail = (fixFirst, finish, built) => scope("build", async () => {
1301
- const frozen = built?.frozen;
1293
+ const tail = (fixFirst, finish, green = false) => scope("build", async () => {
1302
1294
  const escalations = { rounds: 0 };
1303
- const guardedFix = guarded(frozen, () => fixCheck());
1295
+ const fix = fixCheck;
1304
1296
  const repair = async () => {
1305
- await guardedFix();
1306
- await healthy(guardedFix, {
1297
+ await fix();
1298
+ await healthy(fix, {
1307
1299
  fixesSoFar: 1,
1308
1300
  escalations
1309
1301
  });
1310
1302
  };
1311
1303
  if (fixFirst) await repair();
1312
- else await healthy(guarded(frozen, () => fixCheck(built)), {
1313
- escalations,
1314
- sweepOnGreen: [ARCHITECTURE]
1315
- });
1316
- if (await qualityLap() === "findings") if (await fixQualityFindings(escalations, frozen)) await healthy(guardedFix, { escalations });
1304
+ else if (!green) await healthy(fix, { escalations });
1305
+ if (await qualityLap() === "findings") if (await fixQualityFindings(escalations)) await healthy(fix, { escalations });
1317
1306
  else await repair();
1318
- return finish(escalations, frozen);
1307
+ return finish(escalations);
1319
1308
  });
1320
- const handOffScript = (reviewed) => [
1321
- "#!/usr/bin/env sh",
1322
- "set -e",
1323
- `git show ${quote(`${reviewed}:${REVIEW}`)} > ${quote(REVIEW_ORIG)}`,
1324
- ""
1325
- ].join("\n");
1326
1309
  /**
1327
1310
  * The review and fix workflows' human review since `base`: a sign-off closes
1328
1311
  * `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
@@ -1332,11 +1315,11 @@ const handOffReview = (base, escalations) => reviewToHandOff(base, {
1332
1315
  file: REVIEW,
1333
1316
  reviewing,
1334
1317
  fixRisks,
1335
- keepGreen: () => healthy(() => fixCheck(), { escalations }),
1318
+ keepGreen: () => healthy(fixCheck, { escalations }),
1336
1319
  awaitReview: (b) => awaitReview(b, handOffAwaitReviewMessage(b)),
1337
1320
  reviewMissing,
1338
1321
  close: closeReview,
1339
- handOff: (reviewed) => run("review.handing-off", handOffScript(reviewed), { label: "Handing your notes to fix" }),
1322
+ handOff: (reviewed) => run("review.handing-off", showScript(reviewed, REVIEW, REVIEW_ORIG), { label: "Handing your notes to fix" }),
1340
1323
  threadsPending: "gtd land: open-threads: a thread in .gtd/REVIEW.md or an `H:` code comment waits on the agent — nothing answers it here; delete it, or turn it into a note for fix.",
1341
1324
  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.",
1342
1325
  deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
@@ -1372,6 +1355,7 @@ const access = (vars) => ({
1372
1355
  architecture: { write: [".gtd/REQUIREMENTS.md"] },
1373
1356
  "architecture.decompose": { write: [".gtd/packages/**", ".gtd/ARCHITECTURE.md"] },
1374
1357
  "build.review": { write: [".gtd/REVIEW.md"] },
1358
+ "build.review.defer": { write: [".gtd/TODO.md"] },
1375
1359
  "build.review.fix.nits": {},
1376
1360
  "build.review.fix.risks": {},
1377
1361
  ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] }))
@@ -1385,4 +1369,4 @@ const steering = {
1385
1369
  [REVIEW]: "review"
1386
1370
  };
1387
1371
  //#endregion
1388
- export { unwindFailure as $, architectureAuthorPrompt as A, designTriagePrompt as B, fixCheck as C, reviewing as D, reviewMissing as E, buildReviewFixNitsPrompt as F, packagesItemFixSuitePrompt as G, fixReviewNotesPrompt as H, buildReviewTriageMessage as I, reviewGateBlockedMessage as J, reviewEditNotesCapture as K, builderSystem as L, architectureGateAnswerMessage as M, buildReviewAnswerQuestionsPrompt as N, agentWithSkills as O, buildReviewCollectingPrompt as P, unwindFailedMessage as Q, designGateAnswerMessage as R, coder as S, planner as T, idleMessage as U, finisherSystem as V, packagesItemBuildingPrompt as W, reviewerSystem as X, reviewNotesCapture as Y, startGateBlockedMessage as Z, ARCHITECTURE as _, REVIEW_ORIG as a, REVIEW as b, refusePendingHandOff as c, shipped as d, defaults as f, healthy as g, gate as h, skills as i, architectureDecomposePrompt as j, architectSystem as k, tail as l, baseline as m, summary as n, closeReview as o, envDefaults as p, reviewEditsCapture as q, access as r, handOffTail as s, steering as t, guarded as u, FEEDBACK as v, fixRisks as w, awaitReview as x, REQUIREMENTS as y, designSystem as z };
1372
+ export { unwindFailedMessage as $, architectureDecomposePrompt as A, designSystem as B, fixRisks as C, agentWithSkills as D, reviewing as E, buildReviewFixNitsPrompt as F, packagesItemBuildingPrompt as G, finisherSystem as H, buildReviewTriageMessage as I, reviewEditsCapture as J, packagesItemFixSuitePrompt as K, builderSystem as L, buildReviewAnswerQuestionsPrompt as M, buildReviewAwaitAfterDeferMessage as N, architectSystem as O, buildReviewCollectingPrompt as P, startGateBlockedMessage as Q, deferNotesScript as R, fixCheck as S, reviewMissing as T, fixReviewNotesPrompt as U, designTriagePrompt as V, idleMessage as W, reviewNotesCapture as X, reviewGateBlockedMessage as Y, reviewerSystem as Z, FEEDBACK as _, REVIEW_ORIG as a, awaitReview as b, refusePendingHandOff as c, defaults as d, unwindFailure as et, envDefaults as f, ARCHITECTURE as g, healthy as h, skills as i, architectureGateAnswerMessage as j, architectureAuthorPrompt as k, tail as l, gate as m, summary as n, closeReview as o, baseline as p, reviewEditNotesCapture as q, access as r, handOffTail as s, steering as t, shipped as u, REQUIREMENTS as v, planner as w, coder as x, REVIEW as y, designGateAnswerMessage as z };
@@ -1,4 +1,12 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
+ //#region src/steering/SteeringFormat.ts
3
+ /** `file:line:col: message` — the shape editors and grep already jump on. Stored 0-based, printed 1-based. A finding with a `line` but no `range`, or none at all, degrades to the shorter forms. */
4
+ const formatFinding = (file, finding) => {
5
+ if (finding.line === void 0) return finding.message;
6
+ const col = finding.range !== void 0 ? `:${finding.range.start.character + 1}` : "";
7
+ return `${file}:${finding.line + 1}${col}: ${finding.message}`;
8
+ };
9
+ //#endregion
2
10
  //#region node_modules/mdast-util-to-string/lib/index.js
3
11
  /**
4
12
  * @typedef {import('mdast').Nodes} Nodes
@@ -15912,10 +15920,13 @@ const reviewNotes = (before, after) => {
15912
15920
  return [...parseReviewDoc(after).changesets.flatMap((chunk) => {
15913
15921
  const prior = oldChunks.get(chunk.title);
15914
15922
  return [...chunkNote(chunk, prior), ...pointerNotes(chunk, prior)];
15915
- }), ...footnoteNotes(before, after)].sort((a, b) => a.line - b.line).map((f, i) => ({
15916
- id: `note-${i + 1}`,
15917
- ...f.note
15918
- }));
15923
+ }), ...footnoteNotes(before, after)].sort((a, b) => a.line - b.line).map((f, i) => {
15924
+ return {
15925
+ id: `note-${i + 1}`,
15926
+ ...f.note,
15927
+ forced: /^!(\s|$)/.test(f.note.text)
15928
+ };
15929
+ });
15919
15930
  };
15920
15931
  /** Pointer notes the reviewer opened with `Risk:`, in document order — the ones the workflow fixes before the human gate. */
15921
15932
  const reviewRisks = (content) => parseReviewDoc(content).changesets.flatMap((chunk) => chunk.files.flatMap((file) => file.note?.startsWith("Risk:") ? [{
@@ -16293,4 +16304,4 @@ const viewOf = (format, content) => ({
16293
16304
  /** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
16294
16305
  const openThreads = (format, content) => format.openThreads?.(content) ?? [];
16295
16306
  //#endregion
16296
- export { headingSections as C, headingSectionBodies as S, openThreadFindings as _, unansweredQuestions as a, parseThreadsWithFindings as b, stripCodeThreads as c, reviewNotes as d, reviewRisks as f, THREAD_REPLY_ACTION_TITLE as g, FOOTNOTE_ACTION_TITLE as h, steeringFormatOrFreeForm as i, freeFormFormat as l, isAnswered as m, openThreads as n, viewOf as o, FREE_TEXT_PLACEHOLDER as p, steeringFormatFor as r, parseCodeThreads as s, BUILT_IN_MODE_NAMES as t, applySteeringEdits as u, parseFootnotes as v, getParseCount as x, parseThreads as y };
16307
+ export { headingSections as C, headingSectionBodies as S, openThreadFindings as _, unansweredQuestions as a, parseThreadsWithFindings as b, stripCodeThreads as c, reviewNotes as d, reviewRisks as f, THREAD_REPLY_ACTION_TITLE as g, FOOTNOTE_ACTION_TITLE as h, steeringFormatOrFreeForm as i, freeFormFormat as l, isAnswered as m, openThreads as n, viewOf as o, FREE_TEXT_PLACEHOLDER as p, steeringFormatFor as r, parseCodeThreads as s, BUILT_IN_MODE_NAMES as t, applySteeringEdits as u, parseFootnotes as v, formatFinding as w, getParseCount as x, parseThreads as y };
package/dist/flows.mjs CHANGED
@@ -237,6 +237,13 @@ const moveScript = (from, to) => [
237
237
  `mv -- ${quote(from)} ${quote(to)}`,
238
238
  ""
239
239
  ].join("\n");
240
+ /** Write `path` as committed at `ref` to `to`. */
241
+ const showScript = (ref, path, to) => [
242
+ "#!/usr/bin/env sh",
243
+ "set -e",
244
+ `git show ${quote(`${ref}:${path}`)} > ${quote(to)}`,
245
+ ""
246
+ ].join("\n");
240
247
  //#endregion
241
248
  //#region src/flows/helpers.ts
242
249
  /** Whether a judge answered `expected` at a probability of at least `minP`. */
@@ -515,6 +522,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
515
522
  scope: () => scope,
516
523
  sectionBodies: () => sectionBodies,
517
524
  sections: () => sections,
525
+ showScript: () => showScript,
518
526
  skillsFor: () => skillsFor,
519
527
  start: () => start,
520
528
  threads: () => threads,
@@ -522,4 +530,4 @@ var flows_exports = /* @__PURE__ */ __exportAll({
522
530
  wrote: () => wrote
523
531
  });
524
532
  //#endregion
525
- export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, sectionBodies, sections, skillsFor, start, flows_exports as t, threads, vars, wrote };
533
+ export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, sectionBodies, sections, showScript, skillsFor, start, flows_exports as t, threads, vars, wrote };