@pmelab/gtd 55.0.0 → 57.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +72 -80
  3. package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-X2RkNHin.mjs} +110 -318
  4. package/dist/chunks/{review-BJGKwlYv.mjs → review-v7sh5GF4.mjs} +30 -125
  5. package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
  6. package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-S04KEewi.mjs} +144 -56
  7. package/dist/gtd.bundle.mjs +259 -22
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +6 -6
  10. package/dist/web/main.css +1 -1
  11. package/dist/web/main.js +551 -66
  12. package/dist/workflow-kit.mjs +1 -1
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +63 -175
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +3 -3
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +603 -727
  19. package/package.json +1 -1
  20. package/skills/authoring/SKILL.md +3 -3
  21. package/src/cli/Cli.ts +5 -5
  22. package/src/edge/Edge.test.ts +1 -1
  23. package/src/steering/FORMATS.md +5 -1
  24. package/src/steering/SteeringFormats.test.ts +2 -2
  25. package/src/steering/contract.test.ts +1 -0
  26. package/src/steering/findings.test.ts +47 -0
  27. package/src/steering/findings.ts +350 -0
  28. package/src/steering/fixtures/findings/gate.md +34 -0
  29. package/src/steering/fixtures/findings/gate.read.json +87 -0
  30. package/src/steering/index.test.ts +2 -2
  31. package/src/steering/index.ts +4 -3
  32. package/src/steering/review.test.ts +1 -75
  33. package/src/steering/review.ts +1 -74
  34. package/src/workflow/compile.test.ts +1 -0
  35. package/src/workflows/feature/criteria-coverage.feature +1 -7
  36. package/src/workflows/feature/criteria.test.ts +1 -41
  37. package/src/workflows/feature/criteria.ts +2 -14
  38. package/src/workflows/feature/fast-path.feature +0 -1
  39. package/src/workflows/feature/feature.test.ts +1 -1
  40. package/src/workflows/feature/feature.ts +16 -25
  41. package/src/workflows/feature/flag-mode.feature +1 -1
  42. package/src/workflows/feature/lens-models.feature +0 -1
  43. package/src/workflows/feature/lens-review.feature +323 -172
  44. package/src/workflows/feature/naive-test.feature +258 -0
  45. package/src/workflows/feature/one-step-land.feature +0 -1
  46. package/src/workflows/feature/refactor-absorbs.feature +1 -8
  47. package/src/workflows/feature/refactor-first.feature +2 -11
  48. package/src/workflows/feature/refactor-rebuild.feature +4 -25
  49. package/src/workflows/feature/refactor-review.feature +33 -118
  50. package/src/workflows/feature/refactor.test.ts +0 -2
  51. package/src/workflows/feature/refactor.ts +0 -1
  52. package/src/workflows/feature/review-gate.feature +7 -24
  53. package/src/workflows/feature/reviewer-walkthrough.feature +16 -104
  54. package/src/workflows/feature/seam-rejection.feature +0 -1
  55. package/src/workflows/feature/seam.feature +0 -1
  56. package/src/workflows/feature/seam.test.ts +1 -2
  57. package/src/workflows/feature/seam.ts +56 -10
  58. package/src/workflows/feature/standards.feature +75 -35
  59. package/src/workflows/fix/fix-lens-origin.feature +20 -7
  60. package/src/workflows/fix/fix-quality.feature +102 -23
  61. package/src/workflows/review/review-door.feature +62 -59
  62. package/src/workflows/shared/learn.test.ts +0 -14
  63. package/src/workflows/shared/learn.ts +1 -6
  64. package/src/workflows/shared/lenses.ts +149 -43
  65. package/src/workflows/shared/options.ts +9 -1
  66. package/src/workflows/shared/prose.ts +0 -6
  67. package/src/workflows/shared/review.test.ts +27 -26
  68. package/src/workflows/shared/review.ts +29 -196
  69. package/src/workflows/shared/skills.test.ts +0 -4
  70. package/src/workflows/shared/skills.ts +0 -5
  71. package/src/workflows/shared/steps.ts +1 -1
  72. package/src/workflows/shared/tail.ts +18 -9
  73. package/src/workflows/shared/text.test.ts +125 -329
  74. package/src/workflows/shared/text.ts +173 -471
  75. package/src/workflows/shared/vars.ts +2 -3
  76. package/src/workflows/feature/hands-on-skip.feature +0 -316
  77. package/src/workflows/feature/prototype-rounds.feature +0 -156
  78. package/src/workflows/feature/prototype.feature +0 -428
  79. package/src/workflows/feature/prototype.test.ts +0 -43
  80. package/src/workflows/feature/prototype.ts +0 -198
  81. package/src/workflows/feature/review-answers.feature +0 -324
  82. package/src/workflows/feature/severity-budget.feature +0 -116
  83. package/src/workflows/feature/severity-sorted-review.feature +0 -198
@@ -1,17 +1,24 @@
1
1
  import {
2
2
  answered,
3
3
  head,
4
+ human,
4
5
  judge,
5
6
  mark,
6
7
  numeric,
7
8
  read,
8
- removeScript,
9
+ refuse,
9
10
  run,
10
11
  sectionBodies,
11
12
  vars,
12
13
  writeScript,
13
14
  type JudgeQuestion,
14
15
  } from "../../flows/index.js"
16
+ import {
17
+ findingEntries,
18
+ formatFinding,
19
+ steeringFormatFor,
20
+ type FindingEntry,
21
+ } from "../../steering/index.js"
15
22
  import { FIX_CAP, guarded, healthy, type EscalationCount, type Guard } from "./health.js"
16
23
  import { fixCheck, fixQuality, QUALITY } from "./steps.js"
17
24
  import * as t from "./text.js"
@@ -27,19 +34,27 @@ export interface Finding {
27
34
 
28
35
  type FindingKind = "mechanical" | "noise" | "design"
29
36
 
30
- /** What the triaged quality lap did with its findings, for the review to list. */
37
+ /** What the triaged quality lap and its finding gate did with the findings, for the review to list. */
31
38
  export interface LensReport {
32
- /** Design findings, then the mechanical ones the fix cap left. */
33
- readonly notes: readonly (Finding &
34
- Origin & {
35
- readonly unresolved?: true
36
- readonly unclear?: true
37
- })[]
39
+ /** Fixed by the fix turns the judge sent them to, with the fixing commit. */
38
40
  readonly fixed: readonly (Finding & Origin & { readonly commit: string })[]
39
- readonly noise: readonly (Finding & Origin & { readonly p: number })[]
41
+ /** Ticked at the finding gate and fixed, with the fixing commit. */
42
+ readonly gateFixed: readonly (Finding & Origin & { readonly commit: string })[]
43
+ /** Left unticked at the finding gate. */
44
+ readonly dropped: readonly (Finding & Origin)[]
45
+ /** Called noise by the judge and left unticked at the finding gate; `p` absent on one the human added there. */
46
+ readonly noise: readonly (Finding & Origin & { readonly p?: number })[]
47
+ /** Ticked at the finding gate, still open after the fix turns. */
48
+ readonly unresolved: readonly (Finding & Origin & { readonly unclear?: true })[]
40
49
  }
41
50
 
42
- export const NO_FINDINGS: LensReport = { notes: [], fixed: [], noise: [] }
51
+ export const NO_FINDINGS: LensReport = {
52
+ fixed: [],
53
+ gateFixed: [],
54
+ dropped: [],
55
+ noise: [],
56
+ unresolved: [],
57
+ }
43
58
 
44
59
  const UNTITLED = "Quality findings"
45
60
 
@@ -138,14 +153,117 @@ export interface TriageOptions {
138
153
  readonly lensOf?: ((f: Finding) => string | undefined) | undefined
139
154
  }
140
155
 
156
+ type Note = Finding & Origin & { readonly unresolved?: true; readonly unclear?: true }
157
+
158
+ interface Fixing {
159
+ readonly fixed: readonly (Finding & Origin & { readonly commit: string })[]
160
+ readonly open: readonly (Finding & Origin)[]
161
+ readonly unclear: readonly (Finding & Origin)[]
162
+ }
163
+
164
+ const NOTHING_FIXED: Fixing = { fixed: [], open: [], unclear: [] }
165
+
166
+ /** Up to `FIX_CAP` fix turns over `.gtd/QUALITY.md`, which holds one chunk per finding in `findings`. */
167
+ const fixTurns = async (
168
+ findings: readonly (Finding & Origin)[],
169
+ options: TriageOptions,
170
+ ): Promise<Fixing> => {
171
+ const fixed: (Finding & Origin & { commit: string })[] = []
172
+ const unclear: (Finding & Origin)[] = []
173
+ let open: readonly (Finding & Origin)[] = findings
174
+ for (let turns = 0; turns < FIX_CAP && open.length > 0; turns++) {
175
+ await guarded(() => fixQuality(options.rules), options.guard)()
176
+ const turn = afterFix(open, findingsOf(read(QUALITY) ?? ""))
177
+ const commit = head()
178
+ fixed.push(...turn.fixed.map((f) => ({ ...f, commit })))
179
+ unclear.push(...turn.unclear)
180
+ // A turn that renamed a chunk hands back fresh parses: keep the lens a same-titled finding had.
181
+ open = turn.open.map((f) => ({ ...findings.find((m) => m.title === f.title), ...f }))
182
+ }
183
+ const { escalations, guard, guardRests, rules } = options
184
+ await healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules })
185
+ return { fixed, open, unclear }
186
+ }
187
+
188
+ /** Matches each entry the human left to the finding it was written from, by title, first unused first; `left` is what no entry claimed. */
189
+ const matcher = <F extends Finding>(findings: readonly F[]) => {
190
+ const left = [...findings]
191
+ const of = (e: FindingEntry): F | undefined => {
192
+ const at = left.findIndex((f) => f.title === e.title)
193
+ return at < 0 ? undefined : left.splice(at, 1)[0]
194
+ }
195
+ return { of, left }
196
+ }
197
+
198
+ const gateProblems = (text: string): readonly string[] =>
199
+ steeringFormatFor("findings")!
200
+ .validate(text)
201
+ .map((f) => ` - ${formatFinding(QUALITY, f)}`)
202
+
141
203
  /**
142
- * Judge each finding in `.gtd/QUALITY.md`, set the noise and design ones
143
- * aside, and fix the mechanical ones for up to `FIX_CAP` turns. Leaves
144
- * `.gtd/QUALITY.md` holding only what the fixer left: the caller must remove
145
- * it, or the next lap appends to stale findings.
204
+ * The human gate over the open notes and the noise: a ticked finding is
205
+ * fixed — never re-planned — and the rest dropped. No notes and no noise, no gate.
206
+ */
207
+ const findingGate = async (
208
+ notes: readonly Note[],
209
+ noise: readonly (Finding & Origin & { readonly p: number })[],
210
+ options: TriageOptions,
211
+ ): Promise<Omit<LensReport, "fixed">> => {
212
+ if (notes.length + noise.length === 0) return NO_FINDINGS
213
+ const written = t.findingGateFile(notes, noise)
214
+ await run("quality.gating", writeScript(QUALITY, written), {
215
+ label: "Listing the findings for you",
216
+ })
217
+ await human("quality.gate", {
218
+ message: t.findingGateMessage(),
219
+ label: "Deciding on the quality findings",
220
+ file: QUALITY,
221
+ mode: "findings",
222
+ acceptClean: true,
223
+ })
224
+ // A deleted file decides nothing: every finding is dropped, as if untouched.
225
+ const answered = read(QUALITY) ?? written
226
+ const problems = gateProblems(answered)
227
+ if (problems.length > 0)
228
+ refuse(
229
+ `gtd land: finding-gate: ${problems.length} problem(s) in ${QUALITY} — keep one \`- [ ] fix\` box per finding, ticked to fix it or unticked to drop it, and write guidance only under a ticked one:\n${problems.join("\n")}`,
230
+ )
231
+ const noteOf = matcher(notes)
232
+ const noiseOf = matcher(noise)
233
+ // An entry the human added is a finding of its own.
234
+ const decided = findingEntries(answered).map((e) => ({
235
+ e,
236
+ finding: (e.noise ? noiseOf.of(e) : noteOf.of(e)) ?? { id: "", title: e.title, body: e.body },
237
+ }))
238
+ // The finding as gtd wrote it: a human steers with guidance, not by editing its text.
239
+ const ticked = decided
240
+ .filter(({ e }) => e.fix)
241
+ .map(({ e, finding }) => ({ ...finding, body: t.withGuidance(finding.body, e.guidance) }))
242
+ let fixing = NOTHING_FIXED
243
+ if (ticked.length > 0) {
244
+ await run("quality.picking", writeScript(QUALITY, t.qualityFindingsFile(ticked)), {
245
+ label: "Keeping the findings you ticked",
246
+ })
247
+ fixing = await fixTurns(ticked, options)
248
+ }
249
+ const unticked = (noisy: boolean) =>
250
+ decided.filter(({ e }) => !e.fix && e.noise === noisy).map(({ finding }) => finding)
251
+ // A finding whose entry the human deleted is dropped too.
252
+ return {
253
+ gateFixed: fixing.fixed,
254
+ dropped: [...unticked(false), ...noteOf.left],
255
+ noise: [...unticked(true), ...noiseOf.left],
256
+ unresolved: [...fixing.open, ...fixing.unclear.map((f) => ({ ...f, unclear: true as const }))],
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Judge each finding in `.gtd/QUALITY.md` and fix the mechanical ones for up
262
+ * to `FIX_CAP` turns; the rest — design, unresolved and noise — go to the
263
+ * finding gate. Leaves `.gtd/QUALITY.md` holding only what the last fixer
264
+ * left: the caller must remove it, or the next lap appends to stale findings.
146
265
  */
147
266
  export const triageFindings = async (options: TriageOptions): Promise<LensReport> => {
148
- const { escalations, guard, guardRests, rules } = options
149
267
  const withLens = (f: Finding): Finding & Origin => {
150
268
  const lens = options.lensOf?.(f)
151
269
  return lens === undefined ? f : { ...f, lens }
@@ -153,36 +271,24 @@ export const triageFindings = async (options: TriageOptions): Promise<LensReport
153
271
  const findings = findingsOf(read(QUALITY) ?? "").map(withLens)
154
272
  if (findings.length === 0) return NO_FINDINGS
155
273
  const { mechanical, noise, design } = await classify(findings)
156
- // Rewritten whenever it is not one chunk per mechanical finding: the fixer deletes chunks.
157
- const sorted = t.qualityFindingsFile(mechanical)
158
- if (read(QUALITY) !== sorted) {
159
- await run(
160
- "quality.sorting",
161
- mechanical.length === 0 ? removeScript([QUALITY]) : writeScript(QUALITY, sorted),
162
- { label: "Setting findings aside for the review" },
163
- )
274
+ let auto = NOTHING_FIXED
275
+ if (mechanical.length > 0) {
276
+ // Rewritten whenever it is not one chunk per mechanical finding: the fixer deletes chunks.
277
+ const sorted = t.qualityFindingsFile(mechanical)
278
+ if (read(QUALITY) !== sorted)
279
+ await run("quality.sorting", writeScript(QUALITY, sorted), {
280
+ label: "Setting findings aside for the finding gate",
281
+ })
282
+ auto = await fixTurns(mechanical, options)
164
283
  }
165
- const fixed: (Finding & { commit: string })[] = []
166
- const unclear: Finding[] = []
167
- let open: readonly Finding[] = mechanical
168
- for (let turns = 0; turns < FIX_CAP && open.length > 0; turns++) {
169
- await guarded(() => fixQuality(rules), guard)()
170
- const turn = afterFix(open, findingsOf(read(QUALITY) ?? ""))
171
- const commit = head()
172
- fixed.push(...turn.fixed.map((f) => ({ ...f, commit })))
173
- unclear.push(...turn.unclear)
174
- // A turn that renamed a chunk hands back fresh parses: keep the lens a same-titled finding had.
175
- open = turn.open.map((f) => ({ ...mechanical.find((m) => m.title === f.title), ...f }))
176
- }
177
- if (mechanical.length > 0)
178
- await healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules })
179
- return {
180
- notes: [
284
+ const gated = await findingGate(
285
+ [
181
286
  ...design,
182
- ...open.map((f) => ({ ...f, unresolved: true as const })),
183
- ...unclear.map((f) => ({ ...f, unclear: true as const })),
287
+ ...auto.open.map((f) => ({ ...f, unresolved: true as const })),
288
+ ...auto.unclear.map((f) => ({ ...f, unclear: true as const })),
184
289
  ],
185
- fixed,
186
290
  noise,
187
- }
291
+ options,
292
+ )
293
+ return { ...gated, fixed: auto.fixed }
188
294
  }
@@ -24,11 +24,11 @@ export const ShippedOptions = Schema.Struct({
24
24
  seamAllow: setting,
25
25
  qualityReviews: setting,
26
26
  refactorQualityReviews: setting,
27
- prototypeRounds: setting,
28
27
  testCommand: setting,
29
28
  plannerModel: setting,
30
29
  coderModel: setting,
31
30
  flagMode: Schema.optionalWith(Schema.Boolean, { exact: true }),
31
+ naiveTest: Schema.optionalWith(Schema.Boolean, { exact: true }),
32
32
  })
33
33
 
34
34
  export type ShippedOptions = Schema.Schema.Encoded<typeof ShippedOptions>
@@ -53,6 +53,14 @@ export const flagModeOn = (): boolean => {
53
53
  return value === "true"
54
54
  }
55
55
 
56
+ /** The `naiveTest` setting, pinned at process start. Anything but true/false fails rather than guess. */
57
+ export const naiveTestOn = (): boolean => {
58
+ const value = vars.naiveTest ?? defaults.naiveTest!
59
+ if (value !== "true" && value !== "false")
60
+ refuse(`gtd: naiveTest must be "true" or "false", got ${JSON.stringify(value)}`)
61
+ return value === "true"
62
+ }
63
+
56
64
  /** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
57
65
  export const shipped = (
58
66
  flow: Flow,
@@ -88,12 +88,6 @@ things when redirected — a failing check, or reviewer feedback. Stay
88
88
  strictly inside that task; never touch code outside it. Treat feedback as the
89
89
  diagnosis to act on; discard it, with reason, only when simply wrong.`
90
90
 
91
- export const testerPersona = `You are the hands-on tester in gtd's build pipeline: use what was just
92
- built the way a person would — run the real program, open the real
93
- page, call the real endpoint — and report exactly what you did and
94
- what you saw. Never fix anything yourself, and never pass a criterion
95
- you did not watch hold.`
96
-
97
91
  export const finisherPersona = `You are the closing identity in gtd's build pipeline — the last coder
98
92
  before it closes. Fix a late-breaking failing check after sign-off,
99
93
  focused and minimal. Every turn lands its own commit — nothing here
@@ -47,7 +47,7 @@ interface Drive {
47
47
  readonly vars?: Readonly<Record<string, string>>
48
48
  /** The REVIEW.md the human leaves; defaults to `notes` laid over the baseline. */
49
49
  readonly after?: string
50
- /** The REVIEW.md each `review.composing` turn writes, in order; also raises the review-turn stop to one past the last. */
50
+ /** The REVIEW.md each `review.reviewing` turn writes, in order; also raises the review-turn stop to one past the last. */
51
51
  readonly reviewDocs?: readonly (string | undefined)[]
52
52
  /** Let the run pass the second gate, stopping at the first range read after it. */
53
53
  readonly passSecondGate?: boolean
@@ -94,7 +94,7 @@ const drive = async (d: Drive): Promise<Run> => {
94
94
  let awaits = 0
95
95
  let reviews = 0
96
96
  const effects: Record<string, () => void> = {
97
- "review.composing": () => {
97
+ "review.reviewing": () => {
98
98
  const docs = d.reviewDocs
99
99
  if (++reviews > (docs?.length ?? 0) + (docs === undefined ? 1 : 0)) throw new Stop()
100
100
  const written = docs?.[reviews - 1]
@@ -184,10 +184,10 @@ describe("the risk-fix pass", () => {
184
184
  it("a marked risk is fixed, kept green, re-reviewed, then rests at the gate", async () => {
185
185
  const run = await drive({ notes: [], reviewDocs: [risky, clean] })
186
186
  expect(run.log.slice(0, 5)).toEqual([
187
- "review.composing",
187
+ "review.reviewing",
188
188
  "review.fix.risks.fixing",
189
189
  "health.check",
190
- "review.composing",
190
+ "review.reviewing",
191
191
  "review.await-review",
192
192
  ])
193
193
  expect(run.prompts.get("review.fix.risks.fixing")).toContain("risk-1")
@@ -199,10 +199,10 @@ describe("the risk-fix pass", () => {
199
199
  const run = await drive({ notes: [], reviewDocs: [risky, risky] })
200
200
  expect(run.log.filter((n) => n === "review.fix.risks.fixing")).toHaveLength(1)
201
201
  expect(run.log.slice(0, 5)).toEqual([
202
- "review.composing",
202
+ "review.reviewing",
203
203
  "review.fix.risks.fixing",
204
204
  "health.check",
205
- "review.composing",
205
+ "review.reviewing",
206
206
  "review.await-review",
207
207
  ])
208
208
  })
@@ -232,7 +232,7 @@ describe("the risk-fix pass", () => {
232
232
  it("no marker means no fix-risks step", async () => {
233
233
  const run = await drive({ notes: [], reviewDocs: [clean] })
234
234
  expect(run.log).not.toContain("review.fix.risks.fixing")
235
- expect(run.log[0]).toBe("review.composing")
235
+ expect(run.log[0]).toBe("review.reviewing")
236
236
  expect(run.log[1]).toBe("review.await-review")
237
237
  })
238
238
 
@@ -243,16 +243,16 @@ describe("the risk-fix pass", () => {
243
243
  reviewDocs: [undefined, risky, clean],
244
244
  })
245
245
  expect(run.log).toEqual([
246
- "review.composing",
246
+ "review.reviewing",
247
247
  "review.await-review",
248
248
  "review.triage",
249
249
  "review.fix.nits.fixing",
250
250
  "health.check",
251
251
  "review.closing",
252
- "review.composing",
252
+ "review.reviewing",
253
253
  "review.fix.risks.fixing",
254
254
  "health.check",
255
- "review.composing",
255
+ "review.reviewing",
256
256
  "review.await-review",
257
257
  ])
258
258
  })
@@ -264,8 +264,9 @@ describe("the guard", () => {
264
264
 
265
265
  it("runs right after the risk-fix turn", async () => {
266
266
  const run = await drive({ notes: [], reviewDocs: [risky, clean], guarded: true })
267
- expect(run.log.slice(0, 4)).toEqual([
268
- "review.composing",
267
+ expect(run.log.slice(0, 5)).toEqual([
268
+ "review.reviewing",
269
+ "guard",
269
270
  "review.fix.risks.fixing",
270
271
  "guard",
271
272
  "health.check",
@@ -278,7 +279,7 @@ describe("the guard", () => {
278
279
  answers: { "note-1": verdict("nit") },
279
280
  guarded: true,
280
281
  })
281
- expect(run.log.slice(2, 6)).toEqual([
282
+ expect(run.log.slice(3, 7)).toEqual([
282
283
  "review.triage",
283
284
  "review.fix.nits.fixing",
284
285
  "guard",
@@ -443,7 +444,7 @@ describe("review verdict routing", () => {
443
444
  it("an unanswered note counts as edit", async () => {
444
445
  const run = await drive({ notes: ["add — typo", "sub", "mul"], answers: {} })
445
446
  expect(run.log).toEqual([
446
- "review.composing",
447
+ "review.reviewing",
447
448
  "review.await-review",
448
449
  "review.triage",
449
450
  "review.closing",
@@ -478,7 +479,7 @@ describe("review verdict routing", () => {
478
479
  it("only praise closes and signs off with no agent turn", async () => {
479
480
  const run = await drive({ notes, answers: { "note-1": verdict("praise") } })
480
481
  expect(run.log).toEqual([
481
- "review.composing",
482
+ "review.reviewing",
482
483
  "review.await-review",
483
484
  "review.triage",
484
485
  "review.closing",
@@ -492,7 +493,7 @@ describe("review verdict routing", () => {
492
493
  answers: { "note-1": verdict("question"), "note-2": verdict("question") },
493
494
  })
494
495
  expect(run.log).toEqual([
495
- "review.composing",
496
+ "review.reviewing",
496
497
  "review.await-review",
497
498
  "review.triage",
498
499
  "review.answer-review-questions",
@@ -508,17 +509,17 @@ describe("review verdict routing", () => {
508
509
  answers: { "note-1": verdict("question"), "note-2": verdict("nit") },
509
510
  })
510
511
  expect(run.log).toEqual([
511
- "review.composing",
512
+ "review.reviewing",
512
513
  "review.await-review",
513
514
  "review.triage",
514
515
  "review.answer-review-questions",
515
516
  "review.fix.nits.fixing",
516
517
  "health.check",
517
518
  "review.closing",
518
- "review.composing",
519
+ "review.reviewing",
519
520
  ])
520
521
  expect(run.prompts.get("review.fix.nits.fixing")).not.toContain("why?")
521
- expect(run.prompts.get("review.composing")).toContain("git show 'c4:.gtd/REVIEW.md'")
522
+ expect(run.prompts.get("review.reviewing")).toContain("Run: git show c4")
522
523
  expect(run.result).toBe("stopped")
523
524
  })
524
525
 
@@ -527,8 +528,8 @@ describe("review verdict routing", () => {
527
528
  notes: ["add — typo", "sub", "mul"],
528
529
  answers: { "note-1": verdict("nit") },
529
530
  })
530
- expect(run.prompts.get("review.composing")).not.toContain("git show")
531
- expect(run.prompts.get("review.composing")).toContain("## Fixed from your notes")
531
+ expect(run.prompts.get("review.reviewing")).not.toContain("git show")
532
+ expect(run.prompts.get("review.reviewing")).toContain("worked on the human's nits")
532
533
  })
533
534
 
534
535
  it("edits run after questions and nits, and the capture names only the edits and the answering commit", async () => {
@@ -537,7 +538,7 @@ describe("review verdict routing", () => {
537
538
  answers: { "note-2": verdict("question"), "note-3": verdict("nit") },
538
539
  })
539
540
  expect(run.log).toEqual([
540
- "review.composing",
541
+ "review.reviewing",
541
542
  "review.await-review",
542
543
  "review.triage",
543
544
  "review.answer-review-questions",
@@ -557,7 +558,7 @@ describe("review verdict routing", () => {
557
558
  it("a noted round with no extractable note collects as one edit, without judging", async () => {
558
559
  const run = await drive({ notes: [], after: BEFORE + "\nSome stray prose.\n" })
559
560
  expect(run.log).toEqual([
560
- "review.composing",
561
+ "review.reviewing",
561
562
  "review.await-review",
562
563
  "review.closing",
563
564
  "review.collecting",
@@ -682,7 +683,7 @@ describe("severity routing", () => {
682
683
  answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
683
684
  })
684
685
  expect(run.log).toEqual([
685
- "review.composing",
686
+ "review.reviewing",
686
687
  "review.await-review",
687
688
  "review.triage",
688
689
  "review.closing",
@@ -762,7 +763,7 @@ describe("BuildTailOptions.lensBase", () => {
762
763
  if (request.kind === "restart") return Promise.resolve()
763
764
  if (request.kind === "agent" && request.name.startsWith("quality."))
764
765
  prompts.push(request.prompt)
765
- if (request.name === "review.composing") throw new Stop()
766
+ if (request.name === "review.reviewing") throw new Stop()
766
767
  return Promise.resolve()
767
768
  },
768
769
  pushScope: () => undefined,
@@ -1000,7 +1001,7 @@ describe("buildTail without rules", () => {
1000
1001
  if (request.kind === "agent") prompts.set(request.name, request.prompt)
1001
1002
  if (request.name === "health.check") checks++
1002
1003
  if (request.name === "fix.quality.fixing") files.delete(".gtd/QUALITY.md")
1003
- if (request.name === "review.composing" || request.name === "health.stop")
1004
+ if (request.name === "review.reviewing" || request.name === "health.stop")
1004
1005
  throw new Stop()
1005
1006
  return Promise.resolve(
1006
1007
  request.kind === "judge"