@pmelab/gtd 58.2.0 → 59.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 (133) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +115 -390
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
  4. package/dist/gtd.bundle.mjs +76 -59
  5. package/dist/testkit/gherkin.mjs +1 -1
  6. package/dist/testkit.mjs +1 -1
  7. package/package.json +3 -42
  8. package/skills/authoring/SKILL.md +31 -25
  9. package/src/cli/Cli.test.ts +15 -19
  10. package/src/cli/Cli.ts +2 -4
  11. package/src/cli/Program.test.ts +60 -29
  12. package/src/cli/Program.ts +5 -7
  13. package/src/edge/Edge.ts +3 -3
  14. package/src/flags/index.test.ts +0 -13
  15. package/src/flags/index.ts +1 -3
  16. package/src/install/Install.test.ts +14 -11
  17. package/src/install/Install.ts +38 -15
  18. package/src/install/index.ts +1 -2
  19. package/src/replay/Replay.test.ts +1 -1
  20. package/src/replay/index.ts +1 -1
  21. package/src/step/planStart.ts +6 -1
  22. package/src/twins/FlowScripts.ts +1 -1
  23. package/src/workflow/index.ts +1 -0
  24. package/src/workflow/load.test.ts +138 -113
  25. package/src/workflow/load.ts +1 -1
  26. package/src/workflow/vars.test.ts +30 -3
  27. package/src/workflow/vars.ts +34 -2
  28. package/src/workflow-kit/index.ts +1 -1
  29. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  30. package/dist/chunks/review-Bfaror38.mjs +0 -427
  31. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  32. package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
  33. package/dist/workflows/cleanup.mjs +0 -36
  34. package/dist/workflows/feature.mjs +0 -1018
  35. package/dist/workflows/fix.mjs +0 -41
  36. package/dist/workflows/review.mjs +0 -45
  37. package/dist/workflows/rollout.mjs +0 -48
  38. package/dist/workflows.mjs +0 -40651
  39. package/src/install/ConfigScaffold.ts +0 -22
  40. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  41. package/src/workflows/cleanup/cleanup.feature +0 -315
  42. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  43. package/src/workflows/cleanup/cleanup.ts +0 -52
  44. package/src/workflows/cleanup/index.ts +0 -6
  45. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  46. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  47. package/src/workflows/feature/criteria-coverage.feature +0 -102
  48. package/src/workflows/feature/criteria.test.ts +0 -147
  49. package/src/workflows/feature/criteria.ts +0 -267
  50. package/src/workflows/feature/fast-path.feature +0 -93
  51. package/src/workflows/feature/feature.test.ts +0 -338
  52. package/src/workflows/feature/feature.ts +0 -156
  53. package/src/workflows/feature/green.ts +0 -19
  54. package/src/workflows/feature/index.ts +0 -18
  55. package/src/workflows/feature/lens-models.feature +0 -69
  56. package/src/workflows/feature/lens-review.feature +0 -566
  57. package/src/workflows/feature/naive-acceptance.feature +0 -147
  58. package/src/workflows/feature/naive-test.feature +0 -263
  59. package/src/workflows/feature/one-step-land.feature +0 -93
  60. package/src/workflows/feature/planning.ts +0 -41
  61. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  62. package/src/workflows/feature/refactor-first.feature +0 -158
  63. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  64. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  65. package/src/workflows/feature/refactor-review.feature +0 -1528
  66. package/src/workflows/feature/refactor.test.ts +0 -1220
  67. package/src/workflows/feature/refactor.ts +0 -368
  68. package/src/workflows/feature/release-flag.feature +0 -167
  69. package/src/workflows/feature/release-flag.test.ts +0 -49
  70. package/src/workflows/feature/release-flag.ts +0 -46
  71. package/src/workflows/feature/review-gate.feature +0 -241
  72. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  73. package/src/workflows/feature/risk-tier.feature +0 -351
  74. package/src/workflows/feature/seam-rejection.feature +0 -146
  75. package/src/workflows/feature/seam.feature +0 -297
  76. package/src/workflows/feature/seam.test.ts +0 -527
  77. package/src/workflows/feature/seam.ts +0 -426
  78. package/src/workflows/feature/standards.feature +0 -354
  79. package/src/workflows/feature/steps.test.ts +0 -65
  80. package/src/workflows/feature/steps.ts +0 -45
  81. package/src/workflows/feature/tier-pin.feature +0 -66
  82. package/src/workflows/feature/tier.test.ts +0 -166
  83. package/src/workflows/feature/tier.ts +0 -56
  84. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  85. package/src/workflows/fix/fix-quality.feature +0 -212
  86. package/src/workflows/fix/fix.feature +0 -79
  87. package/src/workflows/fix/fix.test.ts +0 -74
  88. package/src/workflows/fix/fix.ts +0 -52
  89. package/src/workflows/fix/index.ts +0 -6
  90. package/src/workflows/index.ts +0 -47
  91. package/src/workflows/review/index.ts +0 -6
  92. package/src/workflows/review/review-door.feature +0 -199
  93. package/src/workflows/review/review.test.ts +0 -27
  94. package/src/workflows/review/review.ts +0 -60
  95. package/src/workflows/rollout/index.ts +0 -6
  96. package/src/workflows/rollout/rollout.feature +0 -510
  97. package/src/workflows/rollout/rollout.test.ts +0 -263
  98. package/src/workflows/rollout/rollout.ts +0 -82
  99. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  100. package/src/workflows/shared/access.test.ts +0 -62
  101. package/src/workflows/shared/access.ts +0 -24
  102. package/src/workflows/shared/config.fixture.ts +0 -12
  103. package/src/workflows/shared/fences.ts +0 -24
  104. package/src/workflows/shared/health.test.ts +0 -148
  105. package/src/workflows/shared/health.ts +0 -191
  106. package/src/workflows/shared/learn.test.ts +0 -140
  107. package/src/workflows/shared/learn.ts +0 -201
  108. package/src/workflows/shared/lenses.test.ts +0 -39
  109. package/src/workflows/shared/lenses.ts +0 -294
  110. package/src/workflows/shared/options.test.ts +0 -157
  111. package/src/workflows/shared/options.ts +0 -92
  112. package/src/workflows/shared/progress.feature +0 -124
  113. package/src/workflows/shared/prose.ts +0 -263
  114. package/src/workflows/shared/recheck.feature +0 -60
  115. package/src/workflows/shared/registry.test.ts +0 -345
  116. package/src/workflows/shared/registry.ts +0 -183
  117. package/src/workflows/shared/reserved.ts +0 -11
  118. package/src/workflows/shared/review.test.ts +0 -1062
  119. package/src/workflows/shared/review.ts +0 -446
  120. package/src/workflows/shared/skills.test.ts +0 -121
  121. package/src/workflows/shared/skills.ts +0 -49
  122. package/src/workflows/shared/standards.test.ts +0 -90
  123. package/src/workflows/shared/standards.ts +0 -156
  124. package/src/workflows/shared/steps.test.ts +0 -219
  125. package/src/workflows/shared/steps.ts +0 -201
  126. package/src/workflows/shared/tail.test.ts +0 -454
  127. package/src/workflows/shared/tail.ts +0 -264
  128. package/src/workflows/shared/text.fixture.ts +0 -128
  129. package/src/workflows/shared/text.test.ts +0 -1430
  130. package/src/workflows/shared/text.ts +0 -1700
  131. package/src/workflows/shared/threads.test.ts +0 -129
  132. package/src/workflows/shared/vars.ts +0 -47
  133. package/src/workflows/shared/workflows.steps.ts +0 -54
@@ -1,1062 +0,0 @@
1
- import { afterEach, describe, expect, it } from "vitest"
2
- import {
3
- installContext,
4
- type Change,
5
- type JudgeAnswer,
6
- type JudgeQuestion,
7
- type ScopeOptions,
8
- type StepRequest,
9
- } from "../../flows/index.js"
10
- import {
11
- buildTail,
12
- humanReview,
13
- reUnwind,
14
- severityOf,
15
- type BuildTailOptions,
16
- type ReviewOutcome,
17
- } from "./review.js"
18
- import type { ReviewNote } from "../../steering/index.js"
19
- import * as bundled from "../index.js"
20
- import { fixtureContext } from "./text.fixture.js"
21
- import { toRequest } from "../../judges/index.js"
22
-
23
- afterEach(() => installContext(undefined))
24
-
25
- const REVIEW = ".gtd/REVIEW.md"
26
- const REQUIREMENTS = ".gtd/REQUIREMENTS.md"
27
-
28
- class Stop extends Error {}
29
-
30
- const doc = (notes: readonly string[]): string =>
31
- [
32
- "# Review: abc1234",
33
- "",
34
- "<!-- base: 0000000000000000000000000000000000000000 -->",
35
- "",
36
- "## calc",
37
- ...notes.flatMap((note, i) => [`- [ ] ./src/calc.ts#${i + 1}-${i + 1}`, note]),
38
- "",
39
- ].join("\n")
40
-
41
- const BEFORE = doc(["add", "sub", "mul"])
42
-
43
- interface Drive {
44
- readonly notes: readonly string[]
45
- readonly answers?: Readonly<Record<string, JudgeAnswer>>
46
- readonly truncated?: readonly string[]
47
- readonly vars?: Readonly<Record<string, string>>
48
- /** The REVIEW.md the human leaves; defaults to `notes` laid over the baseline. */
49
- readonly after?: string
50
- /** The REVIEW.md each `review.reviewing` turn writes, in order; also raises the review-turn stop to one past the last. */
51
- readonly reviewDocs?: readonly (string | undefined)[]
52
- /** Let the run pass the second gate, stopping at the first range read after it. */
53
- readonly passSecondGate?: boolean
54
- /** Options as `buildTail` passes them; `guard` is logged as `guard` when it runs. */
55
- readonly options?: Omit<BuildTailOptions, "guard">
56
- /** Install a guard that logs `guard` into the step log. */
57
- readonly guarded?: boolean
58
- /** The pending changes `changes()` reports at the collecting turn. */
59
- readonly pending?: readonly Change[]
60
- /** Files the harness holds from the start. */
61
- readonly files?: Readonly<Record<string, string>>
62
- }
63
-
64
- interface Run {
65
- readonly log: string[]
66
- readonly prompts: Map<string, string>
67
- readonly result: ReviewOutcome | "stopped"
68
- readonly judged: readonly string[]
69
- readonly asked: readonly JudgeQuestion[]
70
- /** `acceptClean` of each `review.await-review` gate, in order. */
71
- readonly gates: readonly boolean[]
72
- readonly gateMessages: readonly string[]
73
- /** Every `changesSince` argument, in order. */
74
- readonly since: readonly string[]
75
- /** Every scope opened, in order. */
76
- readonly scopes: readonly ScopeOptions[]
77
- }
78
-
79
- /** Run `review` against a scripted context; a second pass at the gate or a fresh review stops the run. */
80
- const drive = async (d: Drive): Promise<Run> => {
81
- const log: string[] = []
82
- const prompts = new Map<string, string>()
83
- const asked: JudgeQuestion[] = []
84
- const gates: boolean[] = []
85
- const gateMessages: string[] = []
86
- const since: string[] = []
87
- const scopes: ScopeOptions[] = []
88
- const files = new Map<string, string>([
89
- [REVIEW, d.after ?? doc(d.notes)],
90
- ...Object.entries(d.files ?? {}),
91
- ])
92
- let steps = 0
93
- let nitsFixed = false
94
- let awaits = 0
95
- let reviews = 0
96
- const effects: Record<string, () => void> = {
97
- "review.reviewing": () => {
98
- const docs = d.reviewDocs
99
- if (++reviews > (docs?.length ?? 0) + (docs === undefined ? 1 : 0)) throw new Stop()
100
- const written = docs?.[reviews - 1]
101
- if (written !== undefined) files.set(REVIEW, written)
102
- },
103
- "review.await-review": () => {
104
- if (++awaits > (d.passSecondGate ? 2 : 1)) throw new Stop()
105
- },
106
- "review.closing": () => void files.delete(REVIEW),
107
- "review.collecting": () => void files.set(REQUIREMENTS, "## Concern"),
108
- "review.fix.nits.fixing": () => {
109
- nitsFixed = true
110
- },
111
- }
112
- const record = (request: StepRequest): void => {
113
- if (request.kind === "human" && request.name === "review.await-review") {
114
- gates.push(request.options.acceptClean === true)
115
- gateMessages.push(request.options.message ?? "")
116
- }
117
- if (request.kind === "agent") prompts.set(request.name, request.prompt)
118
- if (request.kind === "run") prompts.set(request.name, request.body)
119
- if (request.kind === "judge") asked.push(...request.questions)
120
- }
121
- const context = fixtureContext(
122
- { vars: d.vars ?? {} },
123
- {
124
- step: (request: StepRequest) => {
125
- steps++
126
- if (request.kind === "restart") return Promise.resolve()
127
- log.push(request.name)
128
- record(request)
129
- effects[request.name]?.()
130
- if (request.kind !== "judge") return Promise.resolve()
131
- return Promise.resolve({ answers: d.answers ?? {}, truncated: d.truncated ?? [] })
132
- },
133
- refuse: (message: string): never => {
134
- throw new Error(message)
135
- },
136
- pushScope: (scope) => void scopes.push(scope),
137
- popScope: () => undefined,
138
- read: (path) => files.get(path),
139
- changes: (): readonly Change[] => d.pending ?? [],
140
- changesSince: (from: string): readonly Change[] => {
141
- since.push(from)
142
- if (d.passSecondGate && awaits === 2) throw new Stop()
143
- return [
144
- { path: REVIEW, status: "modified", before: BEFORE, after: files.get(REVIEW) },
145
- ...(nitsFixed
146
- ? [{ path: "src/calc.ts", status: "modified" as const, before: "a", after: "b" }]
147
- : []),
148
- ]
149
- },
150
- head: () => `c${steps}`,
151
- start: () => "c0",
152
- },
153
- )
154
- installContext(context)
155
- let result: ReviewOutcome | "stopped"
156
- try {
157
- result = await humanReview("base", undefined, {
158
- ...d.options,
159
- ...(d.guarded ? { guard: () => void log.push("guard") } : {}),
160
- })
161
- } catch (error) {
162
- if (!(error instanceof Stop)) throw error
163
- result = "stopped"
164
- }
165
- return {
166
- log,
167
- prompts,
168
- result,
169
- judged: asked.map((q) => q.id),
170
- asked,
171
- gates,
172
- gateMessages,
173
- since,
174
- scopes,
175
- }
176
- }
177
-
178
- const verdict = (answer: string, p = 0.9): JudgeAnswer => ({ answer, p })
179
-
180
- describe("the risk-fix pass", () => {
181
- const risky = doc(["Risk: drops the carry", "sub"])
182
- const clean = doc(["add — fine", "sub"])
183
-
184
- it("a marked risk is fixed, kept green, re-reviewed, then rests at the gate", async () => {
185
- const run = await drive({ notes: [], reviewDocs: [risky, clean] })
186
- expect(run.log.slice(0, 5)).toEqual([
187
- "review.reviewing",
188
- "review.fix.risks.fixing",
189
- "health.check",
190
- "review.reviewing",
191
- "review.await-review",
192
- ])
193
- expect(run.prompts.get("review.fix.risks.fixing")).toContain("risk-1")
194
- expect(run.prompts.get("review.fix.risks.fixing")).toContain("Risk: drops the carry")
195
- expect(run.prompts.get("review.fix.risks.fixing")).toContain("Leave `.gtd/REVIEW.md` untouched")
196
- })
197
-
198
- it("a risk the re-review still marks goes to the gate with no second fix", async () => {
199
- const run = await drive({ notes: [], reviewDocs: [risky, risky] })
200
- expect(run.log.filter((n) => n === "review.fix.risks.fixing")).toHaveLength(1)
201
- expect(run.log.slice(0, 5)).toEqual([
202
- "review.reviewing",
203
- "review.fix.risks.fixing",
204
- "health.check",
205
- "review.reviewing",
206
- "review.await-review",
207
- ])
208
- })
209
-
210
- it("fixAccess wraps both fix turns in their own scope, the step names unchanged", async () => {
211
- const fixAccess = { write: ["src/a.ts", ".gtd/**"] }
212
- const run = await drive({
213
- notes: ["add — typo", "sub", "mul"],
214
- answers: { "note-1": verdict("nit") },
215
- reviewDocs: [risky, clean],
216
- options: { fixAccess },
217
- })
218
- expect(run.scopes).toEqual([
219
- { name: "review.fix.risks", access: fixAccess },
220
- { name: "review.fix.nits", access: fixAccess },
221
- ])
222
- // The harness pushes no scope names, so the step is relative to its own fix scope.
223
- expect(run.log.filter((n) => n === "fixing")).toHaveLength(2)
224
- })
225
-
226
- it("without fixAccess the fix turns open no scope", async () => {
227
- const run = await drive({ notes: [], reviewDocs: [risky, clean] })
228
- expect(run.log).toContain("review.fix.risks.fixing")
229
- expect(run.scopes).toEqual([])
230
- })
231
-
232
- it("no marker means no fix-risks step", async () => {
233
- const run = await drive({ notes: [], reviewDocs: [clean] })
234
- expect(run.log).not.toContain("review.fix.risks.fixing")
235
- expect(run.log[0]).toBe("review.reviewing")
236
- expect(run.log[1]).toBe("review.await-review")
237
- })
238
-
239
- it("a nit re-review round gets its own single pass", async () => {
240
- const run = await drive({
241
- notes: ["add — typo", "sub", "mul"],
242
- answers: { "note-1": verdict("nit") },
243
- reviewDocs: [undefined, risky, clean],
244
- })
245
- expect(run.log).toEqual([
246
- "review.reviewing",
247
- "review.await-review",
248
- "review.triage",
249
- "review.fix.nits.fixing",
250
- "health.check",
251
- "review.closing",
252
- "review.reviewing",
253
- "review.fix.risks.fixing",
254
- "health.check",
255
- "review.reviewing",
256
- "review.await-review",
257
- ])
258
- })
259
- })
260
-
261
- describe("the guard", () => {
262
- const risky = doc(["Risk: drops the carry", "sub"])
263
- const clean = doc(["add — fine", "sub"])
264
-
265
- it("runs right after the risk-fix turn", async () => {
266
- const run = await drive({ notes: [], reviewDocs: [risky, clean], guarded: true })
267
- expect(run.log.slice(0, 5)).toEqual([
268
- "review.reviewing",
269
- "guard",
270
- "review.fix.risks.fixing",
271
- "guard",
272
- "health.check",
273
- ])
274
- })
275
-
276
- it("runs right after the nit-fix turn", async () => {
277
- const run = await drive({
278
- notes: ["add — typo", "sub", "mul"],
279
- answers: { "note-1": verdict("nit") },
280
- guarded: true,
281
- })
282
- expect(run.log.slice(3, 7)).toEqual([
283
- "review.triage",
284
- "review.fix.nits.fixing",
285
- "guard",
286
- "health.check",
287
- ])
288
- })
289
- })
290
-
291
- describe("the feedback target", () => {
292
- const edit = ["add — rename it", "sub", "mul"]
293
- const REFACTOR = ".gtd/REFACTOR.md"
294
-
295
- it("defaults to .gtd/REQUIREMENTS.md via the review.collecting step", async () => {
296
- const run = await drive({ notes: edit })
297
- expect(run.log.at(-1)).toBe("review.collecting")
298
- expect(run.result).toMatchObject({ verdict: "feedback" })
299
- })
300
-
301
- it("file replaces .gtd/REQUIREMENTS.md as the feedback test", async () => {
302
- let collected = ""
303
- const run = await drive({
304
- notes: edit,
305
- options: {
306
- feedback: {
307
- file: REFACTOR,
308
- collecting: (capture) => {
309
- collected = capture
310
- return Promise.resolve()
311
- },
312
- unmatched: "unmatched: refactor",
313
- },
314
- },
315
- files: { [REFACTOR]: "## Concern" },
316
- })
317
- expect(collected).toContain("rename it")
318
- expect(run.log).not.toContain("review.collecting")
319
- expect(run.result).toMatchObject({ verdict: "feedback" })
320
- })
321
-
322
- it("a collecting turn that wrote REQUIREMENTS but not file signs off unrefused with nothing changed", async () => {
323
- const run = await drive({
324
- notes: edit,
325
- options: {
326
- feedback: {
327
- file: REFACTOR,
328
- collecting: () => Promise.resolve(),
329
- unmatched: "unmatched: refactor",
330
- },
331
- },
332
- })
333
- expect(run.result).toEqual({ verdict: "signoff" })
334
- })
335
-
336
- it("a feature review edit note collected to nothing signs off", async () => {
337
- const run = await drive({
338
- notes: edit,
339
- options: {
340
- feedback: {
341
- file: REQUIREMENTS,
342
- collecting: () => Promise.resolve(),
343
- unmatched: "unmatched",
344
- },
345
- },
346
- })
347
- expect(run.result).toEqual({ verdict: "signoff" })
348
- })
349
-
350
- it("dropped is passed through as the refusal for a round collected to nothing", async () => {
351
- await expect(
352
- drive({
353
- notes: edit,
354
- options: {
355
- feedback: {
356
- file: REFACTOR,
357
- collecting: () => Promise.resolve(),
358
- unmatched: "unmatched: refactor",
359
- dropped: (r) => `dropped: ${r.notes.map((n) => n.id).join(",")}`,
360
- },
361
- },
362
- }),
363
- ).rejects.toThrow("dropped: note-1")
364
- })
365
-
366
- it("refuses with unmatched when the collecting turn changed something but not file", async () => {
367
- await expect(
368
- drive({
369
- notes: edit,
370
- options: {
371
- feedback: {
372
- file: REFACTOR,
373
- collecting: () => Promise.resolve(),
374
- unmatched: "unmatched: refactor",
375
- },
376
- },
377
- pending: [{ path: "src/x.ts", status: "modified", before: "a", after: "b" }],
378
- }),
379
- ).rejects.toThrow("unmatched: refactor")
380
- })
381
- })
382
-
383
- describe("reUnwind", () => {
384
- it("restores edited paths from restoreFrom and removes added ones, in the caller's scope", async () => {
385
- const steps: StepRequest[] = []
386
- installContext(
387
- fixtureContext(
388
- {},
389
- {
390
- step: (request) => {
391
- steps.push(request)
392
- return Promise.resolve()
393
- },
394
- read: (path) => (path === "src/a.ts" ? "old" : undefined),
395
- },
396
- ),
397
- )
398
- await reUnwind({
399
- verdict: "feedback",
400
- base: "b1",
401
- restoreFrom: "r1",
402
- edited: [
403
- { path: "src/a.ts", status: "modified", before: "old", after: "new" },
404
- { path: "src/new.ts", status: "added", before: undefined, after: "x" },
405
- ],
406
- })
407
- const [step] = steps
408
- expect(step).toMatchObject({ kind: "run", name: "re-unwind" })
409
- const body = step?.kind === "run" ? step.body : ""
410
- expect(body).toContain("git restore --source='r1' --worktree -- 'src/a.ts'")
411
- expect(body).toContain("rm -f -- 'src/new.ts'")
412
- })
413
- })
414
-
415
- describe("review verdict routing", () => {
416
- const notes = ["add — typo", "sub", "mul"]
417
-
418
- it("judges one note per added note, as one judge rest", async () => {
419
- const run = await drive({ notes: ["add — a", "sub — b", "mul"] })
420
- expect(run.judged).toEqual([
421
- "note-1",
422
- "note-1.security",
423
- "note-1.reversibility",
424
- "note-1.blastRadius",
425
- "note-1.newConcept",
426
- "note-2",
427
- "note-2.security",
428
- "note-2.reversibility",
429
- "note-2.blastRadius",
430
- "note-2.newConcept",
431
- ])
432
- expect(run.log.filter((n) => n === "review.triage")).toHaveLength(1)
433
- })
434
-
435
- it("asks only questions sendable to jev, each choice with its labels", async () => {
436
- const run = await drive({ notes: ["add — a"] })
437
- const { questions } = toRequest(run.asked, {})
438
- const labels = (id: string) => Object.keys((questions[id] as { criteria: object }).criteria)
439
- expect(labels("note-1")).toEqual(["edit", "question", "nit", "praise"])
440
- expect(labels("note-1.reversibility")).toEqual(["one-way", "two-way"])
441
- expect(labels("note-1.blastRadius")).toEqual(["local", "module", "contract"])
442
- })
443
-
444
- it("an unanswered note counts as edit", async () => {
445
- const run = await drive({ notes: ["add — typo", "sub", "mul"], answers: {} })
446
- expect(run.log).toEqual([
447
- "review.reviewing",
448
- "review.await-review",
449
- "review.triage",
450
- "review.closing",
451
- "review.collecting",
452
- ])
453
- expect(run.result).toMatchObject({ verdict: "feedback" })
454
- })
455
-
456
- it("a cut note counts as edit despite a confident praise", async () => {
457
- const run = await drive({
458
- notes,
459
- answers: { "note-1": verdict("praise", 0.99) },
460
- truncated: ["note-1"],
461
- })
462
- expect(run.log).toContain("review.collecting")
463
- })
464
-
465
- it("a verdict below the reviewNoteActionable floor counts as edit", async () => {
466
- const run = await drive({ notes, answers: { "note-1": verdict("praise", 0.5) } })
467
- expect(run.log).toContain("review.collecting")
468
- })
469
-
470
- it("a blank floor makes every note an edit", async () => {
471
- const run = await drive({
472
- notes,
473
- answers: { "note-1": verdict("praise", 1) },
474
- vars: { reviewNoteActionable: "" },
475
- })
476
- expect(run.log).toContain("review.collecting")
477
- })
478
-
479
- it("only praise closes and signs off with no agent turn", async () => {
480
- const run = await drive({ notes, answers: { "note-1": verdict("praise") } })
481
- expect(run.log).toEqual([
482
- "review.reviewing",
483
- "review.await-review",
484
- "review.triage",
485
- "review.closing",
486
- ])
487
- expect(run.result).toEqual({ verdict: "signoff" })
488
- })
489
-
490
- it("questions only are answered, then the gate again, never closed", async () => {
491
- const run = await drive({
492
- notes: ["add — why?", "sub — and here?", "mul"],
493
- answers: { "note-1": verdict("question"), "note-2": verdict("question") },
494
- })
495
- expect(run.log).toEqual([
496
- "review.reviewing",
497
- "review.await-review",
498
- "review.triage",
499
- "review.answer-review-questions",
500
- "review.await-review",
501
- ])
502
- expect(run.prompts.get("review.answer-review-questions")).toContain("note-2")
503
- expect(run.result).toBe("stopped")
504
- })
505
-
506
- it("nits without edits are fixed, kept green, closed, then re-reviewed with the answers carried", async () => {
507
- const run = await drive({
508
- notes: ["add — why?", "sub — typo", "mul"],
509
- answers: { "note-1": verdict("question"), "note-2": verdict("nit") },
510
- })
511
- expect(run.log).toEqual([
512
- "review.reviewing",
513
- "review.await-review",
514
- "review.triage",
515
- "review.answer-review-questions",
516
- "review.fix.nits.fixing",
517
- "health.check",
518
- "review.closing",
519
- "review.reviewing",
520
- ])
521
- expect(run.prompts.get("review.fix.nits.fixing")).not.toContain("why?")
522
- expect(run.prompts.get("review.reviewing")).toContain("Run: git show c4")
523
- expect(run.result).toBe("stopped")
524
- })
525
-
526
- it("nits alone carry nothing over", async () => {
527
- const run = await drive({
528
- notes: ["add — typo", "sub", "mul"],
529
- answers: { "note-1": verdict("nit") },
530
- })
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")
533
- })
534
-
535
- it("edits run after questions and nits, and the capture names only the edits and the answering commit", async () => {
536
- const run = await drive({
537
- notes: ["add — rename it", "sub — why?", "mul — typo"],
538
- answers: { "note-2": verdict("question"), "note-3": verdict("nit") },
539
- })
540
- expect(run.log).toEqual([
541
- "review.reviewing",
542
- "review.await-review",
543
- "review.triage",
544
- "review.answer-review-questions",
545
- "review.fix.nits.fixing",
546
- "health.check",
547
- "review.closing",
548
- "review.collecting",
549
- ])
550
- const capture = run.prompts.get("review.collecting") ?? ""
551
- expect(capture).toContain("note-1")
552
- expect(capture).toContain("rename it")
553
- expect(capture).not.toContain("note-2 —")
554
- expect(capture).not.toContain("note-3 —")
555
- expect(capture).toContain("commit c4 answered")
556
- })
557
-
558
- it("a noted round with no extractable note collects as one edit, without judging", async () => {
559
- const run = await drive({ notes: [], after: BEFORE + "\nSome stray prose.\n" })
560
- expect(run.log).toEqual([
561
- "review.reviewing",
562
- "review.await-review",
563
- "review.closing",
564
- "review.collecting",
565
- ])
566
- expect(run.prompts.get("review.collecting")).toContain("The human's notes are in")
567
- })
568
-
569
- it("never counts a nit fix as a hand-edit", async () => {
570
- const run = await drive({
571
- notes: ["add — rename it", "sub — typo", "mul"],
572
- answers: { "note-2": verdict("nit") },
573
- })
574
- expect(run.result).toMatchObject({ verdict: "feedback", edited: [] })
575
- })
576
- })
577
-
578
- describe("the default quality lenses", () => {
579
- it("are the seven lenses in the settled order", () => {
580
- expect(bundled.defaults.qualityReviews!.split(",").map((l) => l.trim())).toEqual([
581
- "correctness",
582
- "owasp-security",
583
- "ponytail-review",
584
- "test-audit",
585
- "conventions",
586
- "spec-challenge",
587
- "criteria-coverage",
588
- ])
589
- })
590
-
591
- it("expose builtInLenses through the public workflow module", () => {
592
- expect(Object.keys(bundled.builtInLenses).sort()).toEqual([
593
- "conventions",
594
- "correctness",
595
- "criteria-coverage",
596
- "spec-challenge",
597
- ])
598
- })
599
- })
600
-
601
- describe("severityOf", () => {
602
- const note = (forced = false): ReviewNote => ({
603
- id: "n",
604
- kind: "pointer",
605
- anchor: "a",
606
- before: "",
607
- text: "t",
608
- forced,
609
- })
610
- const ans = (o: Partial<Record<string, string>> = {}, p = 0.9) => {
611
- const base: Record<string, string> = {
612
- security: "no",
613
- reversibility: "two-way",
614
- blastRadius: "local",
615
- newConcept: "no",
616
- ...o,
617
- }
618
- return Object.fromEntries(Object.entries(base).map(([k, v]) => [`n.${k}`, { answer: v, p }]))
619
- }
620
- const j = (answers: ReturnType<typeof ans>, truncated: string[] = []) => ({ answers, truncated })
621
-
622
- it("security or one-way is critical and names each answer", () => {
623
- expect(severityOf(note(), j(ans({ security: "yes" })), 0.7)).toMatchObject({
624
- tier: "critical",
625
- reason: "critical: security",
626
- })
627
- expect(
628
- severityOf(note(), j(ans({ security: "yes", reversibility: "one-way" })), 0.7),
629
- ).toMatchObject({ tier: "critical", reason: "critical: security, one-way door", rank: 0 })
630
- })
631
-
632
- it("contract or new concept is should; neither is could", () => {
633
- expect(severityOf(note(), j(ans({ blastRadius: "contract" })), 0.7).tier).toBe("should")
634
- expect(severityOf(note(), j(ans({ newConcept: "yes" })), 0.7)).toMatchObject({
635
- tier: "should",
636
- reason: "should: new concept",
637
- rank: 1,
638
- })
639
- expect(severityOf(note(), j(ans()), 0.7)).toMatchObject({ tier: "could", rank: 2 })
640
- })
641
-
642
- it("unsure is critical: unanswered, unrecognised, cut, low p, blank floor", () => {
643
- const a = ans()
644
- delete (a as Record<string, unknown>)["n.security"]
645
- for (const x of [
646
- severityOf(note(), j(a), 0.7),
647
- severityOf(note(), j(ans({ security: "unsure" })), 0.7),
648
- severityOf(note(), j(ans(), ["n"]), 0.7),
649
- severityOf(note(), j(ans({}, 0.5)), 0.7),
650
- severityOf(note(), j(ans()), Infinity),
651
- ])
652
- expect(x).toMatchObject({ tier: "critical", reason: "critical: judge unsure" })
653
- })
654
-
655
- it("forced is critical regardless of answers", () => {
656
- expect(severityOf(note(true), j({}), 0.7)).toMatchObject({
657
- tier: "critical",
658
- reason: "critical: forced by `!`",
659
- })
660
- })
661
- })
662
-
663
- describe("severity routing", () => {
664
- const sev = (id: string, a: Record<string, string>): Record<string, JudgeAnswer> =>
665
- Object.fromEntries(Object.entries(a).map(([k, v]) => [`${id}.${k}`, verdict(v)]))
666
- const could = {
667
- security: "no",
668
- reversibility: "two-way",
669
- blastRadius: "local",
670
- newConcept: "no",
671
- }
672
-
673
- it("a forced note is asked nothing, not even the verdict, and is a critical edit", async () => {
674
- const run = await drive({ notes: ["! rename it — now", "sub"] })
675
- expect(run.judged.filter((id) => id.startsWith("note-1"))).toEqual([])
676
- expect(run.log).toContain("review.collecting")
677
- expect(run.prompts.get("review.collecting")).toContain("(critical: forced by `!`)")
678
- })
679
-
680
- it("a confident could edit is still re-planned, labelled could", async () => {
681
- const run = await drive({
682
- notes: ["add — quote", "sub", "mul"],
683
- answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
684
- })
685
- expect(run.log).toEqual([
686
- "review.reviewing",
687
- "review.await-review",
688
- "review.triage",
689
- "review.closing",
690
- "review.collecting",
691
- ])
692
- expect(run.prompts.get("review.collecting")).toContain("(could)")
693
- })
694
-
695
- it("severity answers on a nit leave it a nit", async () => {
696
- const run = await drive({
697
- notes: ["add — typo", "sub", "mul"],
698
- answers: { "note-1": verdict("nit"), ...sev("note-1", { reversibility: "one-way" }) },
699
- })
700
- expect(run.log).toContain("review.fix.nits.fixing")
701
- expect(run.log).not.toContain("review.collecting")
702
- })
703
-
704
- it("edits are captured critical before should, doors first, each labelled", async () => {
705
- const run = await drive({
706
- notes: ["add — plain", "sub — concept", "mul — door"],
707
- answers: {
708
- "note-1": verdict("edit"),
709
- ...sev("note-1", { ...could, security: "yes" }),
710
- ...sev("note-2", { ...could, newConcept: "yes" }),
711
- ...sev("note-3", { ...could, reversibility: "one-way" }),
712
- },
713
- })
714
- const capture = run.prompts.get("review.collecting") ?? ""
715
- const at = (id: string): number => capture.indexOf(`- ${id} —`)
716
- expect(at("note-3")).toBeLessThan(at("note-1"))
717
- expect(at("note-1")).toBeLessThan(at("note-2"))
718
- expect(capture).toContain("(critical: one-way door)")
719
- expect(capture).toContain("(should: new concept)")
720
- expect(capture).toContain("(critical: security)")
721
- })
722
-
723
- it("a round of only forced notes judges nothing and tiers each critical", async () => {
724
- const run = await drive({ notes: ["! rename it", "! drop it", "mul"] })
725
- expect(run.log).not.toContain("review.triage")
726
- expect(run.judged).toEqual([])
727
- expect(run.prompts.get("review.collecting")).toContain("(critical: forced by `!`)")
728
- expect(run.prompts.get("review.collecting")).not.toContain("! rename")
729
- })
730
-
731
- it("a note opening with != is not forced and keeps its text", async () => {
732
- const run = await drive({ notes: ["!= should be ===", "sub"] })
733
- expect(run.judged).toContain("note-1.security")
734
- })
735
-
736
- it("the first round's gate accepts a clean land", async () => {
737
- const run = await drive({ notes: ["add", "sub", "mul"] })
738
- expect(run.gates.length).toBeGreaterThan(0)
739
- expect(run.gates.every((g) => g)).toBe(true)
740
- })
741
-
742
- it("a blank reviewSeverityMinP makes every confident could critical", async () => {
743
- const run = await drive({
744
- notes: ["add — quote", "sub", "mul"],
745
- answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
746
- vars: { reviewSeverityMinP: "" },
747
- })
748
- expect(run.prompts.get("review.collecting")).toContain("(critical: judge unsure)")
749
- })
750
- })
751
-
752
- describe("BuildTailOptions.lensBase", () => {
753
- const lensPrompts = async (
754
- options: BuildTailOptions,
755
- call: () => Promise<unknown> = () => buildTail("base", { ...options, green: true }),
756
- ): Promise<string[]> => {
757
- const prompts: string[] = []
758
- installContext(
759
- fixtureContext(
760
- { vars: { qualityReviews: "conventions, correctness" } },
761
- {
762
- step: (request: StepRequest) => {
763
- if (request.kind === "restart") return Promise.resolve()
764
- if (request.kind === "agent" && request.name.startsWith("quality."))
765
- prompts.push(request.prompt)
766
- if (request.name === "review.reviewing") throw new Stop()
767
- return Promise.resolve()
768
- },
769
- pushScope: () => undefined,
770
- popScope: () => undefined,
771
- changesSince: () => {
772
- throw new Stop()
773
- },
774
- start: () => "START-SHA",
775
- },
776
- ),
777
- )
778
- await call().catch((error: unknown) => {
779
- if (!(error instanceof Stop)) throw error
780
- })
781
- return prompts
782
- }
783
-
784
- it("reaches every quality-lens prompt", async () => {
785
- const prompts = await lensPrompts({ lensBase: "LENS-BASE" })
786
- expect(prompts).toHaveLength(2)
787
- for (const prompt of prompts) expect(prompt).toContain("from `LENS-BASE`")
788
- })
789
-
790
- it("absent, the lens prompts name start()", async () => {
791
- const prompts = await lensPrompts({})
792
- expect(prompts).toHaveLength(2)
793
- for (const prompt of prompts) expect(prompt).toContain("from `START-SHA`")
794
- })
795
-
796
- it("the deprecated positional form runs the same tail as the options form", async () => {
797
- const options = { lensBase: "LENS-BASE" }
798
- const modern = await lensPrompts(options)
799
- const legacy = await lensPrompts(options, () => buildTail(false, "base", true, options))
800
- expect(legacy).toEqual(modern)
801
- expect(legacy).toHaveLength(2)
802
- })
803
-
804
- describe("buildTail flags", () => {
805
- const requestNames = async (
806
- call: () => Promise<unknown>,
807
- names: string[] = [],
808
- ): Promise<string[]> => {
809
- installContext(
810
- fixtureContext(
811
- {},
812
- {
813
- step: (request: StepRequest) => {
814
- if (request.kind === "restart") return Promise.resolve()
815
- names.push(`${request.kind}:${request.name}`)
816
- if (names.length >= 4) throw new Stop()
817
- return Promise.resolve()
818
- },
819
- pushScope: () => undefined,
820
- popScope: () => undefined,
821
- changesSince: () => {
822
- throw new Stop()
823
- },
824
- start: () => "START-SHA",
825
- },
826
- ),
827
- )
828
- await call().catch((error: unknown) => {
829
- if (!(error instanceof Stop)) throw error
830
- })
831
- return names
832
- }
833
-
834
- for (const [fixFirst, green] of [
835
- [true, false],
836
- [true, true],
837
- [false, true],
838
- [false, false],
839
- ] as const) {
840
- it(`fixFirst ${fixFirst}, green ${green} match the options form`, async () => {
841
- const modern = await requestNames(() => buildTail("base", { fixFirst, green }))
842
- const legacy = await requestNames(() => buildTail(fixFirst, "base", green))
843
- expect(legacy).toEqual(modern)
844
- })
845
- }
846
-
847
- it("fixFirst and green each change the request sequence", async () => {
848
- const seq = (fixFirst: boolean, green: boolean) =>
849
- requestNames(() => buildTail(fixFirst, "base", green))
850
- const base = await seq(false, false)
851
- expect(await seq(true, false)).not.toEqual(base)
852
- expect(await seq(false, true)).not.toEqual(base)
853
- })
854
-
855
- for (const bad of [undefined, null, 1, {}]) {
856
- it(`rejects naming both forms for sole first argument ${String(bad)}`, async () => {
857
- const names: string[] = []
858
- const rejected = expect(requestNames(() => buildTail(bad as never), names)).rejects
859
- await rejected.toThrow("buildTail(base, options?)")
860
- await rejected.toThrow("buildTail(fixFirst, base, green?, options?)")
861
- expect(names).toEqual([])
862
- })
863
- }
864
-
865
- it("rejects an undefined first argument followed by an options object", async () => {
866
- const names: string[] = []
867
- const rejected = expect(
868
- requestNames(() => buildTail(undefined as never, { green: true } as never), names),
869
- ).rejects
870
- await rejected.toThrow("buildTail(base, options?)")
871
- await rejected.toThrow("buildTail(fixFirst, base, green?, options?)")
872
- expect(names).toEqual([])
873
- })
874
-
875
- const fitsNeither: [string, () => Promise<unknown>][] = [
876
- ["a boolean with no base", () => buildTail(true as never)],
877
- ["false followed by an options object", () => buildTail(false as never, {} as never)],
878
- ["a base followed by a boolean", () => buildTail("base", true as never)],
879
- ["a base followed by a string array", () => buildTail("base", ["x"] as never)],
880
- [
881
- "a base with a third argument",
882
- () => (buildTail as (...args: unknown[]) => Promise<unknown>)("base", {}, true),
883
- ],
884
- ["a number followed by a base", () => buildTail(1 as never, "base")],
885
- ["a string fixFirst followed by a base", () => buildTail("x" as never, "base")],
886
- ["a legacy call with a non-boolean green", () => buildTail(false, "base", 1 as never)],
887
- ["a legacy call with non-object options", () => buildTail(false, "base", true, 1 as never)],
888
- ["a base followed by a Promise", () => buildTail("base", Promise.resolve({}) as never)],
889
- [
890
- "a base with a fifth argument",
891
- () =>
892
- (buildTail as (...args: unknown[]) => Promise<unknown>)(
893
- "base",
894
- {},
895
- undefined,
896
- undefined,
897
- {},
898
- ),
899
- ],
900
- [
901
- "a legacy call with a fifth argument",
902
- () => (buildTail as (...args: unknown[]) => Promise<unknown>)(false, "base", true, {}, {}),
903
- ],
904
- ]
905
- for (const [label, call] of fitsNeither) {
906
- it(`rejects ${label} before any step runs`, async () => {
907
- const names: string[] = []
908
- const rejected = expect(requestNames(call, names)).rejects
909
- await rejected.toThrow("buildTail(base, options?)")
910
- await rejected.toThrow("buildTail(fixFirst, base, green?, options?)")
911
- expect(names).toEqual([])
912
- })
913
- }
914
-
915
- it("reads null and trailing undefined arguments as absent", async () => {
916
- const omitted = await requestNames(() => buildTail("base"))
917
- expect(await requestNames(() => buildTail(null as never, "base", null as never))).toEqual(
918
- omitted,
919
- )
920
- expect(
921
- await requestNames(() =>
922
- (buildTail as (...args: unknown[]) => Promise<unknown>)(
923
- "base",
924
- {},
925
- null,
926
- undefined,
927
- null,
928
- ),
929
- ),
930
- ).toEqual(omitted)
931
- expect(await requestNames(() => buildTail("base", null as never))).toEqual(omitted)
932
- expect(await requestNames(() => buildTail(false, "base", false, null as never))).toEqual(
933
- omitted,
934
- )
935
- })
936
-
937
- it("accepts a class instance as options", async () => {
938
- class Options {
939
- green = true
940
- }
941
- expect(await requestNames(() => buildTail("base", new Options()))).toEqual(
942
- await requestNames(() => buildTail("base", { green: true })),
943
- )
944
- })
945
-
946
- it("accepts a null-prototype options object", async () => {
947
- expect(await requestNames(() => buildTail("base", Object.create(null) as never))).toEqual(
948
- await requestNames(() => buildTail("base")),
949
- )
950
- })
951
-
952
- it("still runs buildTail(undefined, base) as a legacy call", async () => {
953
- const names: string[] = []
954
- await requestNames(() => buildTail(undefined as never, "base"), names)
955
- expect(names).not.toEqual([])
956
- })
957
-
958
- // pins `| undefined` on every BuildTailOptions field under exactOptionalPropertyTypes (typecheck)
959
- it("explicitly undefined options default like omitted ones", async () => {
960
- expect(
961
- await requestNames(() =>
962
- buildTail("base", {
963
- fixFirst: undefined,
964
- green: undefined,
965
- guard: undefined,
966
- guardRests: undefined,
967
- lenses: undefined,
968
- feedback: undefined,
969
- fixAccess: undefined,
970
- lensBase: undefined,
971
- rules: undefined,
972
- lap: undefined,
973
- }),
974
- ),
975
- ).toEqual(await requestNames(() => buildTail("base")))
976
- })
977
- })
978
- })
979
-
980
- // pins the public re-exports from the published workflows entry
981
- export type _PublicBuildTailOptions = [
982
- bundled.BuildTailOptions,
983
- bundled.FeedbackTarget,
984
- bundled.ReviewRange,
985
- bundled.TailOptions,
986
- ]
987
-
988
- describe("buildTail without rules", () => {
989
- const RULES = "Change only these paths"
990
-
991
- const tailPrompts = async (reds: number): Promise<Map<string, string>> => {
992
- const prompts = new Map<string, string>()
993
- let checks = 0
994
- const files = new Map<string, string>([[".gtd/QUALITY.md", "- finding"]])
995
- installContext(
996
- fixtureContext(
997
- { vars: { qualityReviews: "conventions" } },
998
- {
999
- step: (request: StepRequest) => {
1000
- if (request.kind === "restart") return Promise.resolve()
1001
- if (request.kind === "agent") prompts.set(request.name, request.prompt)
1002
- if (request.name === "health.check") checks++
1003
- if (request.name === "fix.quality.fixing") files.delete(".gtd/QUALITY.md")
1004
- if (request.name === "review.reviewing" || request.name === "health.stop")
1005
- throw new Stop()
1006
- return Promise.resolve(
1007
- request.kind === "judge"
1008
- ? { answers: { Q1: { answer: "mechanical", p: 0.9 } }, truncated: [] }
1009
- : undefined,
1010
- )
1011
- },
1012
- pushScope: () => undefined,
1013
- popScope: () => undefined,
1014
- read: (path) =>
1015
- path === ".gtd/FEEDBACK.md" ? (checks <= reds ? "red" : undefined) : files.get(path),
1016
- changes: () =>
1017
- checks <= reds
1018
- ? [{ path: ".gtd/FEEDBACK.md", status: "added", before: undefined, after: "red" }]
1019
- : [],
1020
- matches: (path, pattern) => path === pattern,
1021
- changesSince: () => {
1022
- throw new Stop()
1023
- },
1024
- start: () => "c0",
1025
- },
1026
- ),
1027
- )
1028
- await buildTail("base", { fixFirst: true }).catch((error: unknown) => {
1029
- if (!(error instanceof Stop)) throw error
1030
- })
1031
- return prompts
1032
- }
1033
-
1034
- it("renders the feature tail's fix and describe prompts unchanged", async () => {
1035
- const prompts = await tailPrompts(6)
1036
- expect(prompts.get("fix")).toMatch(/- Leave everything uncommitted — do not commit\n$/)
1037
- expect(prompts.get("health.describe")).toMatch(/this turn only writes the document\n$/)
1038
- for (const prompt of prompts.values()) expect(prompt).not.toContain(RULES)
1039
- })
1040
-
1041
- it("renders the feature tail's quality-fix prompt unchanged", async () => {
1042
- const prompts = await tailPrompts(0)
1043
- expect(prompts.get("fix.quality.fixing")).toMatch(
1044
- /- Leave everything else uncommitted and finish your turn\n$/,
1045
- )
1046
- expect(prompts.get("fix.quality.fixing")).not.toContain(RULES)
1047
- })
1048
-
1049
- it("renders the risk-fix and nit-fix prompts unchanged", async () => {
1050
- const run = await drive({
1051
- notes: [],
1052
- reviewDocs: [doc(["Risk: x", "sub"]), doc(["add", "sub"])],
1053
- })
1054
- expect(run.prompts.get("review.fix.risks.fixing")).toContain("add a test that fails")
1055
- expect(run.prompts.get("review.fix.risks.fixing")).not.toContain(RULES)
1056
- const nits = await drive({
1057
- notes: ["add — typo", "sub", "mul"],
1058
- answers: { "note-1": verdict("nit") },
1059
- })
1060
- expect(nits.prompts.get("review.fix.nits.fixing")).not.toContain(RULES)
1061
- })
1062
- })