@pmelab/gtd 46.0.0 → 47.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": "46.0.0",
3
+ "version": "47.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
@@ -504,12 +504,12 @@ noted in steps 3 and 4 below.
504
504
  unsure verdict (below `qualityTriageMinP`, `0.7`) is `design`; mechanical
505
505
  findings the fix turns leave become notes too. This lap is where code quality
506
506
  is looked at, and every round pays for it. It never replaces step 4 — your
507
- review stays the final gate, and nothing here skips it. Between
508
- [coarse slices](#coarse-slices) no review follows, so there the fix turn
509
- fixes every finding, and those the fix cap leaves rest at
510
- `build.quality-unresolved` until you delete or empty `.gtd/QUALITY.md`; the
511
- suite then runs on any hand edits before the next slice. The `fix` door
512
- (below) repairs a red baseline through the untriaged lap.
507
+ review stays the final gate, and nothing here skips it. Every
508
+ [coarse slice](docs/configuration.md#coarse-slices) runs the same triaged lap
509
+ and never rests for its findings: its notes are carried to the review after
510
+ the last slice, each naming its slice and lens, beside every slice's fixes.
511
+ The `fix` and `cleanup` doors triage their lap too, and hand its notes to
512
+ their own review.
513
513
 
514
514
  A red suite that keeps failing past a few fix attempts escalates instead of
515
515
  retrying forever: an agent turn reads the failing output and writes
@@ -1,6 +1,6 @@
1
1
  import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, mark, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
2
2
  import { d as ANSWER_FIX, f as ANSWER_LEAD, p as ANSWER_WONT_FIX, v as FREE_TEXT_PLACEHOLDER } from "./steering-D7hWeMgE.mjs";
3
- import { escalation as escalation$1, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
3
+ import { gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
4
4
  import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$, s as Number$ } from "./Schema-C6fLlQzx.mjs";
5
5
  //#region src/workflows/shared/registry.ts
6
6
  const FILL = "";
@@ -544,6 +544,20 @@ const SUMMARY_TITLES = [
544
544
  ];
545
545
  const unboxed = (body) => body.replace(/^(\s*(?:>\s*)*(?:[-*+]|\d+[.)])\s+)\[[ xX]\]\s?/gm, "$1");
546
546
  const short = (hash) => hash.slice(0, 7);
547
+ /** `slice 2, lens tests`; empty when neither is known. */
548
+ const originOf = (f) => [...f.slice === void 0 ? [] : [`slice ${f.slice}`], ...f.lens === void 0 ? [] : [`lens ${f.lens}`]].join(", ");
549
+ const originLine = (f) => {
550
+ const origin = originOf(f);
551
+ return origin === "" ? "" : `_From ${origin}._\n\n`;
552
+ };
553
+ const withOrigin = (f, rest) => [rest, originOf(f)].filter((s) => s !== "").join(", ");
554
+ const noteBody = (n) => {
555
+ const body = unboxed(n.body) || "_No detail given._";
556
+ if (n.unresolved === true) return `_The fixer left this unresolved._\n\n${body}`;
557
+ if (n.unclear === true) return `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${body}`;
558
+ return body;
559
+ };
560
+ const carriedOnly = (f) => f.slice === void 0 ? {} : f;
547
561
  /**
548
562
  * \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
549
563
  * answer boxes, then the fixed and noise lists. Chunk titles are made unique —
@@ -570,25 +584,14 @@ const composedReview = (base, report) => {
570
584
  ...report.nitFix === void 0 ? [] : [chunk("Fixed from your notes", `Your nits and fix-as-suggested notes were worked on in ${short(report.nitFix)}; the notes you answered are left out — check the diff.`)],
571
585
  ...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
572
586
  ...conflicts,
573
- ...notes.map((n) => chunk(n.title, `${n.unresolved === true ? `_The fixer left this unresolved._\n\n${unboxed(n.body)}` : n.unclear === true ? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}` : unboxed(n.body) || "_No detail given._"}\n\n${answerBoxes}`)),
574
- ...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
587
+ ...notes.map((n) => chunk(n.title, `${originLine(carriedOnly(n))}${noteBody(n)}\n\n${answerBoxes}`)),
588
+ ...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => fixedLine(f, carriedOnly(f))).join("\n"))],
575
589
  ...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
576
- ...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
590
+ ...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => noiseLine(f, carriedOnly(f))).join("\n"))]
577
591
  ];
578
592
  const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
579
593
  return `# Review: ${short(base)}\n\n<!-- base: ${base} -->\n\n${body.join("\n\n")}\n`;
580
594
  };
581
- const qualityUnresolvedMessage = () => `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
582
- holds findings. Nothing after this fixes them, so the process will not
583
- go on with them open.
584
-
585
- Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
586
- dropping a finding is your call to make.
587
-
588
- What each change does next (then run \`gtd land\`):
589
- - **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the next slice is built, or after the last one you review the change (**build.review.reviewing**).
590
- - **Anything else** — rests here again.
591
- `;
592
595
  const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
593
596
  \`.gtd/FEEDBACK.md\` holds the failing output.
594
597
 
@@ -1254,6 +1257,28 @@ const lapRange = {
1254
1257
  feedback: "it starts at the previous review's boundary, so it covers only what is new since the previous review.",
1255
1258
  refactor: "it covers the whole refactor since it started, on every round. A refactor may update an existing test's call sites only: give any test-file hunk its own chunk, and say so if it changes a test, an assertion or an expected value."
1256
1259
  };
1260
+ const fixedLine = (f, origin = f) => `- ${f.title} (${withOrigin(origin, short(f.commit))})`;
1261
+ const noiseLine = (f, origin = f) => `- ${f.title} (${withOrigin(origin, `p=${f.p}`)})`;
1262
+ /**
1263
+ * `review` with the lap's notes appended as prose chunks — no pointer, no
1264
+ * answer boxes — then what it fixed and dropped. A title the review already
1265
+ * holds gets a numbered suffix, as notes are told apart by title.
1266
+ */
1267
+ const withLapFindings = (review, { notes, fixed, noise }) => {
1268
+ const used = new Set(review.split("\n").filter((line) => line.startsWith("## ")).map((line) => line.slice(3).trim()));
1269
+ const unique = (title) => {
1270
+ let name = title;
1271
+ for (let n = 2; used.has(name); n++) name = `${title} (${n})`;
1272
+ used.add(name);
1273
+ return name;
1274
+ };
1275
+ const chunks = [
1276
+ ...notes.map((n) => `## ${unique(n.title)}\n\n${originLine(n)}${noteBody(n)}`),
1277
+ ...fixed.length === 0 ? [] : [`## ${unique("Fixed automatically")}\n\n${fixed.map((f) => fixedLine(f)).join("\n")}`],
1278
+ ...noise.length === 0 ? [] : [`## ${unique("Dropped as noise")}\n\n${noise.map((f) => noiseLine(f)).join("\n")}`]
1279
+ ];
1280
+ return chunks.length === 0 ? review : `${review.trimEnd()}\n\n${chunks.join("\n\n")}\n`;
1281
+ };
1257
1282
  const buildReviewReviewingPrompt = (base, lap, carry, removed = []) => `${styleBlock}
1258
1283
 
1259
1284
  ${styleFormatContract}
@@ -1798,12 +1823,6 @@ const guardedSteps = (guard, describeGuard, rests = true, rules) => {
1798
1823
  exhausted: guarded(steps.exhausted, rests ? guard : void 0)
1799
1824
  };
1800
1825
  };
1801
- /**
1802
- * Hand a red suite that repeated attempts could not fix to a person: an
1803
- * agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
1804
- * on, the human is told the budget is spent instead.
1805
- */
1806
- const escalation = (count, guard, guardRests = true, rules) => escalation$1(count, guardedSteps(guard, void 0, guardRests, rules));
1807
1826
  /** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
1808
1827
  const retryQuestion = (comparable) => comparable ? {
1809
1828
  id: "verdict",
@@ -1986,4 +2005,4 @@ const steering = {
1986
2005
  [REVIEW]: "review"
1987
2006
  };
1988
2007
  //#endregion
1989
- export { naiveApplyFailure as $, reviewMissing as A, conflictTitle as At, coverageMessage as B, requireFlag as Bt, coder as C, unwindFailure as Ct, fixQuality as D, STANDARDS as Dt, fixNits as E, testFileGlobs as Et, buildReviewTriageMessage as F, standardKey as Ft, designSystem as G, coveringPrompt as H, builderSystem as I, flagMentions as It, fixReviewNotesPrompt as J, designTriagePrompt as K, clearCriteriaMessage as L, flagReferences as Lt, reviewing as M, oneLine as Mt, agentWithSkills as N, parseStandards as Nt, fixRisks as O, conflictBody as Ot, buildReviewCollectingPrompt as P, renderStandards as Pt, implementFixPrompt as Q, clearCriteriaQuestion as R, flipProblems as Rt, awaitReview as S, unwindFailedMessage as St, fixCheck as T, envDefaults as Tt, criterionLine as U, coverageQuestion as V, designGateAnswerMessage as W, idleMessage as X, handOffAwaitReviewMessage as Y, implementBuildingPrompt as Z, REFACTOR_NOTE as _, seamByHandMessage as _t, flagModeOn as a, qualityUnresolvedMessage as at, SLICES as b, startGateBlockedMessage as bt, baseline as c, refactorFixPrompt as ct, guarded as d, reviewEditNotesCapture as dt, naivePassPrompt as et, healthy as f, reviewEditsCapture as ft, REFACTOR as g, reviewerSystem as gt, QUALITY as h, reviewNotesCapture as ht, skills as i, qualityTriageMessage as it, reviewQuality as j, isConflictTitle as jt, planner as k, conflictFor as kt, escalation as l, refactorNotePrompt as lt, HANDOFF as m, reviewGateBlockedMessage as mt, summary as n, oversizedMessage as nt, flagShipped as o, refactorBuildingPrompt as ot, FEEDBACK as p, reviewFeedbackFixPrompt as pt, finisherSystem as q, access as r, qualityFindingsFile as rt, shipped as s, refactorDroppedRefusal as st, steering as t, normaliseCriteriaScript as tt, gate as u, refactorRules as ut, REQUIREMENTS as v, seamSteerMessage as vt, collecting as w, defaults as wt, answerReviewQuestions as x, uncoveredMessage as xt, REVIEW as y, slicingPrompt as yt, composedReview as z, registryEntry as zt };
2008
+ export { naivePassPrompt as $, reviewQuality as A, isConflictTitle as At, coverageQuestion as B, collecting as C, defaults as Ct, fixRisks as D, conflictBody as Dt, fixQuality as E, STANDARDS as Et, builderSystem as F, flagMentions as Ft, designTriagePrompt as G, criterionLine as H, clearCriteriaMessage as I, flagReferences as It, handOffAwaitReviewMessage as J, finisherSystem as K, clearCriteriaQuestion as L, flipProblems as Lt, agentWithSkills as M, parseStandards as Mt, buildReviewCollectingPrompt as N, renderStandards as Nt, planner as O, conflictFor as Ot, buildReviewTriageMessage as P, standardKey as Pt, naiveApplyFailure as Q, composedReview as R, registryEntry as Rt, coder as S, withLapFindings as St, fixNits as T, testFileGlobs as Tt, designGateAnswerMessage as U, coveringPrompt as V, designSystem as W, implementBuildingPrompt as X, idleMessage as Y, implementFixPrompt as Z, REQUIREMENTS as _, slicingPrompt as _t, flagModeOn as a, refactorDroppedRefusal as at, answerReviewQuestions as b, unwindFailedMessage as bt, baseline as c, refactorRules as ct, healthy as d, reviewFeedbackFixPrompt as dt, normaliseCriteriaScript as et, FEEDBACK as f, reviewGateBlockedMessage as ft, REFACTOR_NOTE as g, seamSteerMessage as gt, REFACTOR as h, seamByHandMessage as ht, skills as i, refactorBuildingPrompt as it, reviewing as j, oneLine as jt, reviewMissing as k, conflictTitle as kt, gate as l, reviewEditNotesCapture as lt, QUALITY as m, reviewerSystem as mt, summary as n, qualityFindingsFile as nt, flagShipped as o, refactorFixPrompt as ot, HANDOFF as p, reviewNotesCapture as pt, fixReviewNotesPrompt as q, access as r, qualityTriageMessage as rt, shipped as s, refactorNotePrompt as st, steering as t, oversizedMessage as tt, guarded as u, reviewEditsCapture as ut, REVIEW as v, startGateBlockedMessage as vt, fixCheck as w, envDefaults as wt, awaitReview as x, unwindFailure as xt, SLICES as y, uncoveredMessage as yt, coverageMessage as z, requireFlag as zt };
@@ -1,7 +1,7 @@
1
1
  import { answered, changesSince, head, judge, numeric, read, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
2
2
  import { removedAssertions, review } from "../workflow-kit.mjs";
3
- import { A as reviewMissing, At as conflictTitle, Dt as STANDARDS, E as fixNits, F as buildReviewTriageMessage, Ft as standardKey, Mt as oneLine, Nt as parseStandards, O as fixRisks, Ot as conflictBody, Pt as renderStandards, S as awaitReview, T as fixCheck, d as guarded, dt as reviewEditNotesCapture, f as healthy, ft as reviewEditsCapture, h as QUALITY, ht as reviewNotesCapture, jt as isConflictTitle, kt as conflictFor, w as collecting, x as answerReviewQuestions, y as REVIEW, z as composedReview } from "./reserved-BENPy6IJ.mjs";
4
- import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-CDNMZ-2E.mjs";
3
+ import { At as isConflictTitle, C as collecting, D as fixRisks, Dt as conflictBody, Et as STANDARDS, Mt as parseStandards, Nt as renderStandards, Ot as conflictFor, P as buildReviewTriageMessage, Pt as standardKey, R as composedReview, T as fixNits, b as answerReviewQuestions, d as healthy, jt as oneLine, k as reviewMissing, kt as conflictTitle, lt as reviewEditNotesCapture, pt as reviewNotesCapture, u as guarded, ut as reviewEditsCapture, v as REVIEW, w as fixCheck, x as awaitReview } from "./reserved-DF1HD6jL.mjs";
4
+ import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-D89fU_wf.mjs";
5
5
  //#region src/workflows/shared/learn.ts
6
6
  const SUMMARY_MAX = 200;
7
7
  const BANNER = /^_(?:The fixer left this unresolved\.|A fix turn removed this while renaming another finding — check whether it was fixed\.|No detail given\.)_\s*/;
@@ -276,7 +276,7 @@ const triage = (severities) => async (notes) => {
276
276
  const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
277
277
  /**
278
278
  * gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
279
- * turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
279
+ * turn. A re-review
280
280
  * after fixed nits restores the doc as committed at `carry`, answers kept;
281
281
  * a fresh one leaves out the notes the human already answered.
282
282
  */
@@ -289,7 +289,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
289
289
  added: standardsAddedSince(base),
290
290
  undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
291
291
  }
292
- }), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
292
+ })) : showScript(carry, REVIEW, REVIEW), {
293
293
  label: "Composing the review",
294
294
  file: REVIEW,
295
295
  base
@@ -371,10 +371,7 @@ function buildTail(first, second, ...rest) {
371
371
  return tail((escalations, report) => humanReview(base, escalations, {
372
372
  ...form,
373
373
  report
374
- }), {
375
- ...form,
376
- triage: true
377
- });
374
+ }), form);
378
375
  }
379
376
  const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
380
377
  const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
@@ -1,12 +1,24 @@
1
1
  import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
2
- import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { A as reviewMissing, D as fixQuality, M as reviewing, O as fixRisks, S as awaitReview, T as fixCheck, Y as handOffAwaitReviewMessage, d as guarded, f as healthy, h as QUALITY, it as qualityTriageMessage, j as reviewQuality, l as escalation, rt as qualityFindingsFile, y as REVIEW } from "./reserved-BENPy6IJ.mjs";
2
+ import { lensEntriesOf, lensOf, qualityLap, reviewToHandOff } from "../workflow-kit.mjs";
3
+ import { A as reviewQuality, D as fixRisks, E as fixQuality, J as handOffAwaitReviewMessage, St as withLapFindings, d as healthy, j as reviewing, k as reviewMissing, m as QUALITY, nt as qualityFindingsFile, rt as qualityTriageMessage, u as guarded, v as REVIEW, w as fixCheck, x as awaitReview } from "./reserved-DF1HD6jL.mjs";
4
4
  //#region src/workflows/shared/lenses.ts
5
5
  const NO_FINDINGS = {
6
6
  notes: [],
7
7
  fixed: [],
8
8
  noise: []
9
9
  };
10
+ /** `reports` as one, each finding labelled with the slice it came from, in slice order. */
11
+ const joinReports = (reports) => {
12
+ const tag = (slice, fs) => fs.map((f) => ({
13
+ ...f,
14
+ slice
15
+ }));
16
+ return {
17
+ notes: reports.flatMap((r) => tag(r.slice, r.report.notes)),
18
+ fixed: reports.flatMap((r) => tag(r.slice, r.report.fixed)),
19
+ noise: reports.flatMap((r) => tag(r.slice, r.report.noise))
20
+ };
21
+ };
10
22
  const UNTITLED = "Quality findings";
11
23
  const below = (section) => section.split("\n").slice(1).join("\n").trim();
12
24
  const strayProse = (text, sections) => sections.reduce((rest, body) => rest.replace(body, "\n"), text).split("\n").filter((line) => !/^#\s/.test(line)).join("\n").trim();
@@ -86,7 +98,14 @@ const classify = async (findings) => {
86
98
  */
87
99
  const triageFindings = async (options) => {
88
100
  const { escalations, guard, guardRests, rules } = options;
89
- const findings = findingsOf(read(".gtd/QUALITY.md") ?? "");
101
+ const withLens = (f) => {
102
+ const lens = options.lensOf?.(f);
103
+ return lens === void 0 ? f : {
104
+ ...f,
105
+ lens
106
+ };
107
+ };
108
+ const findings = findingsOf(read(".gtd/QUALITY.md") ?? "").map(withLens);
90
109
  if (findings.length === 0) return NO_FINDINGS;
91
110
  const { mechanical, noise, design } = await classify(findings);
92
111
  const sorted = qualityFindingsFile(mechanical);
@@ -103,7 +122,10 @@ const triageFindings = async (options) => {
103
122
  commit
104
123
  })));
105
124
  unclear.push(...turn.unclear);
106
- open = turn.open;
125
+ open = turn.open.map((f) => ({
126
+ ...mechanical.find((m) => m.title === f.title),
127
+ ...f
128
+ }));
107
129
  }
108
130
  if (mechanical.length > 0) await healthy(() => fixCheck(rules), {
109
131
  escalations,
@@ -131,22 +153,15 @@ const triageFindings = async (options) => {
131
153
  //#region src/workflows/shared/tail.ts
132
154
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
133
155
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
134
- /**
135
- * One review turn per lens over the whole change, each appending every
136
- * finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
137
- * has any.
138
- */
139
- const qualityLap = (guard, lenses, base) => qualityLap$1(lenses ?? lensEntriesOf(vars.qualityReviews ?? ""), guarded((entry) => {
156
+ const lensLap = (guard, lenses, base, seen) => qualityLap(lenses ?? lensEntriesOf(vars.qualityReviews ?? ""), guarded(async (entry) => {
140
157
  const { lens, model } = lensOf(entry);
141
- return reviewQuality(lens, base, model);
158
+ await reviewQuality(lens, base, model);
159
+ seen(lens, findingsOf(read(".gtd/QUALITY.md") ?? ""));
142
160
  }, guard), QUALITY);
143
- /** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
144
- const fixQualityFindings = (escalations, guard, guardRests = true, rules) => fixFindings({
145
- fix: guarded(() => fixQuality(rules), guard),
146
- findings: QUALITY,
147
- cap: 3,
148
- escalate: () => escalation(escalations, guard, guardRests, rules)
149
- });
161
+ const closeQuality = async () => {
162
+ if (read(".gtd/QUALITY.md") === void 0) return;
163
+ await run("quality.closing", removeScript([QUALITY]), { label: "Clearing quality findings" });
164
+ };
150
165
  /** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
151
166
  const closeReview = (round) => run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
152
167
  label: "Closing the review",
@@ -183,7 +198,7 @@ const runTail = (finish, options) => scope("build", async () => {
183
198
  const guardRests = options.guardRests === void 0 ? true : options.guardRests;
184
199
  const escalations = { rounds: 0 };
185
200
  const fix = () => fixCheck(rules);
186
- const repair = async () => {
201
+ if (options.fixFirst) {
187
202
  await guarded(fix, guard)();
188
203
  await healthy(fix, {
189
204
  fixesSoFar: 1,
@@ -192,38 +207,52 @@ const runTail = (finish, options) => scope("build", async () => {
192
207
  guardRests,
193
208
  rules
194
209
  });
195
- };
196
- if (options.fixFirst) await repair();
197
- else if (!options.green) await healthy(fix, {
210
+ } else if (!options.green) await healthy(fix, {
198
211
  escalations,
199
212
  guard,
200
213
  guardRests,
201
214
  rules
202
215
  });
203
- const lap = await qualityLap(guard, lenses, lensBase);
204
- if (options.triage) return finish(escalations, lap === "findings" ? await triageFindings({
205
- escalations,
206
- guard,
207
- guardRests,
208
- rules
209
- }) : NO_FINDINGS);
210
- if (lap === "findings") if (await fixQualityFindings(escalations, guard, guardRests, rules)) await healthy(fix, {
216
+ const byTitle = [];
217
+ const lap = await lensLap(guard, lenses, lensBase, (lens, findings) => {
218
+ const before = byTitle.map((f) => f.title);
219
+ for (const { title } of findings) {
220
+ const at = before.indexOf(title);
221
+ if (at >= 0) before.splice(at, 1);
222
+ else byTitle.push({
223
+ title,
224
+ lens
225
+ });
226
+ }
227
+ });
228
+ const lensOf = (f) => {
229
+ const at = byTitle.findIndex((e) => e.title === f.title);
230
+ return at < 0 ? void 0 : byTitle.splice(at, 1)[0].lens;
231
+ };
232
+ const report = lap === "findings" ? await triageFindings({
211
233
  escalations,
212
234
  guard,
213
235
  guardRests,
214
- rules
215
- });
216
- else await repair();
217
- return finish(escalations, NO_FINDINGS);
236
+ rules,
237
+ lensOf
238
+ }) : NO_FINDINGS;
239
+ await closeQuality();
240
+ return finish(escalations, report);
218
241
  });
219
242
  /**
220
243
  * The fix workflow's human review since `base`: a sign-off closes
221
244
  * `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
222
245
  * `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
223
246
  */
224
- const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
247
+ const handOffReview = (base, escalations, guard, report = NO_FINDINGS) => reviewToHandOff(base, {
225
248
  file: REVIEW,
226
- reviewing: guarded((b, carry) => reviewing(b, carry), guard),
249
+ reviewing: guarded(async (b, carry) => {
250
+ await reviewing(b, carry);
251
+ const review = read(REVIEW);
252
+ if (review === void 0) return;
253
+ const full = withLapFindings(review, report);
254
+ if (full !== review) await run("review.carrying", writeScript(REVIEW, full), { label: "Adding the quality lap's findings" });
255
+ }, guard),
227
256
  fixRisks: guarded(fixRisks, guard),
228
257
  keepGreen: () => healthy(fixCheck, {
229
258
  escalations,
@@ -237,10 +266,10 @@ const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
237
266
  unparsed: "gtd land: review-doc: your edit to .gtd/REVIEW.md contains no note fix can read — restore the deleted chunk, or write the note as a line under its entry.",
238
267
  deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
239
268
  });
240
- /** The tail the fix workflow runs: ends at the hand-off review since `base`. */
241
- const handOffTail = (fixFirst, base, guard) => tail((escalations) => handOffReview(base, escalations, guard), {
269
+ /** The tail the fix workflow runs: ends at the hand-off review since `base`, which gets what the lap left. */
270
+ const handOffTail = (fixFirst, base, guard) => tail((escalations, report) => handOffReview(base, escalations, guard, report), {
242
271
  fixFirst,
243
272
  guard
244
273
  });
245
274
  //#endregion
246
- export { refusePendingHandOff as a, isOptionalObject as i, closeReview as n, tail as o, handOffTail as r, NO_FINDINGS as s, REVIEW_ORIG as t };
275
+ export { refusePendingHandOff as a, joinReports as c, isOptionalObject as i, closeReview as n, tail as o, handOffTail as r, NO_FINDINGS as s, REVIEW_ORIG as t };
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { Bt as requireFlag, C as coder, Lt as flagReferences, Tt as envDefaults, i as skills, n as summary, o as flagShipped, r as access, t as steering, wt as defaults, zt as registryEntry } from "../chunks/reserved-BENPy6IJ.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CDNMZ-2E.mjs";
2
+ import { Ct as defaults, It as flagReferences, Rt as registryEntry, S as coder, i as skills, n as summary, o as flagShipped, r as access, t as steering, wt as envDefaults, zt as requireFlag } from "../chunks/reserved-DF1HD6jL.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-D89fU_wf.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,8 +1,8 @@
1
1
  import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
2
2
  import { lensEntriesOf } from "../workflow-kit.mjs";
3
- import { $ as naiveApplyFailure, B as coverageMessage, C as coder, Ct as unwindFailure, E as fixNits, Et as testFileGlobs, G as designSystem, H as coveringPrompt, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, N as agentWithSkills, P as buildReviewCollectingPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, St as unwindFailedMessage, T as fixCheck, Tt as envDefaults, U as criterionLine, V as coverageQuestion, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as seamByHandMessage, a as flagModeOn, at as qualityUnresolvedMessage, b as SLICES, bt as startGateBlockedMessage, ct as refactorFixPrompt, et as naivePassPrompt, f as healthy, g as REFACTOR, gt as reviewerSystem, h as QUALITY, i as skills, k as planner, lt as refactorNotePrompt, m as HANDOFF, n as summary, nt as oversizedMessage, ot as refactorBuildingPrompt, p as FEEDBACK, r as access, s as shipped, st as refactorDroppedRefusal, t as steering, tt as normaliseCriteriaScript, u as gate, ut as refactorRules, v as REQUIREMENTS, vt as seamSteerMessage, w as collecting, wt as defaults, x as answerReviewQuestions, xt as uncoveredMessage, yt as slicingPrompt } from "../chunks/reserved-BENPy6IJ.mjs";
4
- import { a as refusePendingHandOff, o as tail } from "../chunks/tail-CDNMZ-2E.mjs";
5
- import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-DcbgUwr7.mjs";
3
+ import { $ as naivePassPrompt, B as coverageQuestion, C as collecting, Ct as defaults, F as builderSystem, G as designTriagePrompt, H as criterionLine, I as clearCriteriaMessage, L as clearCriteriaQuestion, M as agentWithSkills, N as buildReviewCollectingPrompt, O as planner, Q as naiveApplyFailure, S as coder, T as fixNits, Tt as testFileGlobs, U as designGateAnswerMessage, V as coveringPrompt, W as designSystem, X as implementBuildingPrompt, Y as idleMessage, Z as implementFixPrompt, _ as REQUIREMENTS, _t as slicingPrompt, a as flagModeOn, at as refactorDroppedRefusal, b as answerReviewQuestions, bt as unwindFailedMessage, ct as refactorRules, d as healthy, et as normaliseCriteriaScript, f as FEEDBACK, g as REFACTOR_NOTE, gt as seamSteerMessage, h as REFACTOR, ht as seamByHandMessage, i as skills, it as refactorBuildingPrompt, l as gate, mt as reviewerSystem, n as summary, ot as refactorFixPrompt, p as HANDOFF, r as access, s as shipped, st as refactorNotePrompt, t as steering, tt as oversizedMessage, vt as startGateBlockedMessage, wt as envDefaults, xt as unwindFailure, y as SLICES, yt as uncoveredMessage, z as coverageMessage } from "../chunks/reserved-DF1HD6jL.mjs";
4
+ import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-D89fU_wf.mjs";
5
+ import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-Bv40kZu9.mjs";
6
6
  import { posix } from "node:path";
7
7
  //#region src/workflows/shared/fences.ts
8
8
  const markFenced = (text) => {
@@ -1103,17 +1103,20 @@ const buildFeature = async (firstBase) => {
1103
1103
  const handedOut = new Set(listedIds());
1104
1104
  const one = slices.length === 1;
1105
1105
  const built = (i) => sliceIds(sliceCriteria(slices, i))?.length !== 0;
1106
+ const reports = [];
1106
1107
  for (const i of slices.keys()) {
1107
1108
  if (!built(i)) continue;
1108
1109
  const n = one ? "" : `-${i + 1}`;
1109
1110
  await buildSlice(i === 0 ? firstBase : head(), {
1111
+ number: `${i + 1}`,
1110
1112
  tier,
1111
1113
  criteria: sliceCriteria(slices, i),
1112
1114
  refactor: `refactor${n}`,
1113
1115
  feature: `feature${n}`,
1114
1116
  more: () => slices.some((_, j) => j > i && built(j)),
1115
1117
  reviewBase: one ? void 0 : firstBase,
1116
- handedOut
1118
+ handedOut,
1119
+ laps: reports
1117
1120
  });
1118
1121
  }
1119
1122
  };
@@ -1145,38 +1148,39 @@ const buildSlice = async (sliceBase, build) => {
1145
1148
  const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? sliceBase : seam.base;
1146
1149
  if (more) {
1147
1150
  const guard = () => requireCriteriaFrozen(REQUIREMENTS);
1148
- await tail(settleQuality, {
1151
+ const report = await tail(async (_, report) => report, {
1149
1152
  green: true,
1150
1153
  lensBase: base,
1151
1154
  guard,
1152
1155
  guardRests: false
1153
1156
  });
1157
+ build.laps.push({
1158
+ slice: build.number,
1159
+ report
1160
+ });
1154
1161
  return;
1155
1162
  }
1156
1163
  const reviewBase = build.reviewBase ?? base;
1157
- await featureFeedback(await reviewedTail(reviewBase, base), base, {
1164
+ const outcome = await reviewedTail(reviewBase, base, {
1165
+ slice: build.number,
1166
+ laps: build.laps
1167
+ });
1168
+ await featureFeedback(outcome, base, {
1158
1169
  closed,
1159
1170
  used
1160
1171
  });
1161
1172
  });
1162
1173
  };
1163
- const settleQuality = async () => {
1164
- let rested = false;
1165
- while ((read(".gtd/QUALITY.md") ?? "").length > 0) {
1166
- rested = true;
1167
- await human("quality-unresolved", {
1168
- message: qualityUnresolvedMessage(),
1169
- label: "Quality findings left unresolved",
1170
- file: QUALITY
1171
- });
1172
- }
1173
- if (rested) await healthy(() => fixCheck());
1174
- };
1175
- /** The triaged quality lap, then the human review since `base`. */
1176
- const reviewedTail = (base, lensBase) => tail((escalations, report) => humanReview(base, escalations, { report }), {
1174
+ /** The quality lap of slice `last.slice`, then the human review since `base`, listing what earlier slices' `laps` left too. */
1175
+ const reviewedTail = (base, lensBase, last = {
1176
+ slice: "",
1177
+ laps: []
1178
+ }) => tail((escalations, report) => humanReview(base, escalations, { report: last.laps.length === 0 ? report : joinReports([...last.laps, {
1179
+ slice: last.slice,
1180
+ report
1181
+ }]) }), {
1177
1182
  green: true,
1178
- lensBase,
1179
- triage: true
1183
+ lensBase
1180
1184
  });
1181
1185
  const featureFeedback = async (first, lensBase, ids) => {
1182
1186
  let outcome = first;
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { g as reviewRisks, h as reviewNotes } from "../chunks/steering-D7hWeMgE.mjs";
3
- import { C as coder, J as fixReviewNotesPrompt, N as agentWithSkills, Tt as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, wt as defaults, y as REVIEW } from "../chunks/reserved-BENPy6IJ.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CDNMZ-2E.mjs";
3
+ import { Ct as defaults, K as finisherSystem, M as agentWithSkills, S as coder, c as baseline, i as skills, n as summary, q as fixReviewNotesPrompt, r as access, s as shipped, t as steering, v as REVIEW, wt as envDefaults } from "../chunks/reserved-DF1HD6jL.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-D89fU_wf.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,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, start } from "../flows.mjs";
2
- import { C as coder, N as agentWithSkills, Tt as envDefaults, i as skills, mt as reviewGateBlockedMessage, n as summary, pt as reviewFeedbackFixPrompt, q as finisherSystem, r as access, s as shipped, t as steering, u as gate, v as REQUIREMENTS, wt as defaults } from "../chunks/reserved-BENPy6IJ.mjs";
3
- import { a as refusePendingHandOff } from "../chunks/tail-CDNMZ-2E.mjs";
4
- import { i as reUnwind, t as buildTail } from "../chunks/review-DcbgUwr7.mjs";
2
+ import { Ct as defaults, K as finisherSystem, M as agentWithSkills, S as coder, _ as REQUIREMENTS, dt as reviewFeedbackFixPrompt, ft as reviewGateBlockedMessage, i as skills, l as gate, n as summary, r as access, s as shipped, t as steering, wt as envDefaults } from "../chunks/reserved-DF1HD6jL.mjs";
3
+ import { a as refusePendingHandOff } from "../chunks/tail-D89fU_wf.mjs";
4
+ import { i as reUnwind, t as buildTail } from "../chunks/review-Bv40kZu9.mjs";
5
5
  //#region src/workflows/review/review.ts
6
6
  const fixFeedback = async () => {
7
7
  await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { Bt as requireFlag, C as coder, It as flagMentions, Rt as flipProblems, Tt as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as defaults } from "../chunks/reserved-BENPy6IJ.mjs";
2
+ import { Ct as defaults, Ft as flagMentions, Lt as flipProblems, S as coder, f as FEEDBACK, i as skills, l as gate, n as summary, o as flagShipped, r as access, t as steering, wt as envDefaults, zt as requireFlag } from "../chunks/reserved-DF1HD6jL.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.";