@pmelab/gtd 33.0.0 → 34.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "33.0.0",
3
+ "version": "34.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
@@ -405,9 +405,10 @@ phone UI does not show them).
405
405
 
406
406
  ### The workflow it ships with
407
407
 
408
- One built-in workflow drives all of that. From where you sit, it has four
408
+ One built-in workflow drives all of that. From where you sit, it has three
409
409
  moments — everything between them runs without you, with the judged exceptions
410
- noted in steps 3 and 4 below.
410
+ noted in step 3 below — plus a fourth, review, only on `refactor-first` or when
411
+ you open the `review` door.
411
412
 
412
413
  1. **You sketch.** Change anything, or write the idea into `.gtd/TODO.md`. Rough
413
414
  is fine; it is treated as a sketch, not as work.
@@ -431,13 +432,15 @@ noted in steps 3 and 4 below.
431
432
  [the naive pass](#the-naive-pass) below; with [flag mode](#the-naive-pass)
432
433
  off, neither runs). Then the change is built test-first, checked against the
433
434
  full suite (`testCommand`) and fixed until it passes — so keep that suite
434
- fast; gtd has no lighter one. Scenario wording is not frozen: you sign the
435
- scenarios off once, in the review's `## Scenarios` chunk, where any assertion
436
- an existing scenario lost is marked `Weakened:`. After the build the quality
437
- lap runs. Three points along the process (two with flag mode off) are judged
438
- rather than always asking you outright — each stops and hands you a verdict
439
- to make (`gtd judge answer`, or land with a clean tree to accept the
440
- conservative default, which never skips work;
435
+ fast; gtd has no lighter one. Scenario wording is not frozen: a review's
436
+ `## Scenarios` chunk marks any assertion an existing scenario lost as
437
+ `Weakened:` — **risk: `feature` has no review, so a build turn that weakens a
438
+ scenario reaches `idle` unflagged unless you open the `review` door.** After
439
+ the build the quality lap runs, and the process ends: the feature workflow
440
+ has no human review of the finished change. Two points along the process (one
441
+ with flag mode off) are judged rather than always asking you outright — each
442
+ stops and hands you a verdict to make (`gtd judge answer`, or land with a
443
+ clean tree to accept the conservative default, which never skips work;
441
444
  `gtd judge run --provider fixed --answers <path>` — or the
442
445
  `GTD_JUDGE_ANSWERS` env var, inline JSON — answers one from a file, piped
443
446
  between `gtd judge --json` and `gtd judge answer`;
@@ -456,8 +459,6 @@ noted in steps 3 and 4 below.
456
459
  or get rejected and retried?
457
460
  - Every red round after the first: was the failure identical, new, or
458
461
  progress?
459
- - After you review: is each of your notes an edit, a question, a nit or
460
- praise?
461
462
 
462
463
  gtd itself never calls a judge: `gtd loop`, the Claude Code mod and the
463
464
  reference driver answer these through `gtd judge run` (auto selection: jev
@@ -469,18 +470,22 @@ noted in steps 3 and 4 below.
469
470
  and skip a gate unattended.**
470
471
 
471
472
  Once the change is built, the whole change goes through a qualitative review
472
- lap before you see anything: six lenses, one turn each, in order —
473
- `correctness`, `owasp-security`, `ponytail-review`, `test-audit`,
474
- `conventions`, `spec-challenge`. `correctness`, `owasp-security` and
475
- `spec-challenge` run on the planner model (`smart`), the other three on the
476
- coder model (`base`); a `qualityReviews` entry `<lens>=<model>` picks a
477
- lens's own. Each traces the change from its own angle and records every
478
- finding, blocking or not; one fix turn then fixes ALL of them once, with no
479
- re-review after the fix. A clean turn means approval only when that lens
480
- found nothing at all. This lap is where code quality is looked at, and every
481
- round pays for it. It never replaces step 4 — your review stays the final
482
- gate, and nothing here skips it. The `fix` door (below) repairs a red
483
- baseline through this same lap.
473
+ lap: six lenses, one turn each, in order — `correctness`, `owasp-security`,
474
+ `ponytail-review`, `test-audit`, `conventions`, `spec-challenge`.
475
+ `correctness`, `owasp-security` and `spec-challenge` run on the planner model
476
+ (`smart`), the other three on the coder model (`base`); a `qualityReviews`
477
+ entry `<lens>=<model>` picks a lens's own. Each traces the change from its
478
+ own angle and records every finding, blocking or not; one fix turn then fixes
479
+ ALL of them once, with no re-review after the fix. A clean turn means
480
+ approval only when that lens found nothing at all. This lap is where code
481
+ quality is looked at, and every round pays for it. It is the last look the
482
+ feature workflow takes: the process ends at `idle` once it is clean (and the
483
+ suite green), with no review rest — **risk: nothing re-reads the code the
484
+ single fix turn writes; only the suite gates it.** Findings the fix cap
485
+ leaves in `.gtd/QUALITY.md` rest at `build.quality-unresolved` until you
486
+ delete or empty the file; the suite then runs on any hand edits. **Nobody
487
+ signs the finished change off unless you open the `review` door** (below).
488
+ The `fix` door repairs a red baseline through this same lap.
484
489
 
485
490
  A red suite that keeps failing past a few fix attempts escalates instead of
486
491
  retrying forever: an agent turn reads the failing output and writes
@@ -491,22 +496,28 @@ noted in steps 3 and 4 below.
491
496
  instead of a third being written, and editing it is what gets the next
492
497
  attempt anywhere new.
493
498
 
494
- 4. **You review.** You get a review document listing what changed and what to
495
- look at. Before you see it, an automatic risk-fix pass
496
- (`build.review.fix.risks.fixing`) runs. Any risk the reviewer names (a note
497
- opening with `Risk:`) is fixed first, the suite kept green, and the review
498
- rewritten — once per review round, so a risk the rewrite still names reaches
499
- you unfixed; risk: a fix lands with no check that the risk was real. Tick the
500
- boxes to approve, or write what is wrong. Approving ends the process;
501
- feedback is judged note by note, each as `edit`, `question`, `nit` or
502
- `praise`. An `edit` sends the process back to step 2 for re-derived
503
- requirements, and the implementation is amended to meet them. A `question` is
504
- answered inline under your note and the process stops at the review again —
505
- no new plan. A `nit` is fixed in one batch, the suite must go green (a red
506
- one gets fix turns first), then a fresh review of the change stops at the
507
- review again. `praise` is dropped; a round of only praise signs off. When a
508
- round mixes them, questions are answered and nits fixed first, then the edits
509
- are planned — risk: the planning lap may redo nit fixes it touches.
499
+ 4. **You review — the refactor, or on request.** The feature workflow asks for
500
+ one review only on `refactor-first`: of the refactor slice, before the
501
+ feature is built (see [the naive pass](#the-naive-pass)). For the finished
502
+ change run `gtd door review <commitish>` (`/gtd review [base]`); that review
503
+ hands notes to `fix` instead of re-planning (below). Either way you get a
504
+ review document listing what changed and what to look at. Before you see it,
505
+ an automatic risk-fix pass (`build.review.fix.risks.fixing`) runs. Any risk
506
+ the reviewer names (a note opening with `Risk:`) is fixed first, the suite
507
+ kept green, and the review rewritten — once per review round, so a risk the
508
+ rewrite still names reaches you unfixed; risk: a fix lands with no check that
509
+ the risk was real. Tick the boxes to approve, or write what is wrong. At the
510
+ refactor review, approving moves on to the feature; feedback is judged note
511
+ by note (a third judged point), each as `edit`, `question`, `nit` or
512
+ `praise`. An `edit` re-derives the refactor (in a custom workflow built on
513
+ `buildTail`, it sends the process back to step 2 for re-derived
514
+ requirements). A `question` is answered inline under your note and the
515
+ process stops at the review again — no new plan. A `nit` is fixed in one
516
+ batch, the suite must go green (a red one gets fix turns first), then a fresh
517
+ review of the change stops at the review again. `praise` is dropped; a round
518
+ of only praise signs off. When a round mixes them, questions are answered and
519
+ nits fixed first, then the edits are planned — risk: the planning lap may
520
+ redo nit fixes it touches.
510
521
 
511
522
  Each `edit` note also gets a severity tier from four judge questions
512
523
  (security, reversibility, blast radius, new concept). `critical` and `should`
@@ -526,7 +537,7 @@ noted in steps 3 and 4 below.
526
537
  You never talk to it. Every exchange is a file in `.gtd/` that you edit in your
527
538
  own editor, and every answer you give is a commit. Your test suite is the gate
528
539
  throughout: a red baseline stops the process before it starts, and a red build
529
- is fixed before review.
540
+ is fixed before the process ends.
530
541
 
531
542
  Prefer a phone or another device over your editor? `gtd ui` opens the file whose
532
543
  turn it is as a small web app, over Tailscale — including a step that names no
@@ -596,16 +607,15 @@ file it added, or the suite is red on it. A judge checks whether it changed
596
607
  anything outside the expected set (tests, steering files; see `seamAllow`).
597
608
  Incidental changes land; a needed seam change is refactored first, in its own
598
609
  slice with its own quality lap (`refactorQualityReviews`) and review, before the
599
- feature is implemented and reviewed; a wrong approach is retried twice, then
600
- rests for you to steer or overrule it. `gtd slices` lists the `refactor` and
601
- `feature` slices. A refactor that already carried the whole feature leaves the
602
- implementation nothing to do: its empty turn passes, and the feature review
603
- covers the whole change. Feedback at the feature review re-runs design and the
604
- implementation, never the naive pass; feedback at the refactor review re-runs
605
- the refactor, never design; a refactor-review change the refactor cannot build
606
- is refused, and the only way out is `gtd abandon`. A process from 27.5.0 or
607
- later whose judge ruled `refactor-first` must `gtd abandon` too. Each slice's
608
- quality lap reviews only that slice. Details in
610
+ feature is implemented; a wrong approach is retried twice, then rests for you to
611
+ steer or overrule it. `gtd slices` lists the `refactor` and `feature` slices. A
612
+ refactor that already carried the whole feature leaves the implementation
613
+ nothing to do: its empty turn passes, and the feature's quality lap covers the
614
+ whole change. Feedback at the refactor review re-runs the refactor, never
615
+ design; a refactor-review change the refactor cannot build is refused, and the
616
+ only way out is `gtd abandon`. A process from 27.5.0 or later whose judge ruled
617
+ `refactor-first` must `gtd abandon` too. Each slice's quality lap reviews only
618
+ that slice. Details in
609
619
  [the naive-pass path](docs/configuration.md#the-naive-pass-path).
610
620
 
611
621
  **Flag mode (`flagMode`, on by default).** On, the implementation is asked for a
@@ -416,6 +416,17 @@ carries it.
416
416
  What each change does next (then run \`gtd land\`):
417
417
  - **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
418
418
  `;
419
+ const qualityUnresolvedMessage = () => `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
420
+ holds findings. Nothing reviews the change after this, so the process
421
+ will not end with them open.
422
+
423
+ Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
424
+ dropping a finding is your call to make.
425
+
426
+ What each change does next (then run \`gtd land\`):
427
+ - **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the process ends (**idle**).
428
+ - **Anything else** — rests here again.
429
+ `;
419
430
  const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
420
431
  \`.gtd/FEEDBACK.md\` holds the failing output.
421
432
 
@@ -1629,4 +1640,4 @@ const steering = {
1629
1640
  [REVIEW]: "review"
1630
1641
  };
1631
1642
  //#endregion
1632
- export { reviewEditNotesCapture as $, buildReviewCollectingPrompt as A, designTriagePrompt as B, planner as C, agentWithSkills as D, reviewing as E, clearCriteriaQuestion as F, implementBuildingPrompt as G, fixReviewNotesPrompt as H, criterionLine as I, normaliseCriteriaScript as J, implementFixPrompt as K, deferNotesScript as L, buildReviewTriageMessage as M, builderSystem as N, buildReviewAnswerQuestionsPrompt as O, clearCriteriaMessage as P, refactorRules as Q, designGateAnswerMessage as R, fixRisks as S, reviewQuality as T, handOffAwaitReviewMessage as U, finisherSystem as V, idleMessage as W, refactorDroppedRefusal as X, refactorBuildingPrompt as Y, refactorFixPrompt as Z, REVIEW as _, flagModeOn as a, seamSteerMessage as at, fixCheck as b, baseline as c, unwindFailure as ct, guarded as d, testFileGlobs as dt, reviewEditsCapture as et, healthy as f, flagMentions as ft, REQUIREMENTS as g, requireFlag as gt, REFACTOR as h, registryEntry as ht, skills as i, seamByHandMessage as it, buildReviewFixNitsPrompt as j, buildReviewAwaitAfterDeferMessage as k, escalation as l, defaults as lt, QUALITY as m, flipProblems as mt, summary as n, reviewNotesCapture as nt, flagShipped as o, startGateBlockedMessage as ot, FEEDBACK as p, flagReferences as pt, naivePassPrompt as q, access as r, reviewerSystem as rt, shipped as s, unwindFailedMessage as st, steering as t, reviewGateBlockedMessage as tt, gate as u, envDefaults as ut, awaitReview as v, reviewMissing as w, fixQuality as x, coder as y, designSystem as z };
1643
+ export { refactorRules as $, buildReviewCollectingPrompt as A, designTriagePrompt as B, planner as C, agentWithSkills as D, reviewing as E, clearCriteriaQuestion as F, implementBuildingPrompt as G, fixReviewNotesPrompt as H, criterionLine as I, normaliseCriteriaScript as J, implementFixPrompt as K, deferNotesScript as L, buildReviewTriageMessage as M, builderSystem as N, buildReviewAnswerQuestionsPrompt as O, clearCriteriaMessage as P, refactorFixPrompt as Q, designGateAnswerMessage as R, fixRisks as S, reviewQuality as T, handOffAwaitReviewMessage as U, finisherSystem as V, idleMessage as W, refactorBuildingPrompt as X, qualityUnresolvedMessage as Y, refactorDroppedRefusal as Z, REVIEW as _, requireFlag as _t, flagModeOn as a, seamByHandMessage as at, fixCheck as b, baseline as c, unwindFailedMessage as ct, guarded as d, envDefaults as dt, reviewEditNotesCapture as et, healthy as f, testFileGlobs as ft, REQUIREMENTS as g, registryEntry as gt, REFACTOR as h, flipProblems as ht, skills as i, reviewerSystem as it, buildReviewFixNitsPrompt as j, buildReviewAwaitAfterDeferMessage as k, escalation as l, unwindFailure as lt, QUALITY as m, flagReferences as mt, summary as n, reviewGateBlockedMessage as nt, flagShipped as o, seamSteerMessage as ot, FEEDBACK as p, flagMentions as pt, naivePassPrompt as q, access as r, reviewNotesCapture as rt, shipped as s, startGateBlockedMessage as st, steering as t, reviewEditsCapture as tt, gate as u, defaults as ut, awaitReview as v, reviewMissing as w, fixQuality as x, coder as y, designSystem as z };
@@ -1,6 +1,6 @@
1
1
  import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
2
2
  import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { E as reviewing, S as fixRisks, T as reviewQuality, U as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, d as guarded, f as healthy, l as escalation, m as QUALITY, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-BU5Zi-nX.mjs";
3
+ import { E as reviewing, S as fixRisks, T as reviewQuality, U as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, d as guarded, f as healthy, l as escalation, m as QUALITY, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-BcJgt5_R.mjs";
4
4
  //#region src/workflows/shared/tail.ts
5
5
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
6
6
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { gt as requireFlag, ht as registryEntry, i as skills, lt as defaults, n as summary, o as flagShipped, pt as flagReferences, r as access, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-BU5Zi-nX.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DuXanjIY.mjs";
2
+ import { _t as requireFlag, dt as envDefaults, gt as registryEntry, i as skills, mt as flagReferences, n as summary, o as flagShipped, r as access, t as steering, ut as defaults, y as coder } from "../chunks/reserved-BcJgt5_R.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CM_fa93v.mjs";
4
4
  //#region src/workflows/cleanup/cleanup.ts
5
5
  const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
6
6
  const cleanupFlow = async () => {
@@ -1,7 +1,7 @@
1
1
  import { answered, changes, changesSince, check, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars } from "../flows.mjs";
2
2
  import { lensEntriesOf, review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as reviewEditNotesCapture, A as buildReviewCollectingPrompt, B as designTriagePrompt, C as planner, D as agentWithSkills, E as reviewing, F as clearCriteriaQuestion, G as implementBuildingPrompt, I as criterionLine, J as normaliseCriteriaScript, K as implementFixPrompt, L as deferNotesScript, M as buildReviewTriageMessage, N as builderSystem, O as buildReviewAnswerQuestionsPrompt, P as clearCriteriaMessage, Q as refactorRules, R as designGateAnswerMessage, S as fixRisks, W as idleMessage, X as refactorDroppedRefusal, Y as refactorBuildingPrompt, Z as refactorFixPrompt, _ as REVIEW, a as flagModeOn, at as seamSteerMessage, b as fixCheck, ct as unwindFailure, d as guarded, dt as testFileGlobs, et as reviewEditsCapture, f as healthy, g as REQUIREMENTS, h as REFACTOR, i as skills, it as seamByHandMessage, j as buildReviewFixNitsPrompt, k as buildReviewAwaitAfterDeferMessage, lt as defaults, n as summary, nt as reviewNotesCapture, ot as startGateBlockedMessage, p as FEEDBACK, q as naivePassPrompt, r as access, rt as reviewerSystem, s as shipped, st as unwindFailedMessage, t as steering, u as gate, ut as envDefaults, v as awaitReview, w as reviewMissing, y as coder, z as designSystem } from "../chunks/reserved-BU5Zi-nX.mjs";
4
- import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-DuXanjIY.mjs";
3
+ import { $ as refactorRules, A as buildReviewCollectingPrompt, B as designTriagePrompt, C as planner, D as agentWithSkills, E as reviewing, F as clearCriteriaQuestion, G as implementBuildingPrompt, I as criterionLine, J as normaliseCriteriaScript, K as implementFixPrompt, L as deferNotesScript, M as buildReviewTriageMessage, N as builderSystem, O as buildReviewAnswerQuestionsPrompt, P as clearCriteriaMessage, Q as refactorFixPrompt, R as designGateAnswerMessage, S as fixRisks, W as idleMessage, X as refactorBuildingPrompt, Y as qualityUnresolvedMessage, Z as refactorDroppedRefusal, _ as REVIEW, a as flagModeOn, at as seamByHandMessage, b as fixCheck, ct as unwindFailedMessage, d as guarded, dt as envDefaults, et as reviewEditNotesCapture, f as healthy, ft as testFileGlobs, g as REQUIREMENTS, h as REFACTOR, i as skills, it as reviewerSystem, j as buildReviewFixNitsPrompt, k as buildReviewAwaitAfterDeferMessage, lt as unwindFailure, m as QUALITY, n as summary, ot as seamSteerMessage, p as FEEDBACK, q as naivePassPrompt, r as access, rt as reviewNotesCapture, s as shipped, st as startGateBlockedMessage, t as steering, tt as reviewEditsCapture, u as gate, ut as defaults, v as awaitReview, w as reviewMissing, y as coder, z as designSystem } from "../chunks/reserved-BcJgt5_R.mjs";
4
+ import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-CM_fa93v.mjs";
5
5
  import { posix } from "node:path";
6
6
  //#region src/workflows/shared/fences.ts
7
7
  const markFenced = (text) => {
@@ -958,7 +958,7 @@ const unwind = () => {
958
958
  const commit = head();
959
959
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
960
960
  };
961
- /** A judged naive pass (none with `flagMode` off), then implement and review until a review round signs off; design must have run since `firstBase`. */
961
+ /** A judged naive pass (none with `flagMode` off), then implement and the quality lap; design must have run since `firstBase`. */
962
962
  const buildFeature = async (firstBase) => {
963
963
  const tier = riskTierOn() ? recordRisk() : void 0;
964
964
  const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
@@ -969,18 +969,31 @@ const buildFeature = async (firstBase) => {
969
969
  afterRefactor: seam.refactored
970
970
  });
971
971
  const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path)) ? firstBase : seam.base;
972
- await featureFeedback(await buildTail(base, {
972
+ await tail(settleQuality, {
973
973
  green: true,
974
974
  lensBase: base
975
- }), base);
975
+ });
976
976
  });
977
977
  };
978
+ const settleQuality = async () => {
979
+ let rested = false;
980
+ while ((read(".gtd/QUALITY.md") ?? "").length > 0) {
981
+ rested = true;
982
+ await human("quality-unresolved", {
983
+ message: qualityUnresolvedMessage(),
984
+ label: "Quality findings left unresolved",
985
+ file: QUALITY
986
+ });
987
+ }
988
+ if (rested) await healthy(() => fixCheck());
989
+ };
978
990
  /** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
979
991
  const planAndBuild = async (firstBase, stated) => {
980
992
  if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
981
993
  await buildFeature(firstBase);
982
994
  };
983
- const featureFeedback = async (first, lensBase) => {
995
+ /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
996
+ const afterTail = async (first) => {
984
997
  let outcome = first;
985
998
  while (outcome.verdict === "feedback") {
986
999
  await reUnwind(outcome);
@@ -991,13 +1004,10 @@ const featureFeedback = async (first, lensBase) => {
991
1004
  });
992
1005
  outcome = await buildTail(outcome.base, {
993
1006
  green: true,
994
- lap: "feedback",
995
- lensBase
1007
+ lap: "feedback"
996
1008
  });
997
1009
  }
998
1010
  };
999
- /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
1000
- const afterTail = (outcome) => featureFeedback(outcome);
1001
1011
  /** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
1002
1012
  const ordinaryStart = async () => {
1003
1013
  await human("idle", {
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
3
- import { D as agentWithSkills, H as fixReviewNotesPrompt, V as finisherSystem, _ as REVIEW, c as baseline, i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-BU5Zi-nX.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-DuXanjIY.mjs";
3
+ import { D as agentWithSkills, H as fixReviewNotesPrompt, V as finisherSystem, _ as REVIEW, c as baseline, dt as envDefaults, i as skills, n as summary, r as access, s as shipped, t as steering, ut as defaults, y as coder } from "../chunks/reserved-BcJgt5_R.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CM_fa93v.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,6 +1,6 @@
1
1
  import { start } from "../flows.mjs";
2
- import { i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, tt as reviewGateBlockedMessage, u as gate, ut as envDefaults } from "../chunks/reserved-BU5Zi-nX.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DuXanjIY.mjs";
2
+ import { dt as envDefaults, i as skills, n as summary, nt as reviewGateBlockedMessage, r as access, s as shipped, t as steering, u as gate, ut as defaults } from "../chunks/reserved-BcJgt5_R.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CM_fa93v.mjs";
4
4
  //#region src/workflows/review/review.ts
5
5
  /**
6
6
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { ft as flagMentions, gt as requireFlag, i as skills, lt as defaults, mt as flipProblems, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, ut as envDefaults, y as coder } from "../chunks/reserved-BU5Zi-nX.mjs";
2
+ import { _t as requireFlag, dt as envDefaults, ht as flipProblems, i as skills, n as summary, o as flagShipped, p as FEEDBACK, pt as flagMentions, r as access, t as steering, u as gate, ut as defaults, y as coder } from "../chunks/reserved-BcJgt5_R.mjs";
3
3
  //#region src/workflows/rollout/rollout.ts
4
4
  const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
5
5
  const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
@@ -17213,6 +17213,17 @@ carries it.
17213
17213
  What each change does next (then run \`gtd land\`):
17214
17214
  - **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
17215
17215
  `;
17216
+ const qualityUnresolvedMessage = () => `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
17217
+ holds findings. Nothing reviews the change after this, so the process
17218
+ will not end with them open.
17219
+
17220
+ Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
17221
+ dropping a finding is your call to make.
17222
+
17223
+ What each change does next (then run \`gtd land\`):
17224
+ - **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the process ends (**idle**).
17225
+ - **Anything else** — rests here again.
17226
+ `;
17216
17227
  const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
17217
17228
  \`.gtd/FEEDBACK.md\` holds the failing output.
17218
17229
 
@@ -38760,7 +38771,7 @@ const unwind = () => {
38760
38771
  const commit = head$1();
38761
38772
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
38762
38773
  };
38763
- /** A judged naive pass (none with `flagMode` off), then implement and review until a review round signs off; design must have run since `firstBase`. */
38774
+ /** A judged naive pass (none with `flagMode` off), then implement and the quality lap; design must have run since `firstBase`. */
38764
38775
  const buildFeature = async (firstBase) => {
38765
38776
  const tier = riskTierOn() ? recordRisk() : void 0;
38766
38777
  const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
@@ -38771,18 +38782,31 @@ const buildFeature = async (firstBase) => {
38771
38782
  afterRefactor: seam.refactored
38772
38783
  });
38773
38784
  const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path)) ? firstBase : seam.base;
38774
- await featureFeedback(await buildTail(base, {
38785
+ await tail(settleQuality, {
38775
38786
  green: true,
38776
38787
  lensBase: base
38777
- }), base);
38788
+ });
38778
38789
  });
38779
38790
  };
38791
+ const settleQuality = async () => {
38792
+ let rested = false;
38793
+ while ((read(".gtd/QUALITY.md") ?? "").length > 0) {
38794
+ rested = true;
38795
+ await human("quality-unresolved", {
38796
+ message: qualityUnresolvedMessage(),
38797
+ label: "Quality findings left unresolved",
38798
+ file: QUALITY
38799
+ });
38800
+ }
38801
+ if (rested) await healthy(() => fixCheck());
38802
+ };
38780
38803
  /** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
38781
38804
  const planAndBuild = async (firstBase, stated) => {
38782
38805
  if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
38783
38806
  await buildFeature(firstBase);
38784
38807
  };
38785
- const featureFeedback = async (first, lensBase) => {
38808
+ /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
38809
+ const afterTail = async (first) => {
38786
38810
  let outcome = first;
38787
38811
  while (outcome.verdict === "feedback") {
38788
38812
  await reUnwind(outcome);
@@ -38793,13 +38817,10 @@ const featureFeedback = async (first, lensBase) => {
38793
38817
  });
38794
38818
  outcome = await buildTail(outcome.base, {
38795
38819
  green: true,
38796
- lap: "feedback",
38797
- lensBase
38820
+ lap: "feedback"
38798
38821
  });
38799
38822
  }
38800
38823
  };
38801
- /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
38802
- const afterTail = (outcome) => featureFeedback(outcome);
38803
38824
  /** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
38804
38825
  const ordinaryStart = async () => {
38805
38826
  await human("idle", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "33.0.0",
3
+ "version": "34.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -224,8 +224,8 @@ workflow.
224
224
 
225
225
  In the bundled default, `planAndBuild` runs the design phase, then
226
226
  `buildFeature`: the naive pass and its seam judge, then the implementation and
227
- review. Add a human sign-off before the first build (a review feedback lap
228
- re-runs design inside `buildFeature` and does not pass it again):
227
+ the quality lap, ending with no human review. Add a human sign-off before the
228
+ build:
229
229
 
230
230
  ```ts
231
231
  const planAndBuild = async (firstBase: string): Promise<void> => {
@@ -108,6 +108,10 @@ Given("a file {string} with:", (world: TestkitWorld, path: string, content: stri
108
108
  world.repo().write(path, withNewline(content))
109
109
  })
110
110
 
111
+ Given("the file {string} is deleted", (world: TestkitWorld, path: string) => {
112
+ world.repo().remove(path)
113
+ })
114
+
111
115
  Given("the working tree is committed as {string}", (world: TestkitWorld, message: string) => {
112
116
  world.repo().commit(message)
113
117
  })
@@ -2,8 +2,8 @@
2
2
  Feature: a trivial change takes the same few beats at either risk tier, flag-free at low
3
3
 
4
4
  With GTD_FLAG_RISK_TIER on, a one-line copy fix goes
5
- from idle through the naive pass, the seam judge's land, implement and human
6
- review back to idle. Low tier walks exactly the steps high does; only the
5
+ from idle through the naive pass, the seam judge's land, implement and the
6
+ quality lap back to idle. Low tier walks exactly the steps high does; only the
7
7
  implementation prompt and the summary differ.
8
8
 
9
9
  Background:
@@ -27,18 +27,6 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
27
27
  And step "implement.health.check" changes nothing
28
28
  And step "implement.closing" runs its script
29
29
  And step "build.quality.correctness.reviewing" changes nothing
30
- And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
31
- """
32
- # Review: abc1234
33
-
34
- <!-- base: 0000000 -->
35
-
36
- ## greeting
37
-
38
- - [ ] ./src/greeting.ts#1-1
39
- typo fixed
40
- """
41
- And step "build.review.closing" runs its script
42
30
 
43
31
  Scenario Outline: a one-line fix runs idle to idle with the same beats at either tier
44
32
  Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
@@ -80,21 +68,6 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
80
68
  export const greeting = "Hello, world!"
81
69
  """
82
70
  When the process is driven
83
- Then the process rests at "build.review.await-review"
84
-
85
- Given a file ".gtd/REVIEW.md" with:
86
- """
87
- # Review: abc1234
88
-
89
- <!-- base: 0000000 -->
90
-
91
- ## greeting
92
-
93
- - [x] ./src/greeting.ts#1-1
94
- typo fixed
95
- """
96
- When gtd lands
97
- And the process is driven
98
71
  Then the process rests at "idle"
99
72
  And "src/greeting.ts" contains "Hello, world!"
100
73
  And the commit subjects end with:
@@ -110,10 +83,7 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
110
83
  gtd(agent): implement.building → implement.health.check
111
84
  gtd(check): implement.health.check → implement.closing
112
85
  gtd(check): implement.closing → build.quality.correctness.reviewing
113
- gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
114
- gtd(agent): build.review.reviewing → build.review.await-review
115
- gtd(human): build.review.await-review → build.review.closing
116
- gtd(check): build.review.closing → idle
86
+ gtd(agent): build.quality.correctness.reviewing → idle
117
87
  """
118
88
  And the output of gtd "summary" <summary> "needs a human reviewer"
119
89
 
@@ -64,107 +64,6 @@ const naivePlanned = () => {
64
64
  return repo
65
65
  }
66
66
 
67
- describe("planAndBuild", () => {
68
- it("a refactor that carried the whole feature: the empty implement turn passes and the review covers the whole change", async () => {
69
- const repo = naivePlanned()
70
- const SUBTRACT = `${CALC}export const subtract = (a: number, b: number) => a - b\n`
71
- expect(
72
- (
73
- await repo.drive({
74
- unwind: () => undefined,
75
- "start-gate.check": () => undefined,
76
- "design.triage": (_, r) =>
77
- r.write(
78
- ".gtd/REQUIREMENTS.md",
79
- "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
80
- ),
81
- "design.gate.answer": () => undefined,
82
- "seam.naive": (_, r) => {
83
- r.write("src/calc.ts", SUBTRACT)
84
- r.write("src/calc.test.ts", ACCEPTANCE)
85
- },
86
- "seam.check": () => undefined,
87
- "seam.judge": () => '[{"id":"shape","answer":"refactor-first","p":0.9}]',
88
- "refactor.deriving": runsItsScript,
89
- "refactor.building": (_, r) => r.write("src/calc.ts", SUBTRACT),
90
- "refactor.health.check": () => undefined,
91
- "refactor.closing": runsItsScript,
92
- "refactor.build.review.reviewing": (_, r) => r.write(".gtd/REVIEW.md", review()),
93
- })
94
- ).state,
95
- ).toBe("refactor.build.review.await-review")
96
- await repo.land()
97
- const rest = await repo.drive({
98
- "refactor.build.review.closing": runsItsScript,
99
- "implement.building": () => undefined,
100
- "implement.health.check": () => undefined,
101
- "implement.closing": runsItsScript,
102
- "build.review.reviewing": (_, r) => r.write(".gtd/REVIEW.md", review()),
103
- })
104
- expect(rest.state).toBe("build.review.await-review")
105
- const base = (await repo.gtd("base")).stdout.trim()
106
- expect(repo.show(base, "src/calc.ts")).toBe(CALC)
107
- })
108
-
109
- it("after land, feedback rests next at design.triage, then implement.building, never seam.naive", async () => {
110
- const repo = naivePlanned()
111
- const requirements = (_: unknown, r: typeof repo) =>
112
- r.write(
113
- ".gtd/REQUIREMENTS.md",
114
- "# Requirements\n\n## Concerns\n\n### Subtract\n\n- `subtract`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
115
- )
116
- const lap = {
117
- unwind: () => undefined,
118
- "start-gate.check": () => undefined,
119
- "design.triage": requirements,
120
- "design.gate.answer": () => undefined,
121
- "seam.naive": (_: unknown, r: typeof repo) => {
122
- r.write("src/calc.ts", `${CALC}// naive\n`)
123
- r.write("src/calc.test.ts", ACCEPTANCE)
124
- },
125
- "seam.check": () => undefined,
126
- "seam.judge": () => '[{"id":"shape","answer":"land","p":0.9}]',
127
- "implement.building": (_: unknown, r: typeof repo) =>
128
- r.write("src/calc.ts", `${CALC}// built\n`),
129
- "implement.health.check": () => undefined,
130
- "implement.closing": runsItsScript,
131
- "build.review.reviewing": (_: unknown, r: typeof repo) => r.write(".gtd/REVIEW.md", review()),
132
- }
133
- expect((await repo.drive(lap)).state).toBe("build.review.await-review")
134
-
135
- repo.write(".gtd/REVIEW.md", review("also export multiply"))
136
- await repo.land()
137
- const collected = await repo.drive({
138
- "build.review.collecting": (_, r) => {
139
- requirements(_, r)
140
- r.write(".gtd/REVIEW.md", `${review("also export multiply")} - A: noted.\n`)
141
- },
142
- })
143
- expect(collected.state).toBe("build.review.await-review")
144
-
145
- repo.write(".gtd/REVIEW.md", review())
146
- await repo.land()
147
- const rest = await repo.drive({
148
- "build.review.closing": runsItsScript,
149
- "re-unwind": runsItsScript,
150
- "design.triage": (_, r) =>
151
- r.write(
152
- ".gtd/REQUIREMENTS.md",
153
- "# Requirements\n\n- multiply\n\n## Acceptance Criteria\n\n- AC-1: done\n",
154
- ),
155
- "design.gate.answer": () => undefined,
156
- })
157
- expect(rest.state).toBe("implement.building")
158
- expect(rest.content).toContain("already exists")
159
- expect(rest.content).not.toContain("write the change fresh")
160
- const subjects = repo.subjects()
161
- const reUnwind = subjects.findIndex((subject) => subject.endsWith("→ re-unwind"))
162
- expect(reUnwind).toBeGreaterThan(-1)
163
- expect(subjects.slice(reUnwind + 1).some((subject) => subject.includes("seam."))).toBe(false)
164
- expect(subjects.filter((subject) => subject.includes("→ seam.naive"))).toHaveLength(1)
165
- })
166
- })
167
-
168
67
  describe("afterTail feedback lap", () => {
169
68
  const TAIL = [
170
69
  `import { start } from "@pmelab/gtd/flows"`,