@pmelab/gtd 29.1.0 → 29.1.1

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": "29.1.0",
3
+ "version": "29.1.1",
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
@@ -420,15 +420,20 @@ noted in steps 3 and 4 below.
420
420
 
421
421
  3. **You wait.** The work is split into vertical slices and built one at a time,
422
422
  each one starting from the unit and e2e tests it declares (a build turn
423
- missing one is refused), checked against the full suite (`testCommand`) and
424
- fixed until it passes before moving on — so keep that suite fast; gtd has no
425
- lighter one. Scenario wording is not frozen: you sign the scenarios off once,
426
- in the review's `## Scenarios` chunk, where any assertion an existing
427
- scenario lost is marked `Weakened:`. After the last package the quality lap
428
- runs. Two points along the process are judged rather than always asking you
429
- outright — each stops and hands you a verdict to make (`gtd judge answer`, or
430
- land with a clean tree to accept the conservative default, which never skips
431
- work; `gtd judge run --provider fixed --answers <path>` — or the
423
+ missing one is refused; a test the package only relies on is marked
424
+ `(unchanged)`; it must exist before the turn, and the build turn must not
425
+ create, edit or delete it — a wrong marker is fixed upstream, in the plan,
426
+ and a process stuck on one is ended with `gtd abandon`; and a package whose
427
+ work lies outside the repository, such as filing issues, closes on recorded
428
+ evidence), checked against the full suite (`testCommand`) and fixed until it
429
+ passes before moving on — so keep that suite fast; gtd has no lighter one.
430
+ Scenario wording is not frozen: you sign the scenarios off once, in the
431
+ review's `## Scenarios` chunk, where any assertion an existing scenario lost
432
+ is marked `Weakened:`. After the last package the quality lap runs. Two
433
+ points along the process are judged rather than always asking you outright —
434
+ each stops and hands you a verdict to make (`gtd judge answer`, or land with
435
+ a clean tree to accept the conservative default, which never skips work;
436
+ `gtd judge run --provider fixed --answers <path>` — or the
432
437
  `GTD_JUDGE_ANSWERS` env var, inline JSON — answers one from a file, piped
433
438
  between `gtd judge --json` and `gtd judge answer`;
434
439
  `gtd judge run --provider jev` asks TypeSafe's Jev instead, with the key in
@@ -691,7 +691,12 @@ ${stateFileRules}
691
691
  - Every package with tests has a \`## Tests\` section, one line per
692
692
  declared test, copied from the architecture entries:
693
693
  \`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
694
- none
694
+ none. List every test the package writes or changes; also list an
695
+ existing test a criterion relies on, marked \`(unchanged)\` —
696
+ \`- unit: \\\`<path>\\\` (unchanged)\` or
697
+ \`- e2e: \\\`<path>\\\` (unchanged)\`; list no other untouched test;
698
+ never mark a file the package also writes or changes — list it once,
699
+ unmarked; only decompose places the marker
695
700
  - Leave everything uncommitted and finish
696
701
  `;
697
702
  const architectureGateAnswerMessage = () => `This gate stops on every process, even with no open question, so
@@ -724,15 +729,26 @@ const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
724
729
  never delete the package file (the declared-tests guard reads it
725
730
  after you)
726
731
  - The package to implement is \`${pkg}\`
727
- - First check its acceptance criteria against the current tree —
728
- an earlier fix turn may already satisfy them. If **every**
729
- criterion is met, implement nothing: write \`.gtd/SATISFIED.md\`
730
- with each criterion's concrete evidence (commit, file, or
731
- symbol), change nothing else, and finish. Otherwise implement
732
- normally and skip that file
732
+ - First check the acceptance criteria against the current tree —
733
+ an earlier fix turn may already meet them
734
+ - **Every criterion already met in the tree**: write
735
+ \`.gtd/SATISFIED.md\` with evidence per criterion — commit, file,
736
+ symbol, issue URL, or command output — and change nothing else
737
+ - **No \`## Tests\` and work outside the tree**: first check it was
738
+ not already done by an earlier attempt (e.g. an existing issue
739
+ with the same title via \`gh\`) and reuse that as evidence, or do
740
+ the work; then write \`.gtd/SATISFIED.md\` with that evidence and
741
+ change nothing else in the tree
742
+ - **Any other package**: implement normally, write no
743
+ \`.gtd/SATISFIED.md\`
733
744
  - Write the tests the package declares under \`## Tests\` first,
734
745
  then the code that makes them pass; the turn is refused unless
735
- every declared test is in your diff
746
+ every declared test is in your diff — except a line marked
747
+ \`(unchanged)\`, which must exist before the turn and stay
748
+ untouched — creating, editing or deleting it refuses the turn. If
749
+ it does not exist yet, or the marker is wrong because the package
750
+ must change that test, stop without creating it and leave it to
751
+ a person
736
752
  - A declared e2e test is a scenario in the repository's own e2e
737
753
  framework for the behaviour its \`## E2E Scenarios\` entry names.
738
754
  Word it to fit the existing steps — reuse and generalise them
@@ -760,7 +776,16 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
760
776
  - Introduce no new flag read and add no second read of an existing one:
761
777
  every match of \`${pattern}\` may move, never increase in number`;
762
778
  const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
763
- const refactorBuildingPrompt = (pkg, strays, pattern, reference, feedback = false) => `${stateFileRules}
779
+ function refactorBuildingPrompt(first, strays, pattern, reference, feedback) {
780
+ return refactorBuildingPromptOf(typeof first === "string" ? {
781
+ pkg: first,
782
+ strays: strays ?? [],
783
+ pattern: pattern ?? "",
784
+ reference,
785
+ feedback
786
+ } : first);
787
+ }
788
+ const refactorBuildingPromptOf = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
764
789
  - The package to implement is \`${pkg}\`; never delete it
765
790
  ${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
766
791
  last refactor. Act on it, within the rules below
@@ -1,6 +1,6 @@
1
1
  import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
2
2
  import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { E as reviewing, S as fixRisks, T as reviewQuality, W as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, c as escalation, d as healthy, m as QUALITY, u as guarded, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-DoHUII2D.mjs";
3
+ import { E as reviewing, S as fixRisks, T as reviewQuality, W as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, c as escalation, d as healthy, m as QUALITY, u as guarded, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-fqf-c9dc.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 { _t as registryEntry, a as flagShipped, dt as defaults, ft as envDefaults, ht as flagReferences, i as skills, n as summary, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-DoHUII2D.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C6YBG5zh.mjs";
2
+ import { _t as registryEntry, a as flagShipped, dt as defaults, ft as envDefaults, ht as flagReferences, i as skills, n as summary, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-fqf-c9dc.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Do11DVcw.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, glob, 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 { lensesOf, review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as refactorFixPrompt, A as architectureDecomposePrompt, B as designSystem, C as planner, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as idleMessage, I as buildReviewTriageMessage, J as naivePassPrompt, K as implementBuildingPrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as refactorDroppedRefusal, R as deferNotesScript, S as fixRisks, V as designTriagePrompt, X as packagesItemFixSuitePrompt, Y as packagesItemBuildingPrompt, Z as refactorBuildingPrompt, _ as REVIEW, at as reviewerSystem, b as fixCheck, ct as startGateBlockedMessage, d as healthy, dt as defaults, et as refactorRules, f as ARCHITECTURE, ft as envDefaults, g as REQUIREMENTS, h as REFACTOR, i as skills, it as reviewNotesCapture, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as gate, lt as unwindFailedMessage, n as summary, nt as reviewEditsCapture, o as shipped, ot as seamByHandMessage, p as FEEDBACK, pt as testFileGlobs, q as implementFixPrompt, r as access, st as seamSteerMessage, t as steering, tt as reviewEditNotesCapture, u as guarded, ut as unwindFailure, v as awaitReview, w as reviewMissing, y as coder, z as designGateAnswerMessage } from "../chunks/reserved-DoHUII2D.mjs";
4
- import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-C6YBG5zh.mjs";
3
+ import { $ as refactorFixPrompt, A as architectureDecomposePrompt, B as designSystem, C as planner, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as idleMessage, I as buildReviewTriageMessage, J as naivePassPrompt, K as implementBuildingPrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as refactorDroppedRefusal, R as deferNotesScript, S as fixRisks, V as designTriagePrompt, X as packagesItemFixSuitePrompt, Y as packagesItemBuildingPrompt, Z as refactorBuildingPrompt, _ as REVIEW, at as reviewerSystem, b as fixCheck, ct as startGateBlockedMessage, d as healthy, dt as defaults, et as refactorRules, f as ARCHITECTURE, ft as envDefaults, g as REQUIREMENTS, h as REFACTOR, i as skills, it as reviewNotesCapture, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as gate, lt as unwindFailedMessage, n as summary, nt as reviewEditsCapture, o as shipped, ot as seamByHandMessage, p as FEEDBACK, pt as testFileGlobs, q as implementFixPrompt, r as access, st as seamSteerMessage, t as steering, tt as reviewEditNotesCapture, u as guarded, ut as unwindFailure, v as awaitReview, w as reviewMissing, y as coder, z as designGateAnswerMessage } from "../chunks/reserved-fqf-c9dc.mjs";
4
+ import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-Do11DVcw.mjs";
5
5
  //#region src/workflows/shared/fences.ts
6
6
  const markFenced = (text) => {
7
7
  let fence;
@@ -148,34 +148,86 @@ const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes,
148
148
  });
149
149
  //#endregion
150
150
  //#region src/workflows/feature/packages.ts
151
- const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
152
- /** The `## Tests` entries of a package file; malformed lines are ignored. */
151
+ const TEST_LINE = /^-\s+(unit|e2e):\s+`([^`]+)`(.*)$/;
152
+ const MARKER = /^[ \t]+\(unchanged\)(?=\s|$)/i;
153
+ const ITEM = /^\s*-\s/;
154
+ /** The `## Tests` entries of a package file; malformed entries are ignored. */
153
155
  const declaredTests = (packageText) => {
154
156
  const found = [];
155
157
  let inTests = false;
156
- for (const line of packageText.split("\n")) if (/^##\s/.test(line)) inTests = /^##\s+Tests\s*$/.test(line);
157
- else if (inTests) {
158
- const match = TEST_LINE.exec(line);
158
+ let entry;
159
+ const flush = () => {
160
+ const match = entry === void 0 ? null : TEST_LINE.exec(entry);
159
161
  if (match) found.push({
160
162
  level: match[1],
161
- path: match[2]
163
+ path: match[2],
164
+ unchanged: MARKER.test(match[3])
162
165
  });
163
- }
166
+ entry = void 0;
167
+ };
168
+ for (const line of packageText.replace(/\r\n?/g, "\n").split("\n")) if (/^##\s/.test(line)) {
169
+ flush();
170
+ inTests = /^##\s+Tests\s*$/.test(line);
171
+ } else if (!inTests) continue;
172
+ else if (ITEM.test(line)) {
173
+ flush();
174
+ entry = line.trim();
175
+ } else if (entry !== void 0 && /^\s/.test(line) && line.trim() !== "") entry = `${entry} ${line.trim()}`;
176
+ else flush();
177
+ flush();
164
178
  return found;
165
179
  };
180
+ const diffBucket = (test, status, turn, satisfied) => {
181
+ if (test.unchanged) {
182
+ if (turn !== "build") return void 0;
183
+ if (status === "modified") return "modifiedMarked";
184
+ return status === "added" ? "addedMarked" : void 0;
185
+ }
186
+ return !satisfied && (status === void 0 || status === "deleted") ? "notInDiff" : void 0;
187
+ };
188
+ const treeBucket = (test, status) => {
189
+ if (read(test.path) !== void 0) return void 0;
190
+ return test.unchanged && status === "deleted" ? "deletedMarked" : "notInTree";
191
+ };
192
+ const unmetBucket = (test, change, turn, satisfied) => diffBucket(test, change?.status, turn, satisfied) ?? treeBucket(test, change?.status);
166
193
  /**
167
194
  * Refuse a build or fix turn unless every test `pkg` declares exists and is in the
168
195
  * diff since `since`. The package text is read as of `since`, so a builder
169
- * editing its own `## Tests` cannot dodge the guard; with `.gtd/SATISFIED.md`
170
- * written, existing in the tree suffices.
196
+ * editing its own `## Tests` (or adding `(unchanged)`) cannot dodge the guard;
197
+ * with `.gtd/SATISFIED.md` written, or the line marked, existing suffices. The
198
+ * build turn must neither modify nor add a marked test; a fix turn may edit it.
171
199
  */
172
- const requireDeclaredTests = (pkg, since) => {
200
+ const requireDeclaredTests = (pkg, since, turn = "fix") => {
173
201
  const range = changesSince(since);
174
202
  const text = range.get(pkg)?.before ?? read(pkg) ?? "";
175
203
  const satisfied = read(".gtd/SATISFIED.md") !== void 0;
176
- const missing = declaredTests(text).map((test) => test.path).filter((path) => satisfied ? read(path) === void 0 : range.get(path) === void 0 || range.get(path)?.status === "deleted" || read(path) === void 0);
177
- if (missing.length === 0) return;
178
- refuse(`gtd land: declared-tests: ${pkg} declares tests that are ${satisfied ? "missing from the tree" : "not in the package's diff"} — write them first:\n${missing.map((path) => ` - ${path}`).join("\n")}`);
204
+ const buckets = {
205
+ notInDiff: [],
206
+ modifiedMarked: [],
207
+ addedMarked: [],
208
+ deletedMarked: [],
209
+ notInTree: []
210
+ };
211
+ const entries = declaredTests(text);
212
+ const seen = /* @__PURE__ */ new Set();
213
+ for (const test of entries) {
214
+ if (test.unchanged && entries.some((e) => !e.unchanged && e.path === test.path)) continue;
215
+ const bucket = unmetBucket(test, range.get(test.path), turn, satisfied);
216
+ if (bucket && !seen.has(test.path)) {
217
+ seen.add(test.path);
218
+ buckets[bucket].push(test.path);
219
+ }
220
+ }
221
+ const { notInDiff, modifiedMarked, addedMarked, deletedMarked, notInTree } = buckets;
222
+ const reasons = [
223
+ ["not in the package's diff — write them", notInDiff],
224
+ ["marked (unchanged) but modified — revert them", modifiedMarked],
225
+ ["marked (unchanged) but added by this turn — the marker promises an existing test; stop and leave it to a person", addedMarked],
226
+ ["marked (unchanged) but deleted — restore them", deletedMarked],
227
+ ["missing from the tree", notInTree]
228
+ ].filter(([, paths]) => paths.length > 0);
229
+ if (reasons.length === 0) return;
230
+ refuse(`gtd land: declared-tests: ${pkg} declares tests this turn left unmet:\n${reasons.map(([reason, paths]) => `${reason}:\n${paths.map((path) => ` - ${path}`).join("\n")}`).join("\n")}`);
179
231
  };
180
232
  /**
181
233
  * Build `pkg`, check its declared tests, keep the full suite green, and close
@@ -183,12 +235,12 @@ const requireDeclaredTests = (pkg, since) => {
183
235
  */
184
236
  const packageItem = async (pkg) => {
185
237
  const from = head();
186
- const declared = (turn) => async () => {
238
+ const declared = (turn, kind) => async () => {
187
239
  await turn();
188
- requireDeclaredTests(pkg, from);
240
+ requireDeclaredTests(pkg, from, kind);
189
241
  };
190
- await declared(() => build(pkg))();
191
- await healthy(declared(fixSuite));
242
+ await declared(() => build(pkg), "build")();
243
+ await healthy(declared(fixSuite, "fix"));
192
244
  await run("closing", removeScript([
193
245
  pkg,
194
246
  ".gtd/SATISFIED.md",
@@ -423,8 +475,15 @@ const review = (base, escalations = { rounds: 0 }, options = {}) => {
423
475
  }
424
476
  });
425
477
  };
426
- /** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
427
- const buildTail = (fixFirst, base, green = false, options = {}) => tail(fixFirst, (escalations) => review(base, escalations, options), green, options.guard, options.lenses, options.guardRests, options.lensBase, options.rules);
478
+ function buildTail(first, second, third = false, fourth = {}) {
479
+ const base = typeof first === "string" ? first : second;
480
+ const options = typeof first === "string" ? second ?? {} : {
481
+ ...fourth,
482
+ fixFirst: first,
483
+ green: third
484
+ };
485
+ return tail(options.fixFirst ?? false, (escalations) => review(base, escalations, options), options.green ?? false, options.guard, options.lenses, options.guardRests, options.lensBase, options.rules);
486
+ }
428
487
  /**
429
488
  * Undo the human's review-round code edits. A path changed since the human
430
489
  * left it is not overwritten; requireRevert names it instead.
@@ -565,7 +624,13 @@ const refactorLap = async (p, guard, feedback) => {
565
624
  label: "Deriving the refactor package",
566
625
  file: REFACTOR_PACKAGE
567
626
  });
568
- await agentWithSkills("building", refactorBuildingPrompt(REFACTOR_PACKAGE, strays, pattern, reference, feedback !== void 0), {
627
+ await agentWithSkills("building", refactorBuildingPrompt({
628
+ pkg: REFACTOR_PACKAGE,
629
+ strays,
630
+ pattern,
631
+ reference,
632
+ feedback: feedback !== void 0
633
+ }), {
569
634
  label: "Refactoring",
570
635
  model: coder(),
571
636
  system: builderSystem()
@@ -621,7 +686,8 @@ const reviewedRefactor = async (shape, options = {}) => {
621
686
  let collected;
622
687
  for (;;) {
623
688
  await refactorLap(p, guard, collected);
624
- const outcome = await buildTail(false, entry, true, {
689
+ const outcome = await buildTail(entry, {
690
+ green: true,
625
691
  guard,
626
692
  guardRests: false,
627
693
  lenses: lensesOf(vars.refactorQualityReviews ?? ""),
@@ -654,20 +720,27 @@ const naivePass = (rejection) => agentWithSkills("naive", naivePassPrompt(reject
654
720
  model: coder(),
655
721
  system: builderSystem()
656
722
  });
657
- const implement = (reference, tier, amend) => scope("implement", async () => {
658
- await agentWithSkills("building", implementBuildingPrompt(reference, tier, amend), {
659
- label: "Implementing",
660
- model: coder(),
661
- system: builderSystem()
723
+ function implement(first, tier, amend) {
724
+ const options = typeof first === "object" && first !== null ? first : {
725
+ reference: first,
726
+ tier,
727
+ amend
728
+ };
729
+ return scope("implement", async () => {
730
+ await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend), {
731
+ label: "Implementing",
732
+ model: coder(),
733
+ system: builderSystem()
734
+ });
735
+ await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
736
+ label: "Fixing the check",
737
+ file: FEEDBACK,
738
+ model: coder(),
739
+ system: builderSystem()
740
+ }));
741
+ await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
662
742
  });
663
- await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
664
- label: "Fixing the check",
665
- file: FEEDBACK,
666
- model: coder(),
667
- system: builderSystem()
668
- }));
669
- await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
670
- });
743
+ }
671
744
  const decide = () => scope("seam", async () => {
672
745
  let rejection;
673
746
  let retries = 0;
@@ -742,7 +815,10 @@ const planAndBuild = async (firstBase, firstLap = "first") => {
742
815
  await design(base);
743
816
  await architecture();
744
817
  await packages();
745
- const outcome = await buildTail(false, base, true, lap === "first" ? {} : { lap });
818
+ const outcome = await buildTail(base, {
819
+ green: true,
820
+ ...lap === "first" ? {} : { lap }
821
+ });
746
822
  if (outcome.verdict === "signoff") return;
747
823
  await reUnwind(outcome);
748
824
  base = outcome.base;
@@ -754,8 +830,14 @@ const naivePlanAndBuild = async (firstBase) => {
754
830
  const tier = riskTierOn() ? recordRisk() : void 0;
755
831
  const seam = await seamBuild(firstBase);
756
832
  await slice("feature", async () => {
757
- await implement(seam.reference, tier);
758
- await featureFeedback(await buildTail(false, seam.base, true, { lensBase: seam.base }), seam.base);
833
+ await implement({
834
+ reference: seam.reference,
835
+ tier
836
+ });
837
+ await featureFeedback(await buildTail(seam.base, {
838
+ green: true,
839
+ lensBase: seam.base
840
+ }), seam.base);
759
841
  });
760
842
  };
761
843
  const featureFeedback = async (first, lensBase) => {
@@ -763,8 +845,12 @@ const featureFeedback = async (first, lensBase) => {
763
845
  while (outcome.verdict === "feedback") {
764
846
  await reUnwind(outcome);
765
847
  await design(outcome.base);
766
- await implement(void 0, riskTierOn() ? recordRisk() : void 0, true);
767
- outcome = await buildTail(false, outcome.base, true, {
848
+ await implement({
849
+ tier: riskTierOn() ? recordRisk() : void 0,
850
+ amend: true
851
+ });
852
+ outcome = await buildTail(outcome.base, {
853
+ green: true,
768
854
  lap: "feedback",
769
855
  ...lensBase === void 0 ? {} : { lensBase }
770
856
  });
@@ -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 finisherSystem, U as fixReviewNotesPrompt, _ as REVIEW, dt as defaults, ft as envDefaults, i as skills, n as summary, o as shipped, r as access, s as baseline, t as steering, y as coder } from "../chunks/reserved-DoHUII2D.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-C6YBG5zh.mjs";
3
+ import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, _ as REVIEW, dt as defaults, ft as envDefaults, i as skills, n as summary, o as shipped, r as access, s as baseline, t as steering, y as coder } from "../chunks/reserved-fqf-c9dc.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-Do11DVcw.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 { dt as defaults, ft as envDefaults, i as skills, l as gate, n as summary, o as shipped, r as access, rt as reviewGateBlockedMessage, t as steering } from "../chunks/reserved-DoHUII2D.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C6YBG5zh.mjs";
2
+ import { dt as defaults, ft as envDefaults, i as skills, l as gate, n as summary, o as shipped, r as access, rt as reviewGateBlockedMessage, t as steering } from "../chunks/reserved-fqf-c9dc.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Do11DVcw.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 { a as flagShipped, dt as defaults, ft as envDefaults, gt as flipProblems, i as skills, l as gate, mt as flagMentions, n as summary, p as FEEDBACK, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-DoHUII2D.mjs";
2
+ import { a as flagShipped, dt as defaults, ft as envDefaults, gt as flipProblems, i as skills, l as gate, mt as flagMentions, n as summary, p as FEEDBACK, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-fqf-c9dc.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.";