@pmelab/gtd 51.0.1 → 52.1.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 (74) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +46 -40
  3. package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-DxB5CROj.mjs} +73 -38
  4. package/dist/chunks/{review-BaCXlIT2.mjs → review-CKMSdvgW.mjs} +11 -9
  5. package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
  6. package/dist/chunks/{tail-CX6Pdsql.mjs → tail-QZc7Uz2U.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +198 -43
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +1 -1
  10. package/dist/web/main.css +1 -1
  11. package/dist/web/main.js +510 -147
  12. package/dist/workflow-kit.mjs +1 -1
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +228 -222
  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 +570 -317
  19. package/package.json +5 -4
  20. package/src/steering/FORMATS.md +4 -1
  21. package/src/steering/SteeringFormat.ts +7 -1
  22. package/src/steering/contract.test.ts +2 -1
  23. package/src/steering/fixtures/review/sectioned.md +31 -0
  24. package/src/steering/fixtures/review/sectioned.read.json +119 -0
  25. package/src/steering/index.ts +4 -0
  26. package/src/steering/qa.ts +1 -1
  27. package/src/steering/review.test.ts +270 -4
  28. package/src/steering/review.ts +328 -69
  29. package/src/testkit/steps.ts +13 -0
  30. package/src/workflows/feature/acceptance-criteria.feature +159 -3
  31. package/src/workflows/feature/continuation-cap.feature +4 -0
  32. package/src/workflows/feature/continuation-rebuild.feature +4 -0
  33. package/src/workflows/feature/continuation-slices.feature +4 -0
  34. package/src/workflows/feature/continuation.feature +4 -0
  35. package/src/workflows/feature/criteria.test.ts +60 -1
  36. package/src/workflows/feature/criteria.ts +87 -8
  37. package/src/workflows/feature/fast-path.feature +5 -6
  38. package/src/workflows/feature/feature.test.ts +6 -5
  39. package/src/workflows/feature/feature.ts +1 -2
  40. package/src/workflows/feature/flag-mode.feature +5 -6
  41. package/src/workflows/feature/green.ts +22 -0
  42. package/src/workflows/feature/hands-on-skip.feature +384 -0
  43. package/src/workflows/feature/lens-models.feature +4 -4
  44. package/src/workflows/feature/lens-review.feature +10 -8
  45. package/src/workflows/feature/naive-acceptance.feature +4 -0
  46. package/src/workflows/feature/one-step-land.feature +5 -6
  47. package/src/workflows/feature/planning.ts +2 -1
  48. package/src/workflows/feature/prototype-rounds.feature +5 -1
  49. package/src/workflows/feature/prototype.feature +18 -9
  50. package/src/workflows/feature/prototype.ts +48 -17
  51. package/src/workflows/feature/refactor-absorbs.feature +7 -8
  52. package/src/workflows/feature/refactor-first.feature +7 -8
  53. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  54. package/src/workflows/feature/refactor-rebuild.feature +11 -12
  55. package/src/workflows/feature/refactor-review.feature +72 -72
  56. package/src/workflows/feature/review-answers.feature +94 -5
  57. package/src/workflows/feature/review-gate.feature +23 -53
  58. package/src/workflows/feature/reviewer-walkthrough.feature +33 -12
  59. package/src/workflows/feature/risk-tier.feature +32 -0
  60. package/src/workflows/feature/seam-rejection.feature +9 -6
  61. package/src/workflows/feature/seam.feature +9 -6
  62. package/src/workflows/feature/seam.test.ts +5 -3
  63. package/src/workflows/feature/seam.ts +2 -3
  64. package/src/workflows/feature/severity-budget.feature +11 -7
  65. package/src/workflows/feature/severity-sorted-review.feature +54 -54
  66. package/src/workflows/feature/slice-findings.feature +9 -15
  67. package/src/workflows/feature/slicing.feature +36 -24
  68. package/src/workflows/feature/standards.feature +4 -4
  69. package/src/workflows/feature/tier-pin.feature +4 -0
  70. package/src/workflows/shared/review.ts +26 -6
  71. package/src/workflows/shared/text.test.ts +85 -11
  72. package/src/workflows/shared/text.ts +95 -42
  73. package/src/workflows/feature/coverage.feature +0 -165
  74. package/src/workflows/feature/coverage.ts +0 -120
@@ -36,6 +36,10 @@ Feature: a change with more criteria than the cap is built in coarse slices
36
36
  """
37
37
  # Requirements
38
38
 
39
+ ## Impact
40
+
41
+ - the greeting module
42
+
39
43
  ## Acceptance Criteria
40
44
 
41
45
  - AC-1: subtract(a, b) returns a - b.
@@ -47,10 +51,6 @@ Feature: a change with more criteria than the cap is built in coarse slices
47
51
  it("[AC-2] multiplies", () => {})
48
52
  """
49
53
  And step "seam.naive" is prompted with "Read `.gtd/REQUIREMENTS.md`"
50
- And judge step "implement.coverage" answers:
51
- """
52
- [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
53
- """
54
54
  When the process is driven
55
55
  Then the process rests at "build.review.reviewing"
56
56
  And the commit subjects end with:
@@ -59,8 +59,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
59
59
  gtd(agent): seam.naive → seam.check
60
60
  gtd(check): seam.check → implement.applying
61
61
  gtd(check): implement.applying → implement.health.check
62
- gtd(check): implement.health.check → implement.coverage
63
- gtd(judge): implement.coverage → implement.closing
62
+ gtd(check): implement.health.check → implement.closing
64
63
  gtd(check): implement.closing → build.review.reviewing
65
64
  """
66
65
  And the output lines of gtd "slices" start with:
@@ -75,6 +74,10 @@ Feature: a change with more criteria than the cap is built in coarse slices
75
74
  """
76
75
  # Requirements
77
76
 
77
+ ## Impact
78
+
79
+ - the greeting module
80
+
78
81
  ## Acceptance Criteria
79
82
 
80
83
  - AC-1: subtract(a, b) returns a - b.
@@ -104,14 +107,6 @@ Feature: a change with more criteria than the cap is built in coarse slices
104
107
  """
105
108
  it("[AC-2] multiplies", () => {})
106
109
  """
107
- And judge step "implement.coverage", when its questions contain "AC-3", answers:
108
- """
109
- [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-3","answer":"yes","p":0.9}]
110
- """
111
- And judge step "implement.coverage", when its questions contain "AC-2", answers:
112
- """
113
- [{"id":"AC-2","answer":"yes","p":0.9}]
114
- """
115
110
  When the process is driven
116
111
  Then the process rests at "build.review.reviewing"
117
112
  And the commit subjects end with:
@@ -122,14 +117,12 @@ Feature: a change with more criteria than the cap is built in coarse slices
122
117
  gtd(agent): seam.naive → seam.check
123
118
  gtd(check): seam.check → implement.applying
124
119
  gtd(check): implement.applying → implement.health.check
125
- gtd(check): implement.health.check → implement.coverage
126
- gtd(judge): implement.coverage → implement.closing
120
+ gtd(check): implement.health.check → implement.closing
127
121
  gtd(check): implement.closing → seam.naive
128
122
  gtd(agent): seam.naive → seam.check
129
123
  gtd(check): seam.check → implement.applying
130
124
  gtd(check): implement.applying → implement.health.check
131
- gtd(check): implement.health.check → implement.coverage
132
- gtd(judge): implement.coverage → implement.closing
125
+ gtd(check): implement.health.check → implement.closing
133
126
  gtd(check): implement.closing → build.review.reviewing
134
127
  """
135
128
  And the output lines of gtd "slices" start with:
@@ -148,6 +141,10 @@ Feature: a change with more criteria than the cap is built in coarse slices
148
141
  """
149
142
  # Requirements
150
143
 
144
+ ## Impact
145
+
146
+ - the greeting module
147
+
151
148
  ## Acceptance Criteria
152
149
 
153
150
  - AC-1: subtract(a, b) returns a - b.
@@ -171,6 +168,10 @@ Feature: a change with more criteria than the cap is built in coarse slices
171
168
  """
172
169
  # Requirements
173
170
 
171
+ ## Impact
172
+
173
+ - the greeting module
174
+
174
175
  ## Acceptance Criteria
175
176
 
176
177
  - AC-1: subtract(a, b) returns a - b.
@@ -196,6 +197,10 @@ Feature: a change with more criteria than the cap is built in coarse slices
196
197
  """
197
198
  # Requirements
198
199
 
200
+ ## Impact
201
+
202
+ - the greeting module
203
+
199
204
  ## Acceptance Criteria
200
205
 
201
206
  - AC-1: subtract(a, b) returns a - b.
@@ -206,6 +211,10 @@ Feature: a change with more criteria than the cap is built in coarse slices
206
211
  """
207
212
  # Requirements
208
213
 
214
+ ## Impact
215
+
216
+ - the greeting module
217
+
209
218
  ## Acceptance Criteria
210
219
 
211
220
  - AC-1: subtract(a, b) returns a - b.
@@ -230,6 +239,10 @@ Feature: a change with more criteria than the cap is built in coarse slices
230
239
  """
231
240
  # Requirements
232
241
 
242
+ ## Impact
243
+
244
+ - the greeting module
245
+
233
246
  ## Acceptance Criteria
234
247
 
235
248
  - AC-1: subtract(a, b) returns a - b.
@@ -253,16 +266,16 @@ Feature: a change with more criteria than the cap is built in coarse slices
253
266
  """
254
267
  it("[AC-1] subtracts", () => {})
255
268
  """
256
- And judge step "implement.coverage" answers:
257
- """
258
- [{"id":"AC-1","answer":"yes","p":0.9}]
259
- """
260
269
  When the process is driven
261
270
  Then the process rests at "seam.steer"
262
271
  Given a file ".gtd/REQUIREMENTS.md" with:
263
272
  """
264
273
  # Requirements
265
274
 
275
+ ## Impact
276
+
277
+ - the greeting module
278
+
266
279
  ## Acceptance Criteria
267
280
 
268
281
  - AC-1: subtract(a, b) returns a - b.
@@ -276,8 +289,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
276
289
  gtd(agent): seam.naive → seam.check
277
290
  gtd(check): seam.check → implement.applying
278
291
  gtd(check): implement.applying → implement.health.check
279
- gtd(check): implement.health.check → implement.coverage
280
- gtd(judge): implement.coverage → implement.closing
292
+ gtd(check): implement.health.check → implement.closing
281
293
  gtd(check): implement.closing → build.review.reviewing
282
294
  """
283
295
  And ".gtd/REQUIREMENTS.md" does not exist
@@ -37,6 +37,10 @@ Feature: the review learns standards from the human's answers
37
37
 
38
38
  - `subtract(a, b)` returns `a - b`.
39
39
 
40
+ ## Impact
41
+
42
+ - the greeting module
43
+
40
44
  ## Acceptance Criteria
41
45
 
42
46
  - AC-1: subtract(a, b) returns a - b.
@@ -55,10 +59,6 @@ Feature: the review learns standards from the human's answers
55
59
  And step "implement.applying" runs its script
56
60
  And step "implement.health.check" changes nothing
57
61
  And step "implement.closing" runs its script
58
- And judge step "implement.coverage" answers:
59
- """
60
- [{"id":"AC-1","answer":"yes","p":0.9}]
61
- """
62
62
  And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
63
63
  """
64
64
  # Quality
@@ -34,6 +34,10 @@ Feature: a process keeps the risk tier setting it started with when GTD_FLAG_RIS
34
34
 
35
35
  low — a new pure function
36
36
 
37
+ ## Impact
38
+
39
+ - the greeting module
40
+
37
41
  ## Acceptance Criteria
38
42
 
39
43
  - AC-1: the change behaves as described.
@@ -16,7 +16,14 @@ import {
16
16
  type Judgment,
17
17
  type ScopeAccess,
18
18
  } from "../../flows/index.js"
19
- import { reviewChunks, reviewRisks, type ReviewNote } from "../../steering/index.js"
19
+ import {
20
+ REVIEW_SECTIONS,
21
+ reviewChunks,
22
+ reviewRisks,
23
+ reviewSections,
24
+ sectionedReview,
25
+ type ReviewNote,
26
+ } from "../../steering/index.js"
20
27
  import * as kit from "../../workflow-kit/index.js"
21
28
  import { guarded, healthy, type EscalationCount } from "./health.js"
22
29
  import { NO_FINDINGS, type LensReport } from "./lenses.js"
@@ -231,11 +238,16 @@ const batched = <T extends { readonly evidence: string }>(
231
238
  }
232
239
 
233
240
  /**
234
- * Rates every chunk the reviewer wrote — a walkthrough chunk or a carried
235
- * finding — and sorts \`full\` by it, each chunk led by its tier and reason.
236
- * gtd's summary chunks are not rated and stay last.
241
+ * Rates every chunk the reviewer wrote and sorts the \`findings\` among them
242
+ * first in \`full\`, each led by its tier and reason; a walkthrough chunk
243
+ * keeps its place and carries no tier. It is still rated, so a review with no
244
+ * findings still reaches \`review.ranking\` and older histories replay.
237
245
  */
238
- const ranked = async (review: string, full: string): Promise<string> => {
246
+ const ranked = async (
247
+ review: string,
248
+ full: string,
249
+ findings: ReadonlySet<string>,
250
+ ): Promise<string> => {
239
251
  const texts = t.chunkTexts(full)
240
252
  const chunks = reviewChunks(review).map((c, i) => ({
241
253
  id: `C${i + 1}`,
@@ -268,6 +280,7 @@ const ranked = async (review: string, full: string): Promise<string> => {
268
280
  full,
269
281
  chunks
270
282
  .map((c, i) => ({ c, i, s: severities.get(c.id)! }))
283
+ .filter(({ c }) => findings.has(c.title))
271
284
  .sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i)
272
285
  .map(({ c, s }) => ({ title: c.title, label: s.reason })),
273
286
  )
@@ -342,6 +355,10 @@ const requirePointers = (
342
355
  ...chunks
343
356
  .filter((c, i) => chunks.findIndex((d) => d.title === c.title) !== i)
344
357
  .map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — a second chunk with this title`),
358
+ ...reviewSections(review).map(
359
+ (s) =>
360
+ ` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} — gtd sorts the chunks into sections; write \`## \` chunks only`,
361
+ ),
345
362
  // A risk is fixed before the human sees it; a note is the human's to decide.
346
363
  ...reviewRisks(review)
347
364
  .filter((r) => [...noted].some((title) => r.anchor.startsWith(`${title} ./`)))
@@ -377,7 +394,10 @@ const walkthrough = async (
377
394
  changesSince(base).some((c) => isCode(c.path)),
378
395
  )
379
396
  // Ranked even when it marks risks: the review written after their fix reaches the human with its own.
380
- const full = await ranked(review, t.withWalkthrough(review, report))
397
+ const full = sectionedReview(
398
+ await ranked(review, t.withWalkthrough(review, report), new Set(notes.map((n) => n.title))),
399
+ new Set(reviewChunks(review).map((c) => c.title)),
400
+ )
381
401
  if (full !== written) {
382
402
  await run("review.carrying", writeScript(REVIEW, full), {
383
403
  label: "Ordering the review and adding the quality lap's findings",
@@ -4,6 +4,8 @@ import { tmpdir } from "node:os"
4
4
  import { join } from "node:path"
5
5
  import { afterEach, describe, expect, it } from "vitest"
6
6
  import {
7
+ designImpactPrompt,
8
+ prototypeFixingPrompt,
7
9
  buildReviewTriageMessage,
8
10
  buildReviewReviewingPrompt,
9
11
  agentWithSkills,
@@ -19,9 +21,12 @@ import {
19
21
  buildReviewCollectingPrompt,
20
22
  designGateAnswerMessage,
21
23
  designTriagePrompt,
24
+ normaliseCriteriaScript,
25
+ prototypeTestingPrompt,
22
26
  implementBuildingPrompt,
23
27
  implementFixPrompt,
24
28
  naivePassPrompt,
29
+ slicingPrompt,
25
30
  refactorBuildingPrompt,
26
31
  refactorDroppedRefusal,
27
32
  refactorFixPrompt,
@@ -170,6 +175,37 @@ describe("the risk tier prompt and gate line", () => {
170
175
  })
171
176
  })
172
177
 
178
+ describe("the (no hands-on) tag", () => {
179
+ it("is explained at triage and at the design gate", () => {
180
+ expect(renderText(() => designTriagePrompt("base"))).toContain(
181
+ "`- AC-<n> (no hands-on): <criterion>`",
182
+ )
183
+ expect(renderText(() => designGateAnswerMessage())).toContain(
184
+ "add or remove `(no hands-on)` between an `AC-<n>` and its colon",
185
+ )
186
+ })
187
+
188
+ it("names the tagged criteria the testing turn skips, and only when there are any", () => {
189
+ expect(renderText(() => prototypeTestingPrompt(undefined, ["AC-2", "AC-3"], false))).toContain(
190
+ "- Skip AC-2, AC-3: no person can observe them",
191
+ )
192
+ expect(renderText(() => prototypeTestingPrompt(undefined, [], false))).not.toContain("Skip")
193
+ })
194
+
195
+ it("is kept when the stated criteria are recorded", () => {
196
+ const script = normaliseCriteriaScript({
197
+ path: "TODO.md",
198
+ commit: "abc",
199
+ criteria: [
200
+ { id: "AC-1", text: "a" },
201
+ { id: "AC-2", text: "b", noHandsOn: true },
202
+ ],
203
+ })
204
+ expect(script).toContain("'- AC-1: a'")
205
+ expect(script).toContain("'- AC-2 (no hands-on): b'")
206
+ })
207
+ })
208
+
173
209
  describe("the shared open-question instruction", () => {
174
210
  it("leaves the option count to the agent, floored at two real options", () => {
175
211
  const prompt = renderText(() => designTriagePrompt("base"))
@@ -889,6 +925,29 @@ describe("buildReviewReviewingPrompt scenarios", () => {
889
925
  })
890
926
  })
891
927
 
928
+ describe("Impact in prompts", () => {
929
+ it("[AC-4] the slicing prompt slices along the units Impact names", () => {
930
+ const prompt = renderText(() => slicingPrompt(8))
931
+ expect(prompt).toContain("## Impact")
932
+ expect(prompt).toMatch(/slice\s+along\s+the\s+units\s+it\s+names/)
933
+ })
934
+
935
+ it.each([
936
+ ["naive", () => naivePassPrompt()],
937
+ ["implement", () => implementBuildingPrompt(undefined)],
938
+ ["prototype fixing", () => prototypeFixingPrompt(undefined)],
939
+ ])("[AC-5] [AC-7] the %s prompt gives Impact as hints, not allowed files", (_n, prompt) => {
940
+ const p = renderText(prompt).replace(/\s+/g, " ")
941
+ expect(p).toContain("`## Impact`")
942
+ expect(p).toContain("not a list of allowed files")
943
+ })
944
+
945
+ it("the prototype testing prompt, which changes no code, carries no Impact hints", () => {
946
+ const p = renderText(() => prototypeTestingPrompt(undefined, [], false))
947
+ expect(p).not.toContain("not a list of allowed files")
948
+ })
949
+ })
950
+
892
951
  describe("the seam phase's texts", () => {
893
952
  const rejection = { strays: ["src/flows/helpers.ts", "src/flows/runtime.ts"], p: 0.85 }
894
953
 
@@ -1281,7 +1340,7 @@ describe("composedReview", () => {
1281
1340
  })
1282
1341
 
1283
1342
  it("asks about each undecided standard in a note that blocks sign-off", () => {
1284
- expect(withConflict).toMatch(/^## Standard conflict [0-9a-f]{8}$/m)
1343
+ expect(withConflict).toMatch(/^## Findings\n\n### Standard conflict [0-9a-f]{8}$/m)
1285
1344
  expect(withConflict).toContain("- [ ] keep the new one (drop the old)")
1286
1345
  expect(unansweredNotes(withConflict)).toHaveLength(1)
1287
1346
  })
@@ -1317,8 +1376,8 @@ describe("composedReview", () => {
1317
1376
  })
1318
1377
  expect(steeringFormatFor("review")!.validate(doc)).toEqual([])
1319
1378
  expect(doc).toContain("# Review: 0123456\n\n<!-- base: " + BASE + " -->")
1320
- expect(doc).toContain("## same\n\none")
1321
- expect(doc).toContain("## same (2)\n\n_The fixer left this unresolved._\n\ntwo")
1379
+ expect(doc).toContain("## Findings\n\n### same\n\none")
1380
+ expect(doc).toContain("### same (2)\n\n_The fixer left this unresolved._\n\ntwo")
1322
1381
  expect(doc).toContain("## Fixed automatically\n\n- f (fedcba9)")
1323
1382
  expect(doc).toContain("## Dropped as noise\n\n- n (p=0.8)")
1324
1383
  })
@@ -1340,12 +1399,13 @@ describe("composedReview", () => {
1340
1399
  fixed: [{ title: "f", commit: "fedcba9876543210" }],
1341
1400
  noise: [],
1342
1401
  })
1343
- const titles = doc.split("\n").filter((l) => l.startsWith("## "))
1402
+ const titles = doc.split("\n").filter((l) => /^#{2,3} /.test(l))
1344
1403
  expect(titles).toEqual([
1345
- "## a",
1346
- "## a (2)",
1347
- "## a (3)",
1348
- "## Fixed automatically (2)",
1404
+ "## Findings",
1405
+ "### a",
1406
+ "### a (2)",
1407
+ "### a (3)",
1408
+ "### Fixed automatically (2)",
1349
1409
  "## Fixed automatically",
1350
1410
  ])
1351
1411
  })
@@ -1376,10 +1436,10 @@ describe("composedReview", () => {
1376
1436
  noise: [],
1377
1437
  })
1378
1438
  expect(doc).toContain(
1379
- "## a\n\n- ends in a list\n\nTick exactly one:\n\n- [ ] fix as suggested\n- [ ] won't fix\n- [ ] _your answer_\n\n## Fixed automatically",
1439
+ "### a\n\n- ends in a list\n\nTick exactly one:\n\n- [ ] fix as suggested\n- [ ] won't fix\n- [ ] _your answer_\n\n## Fixed automatically",
1380
1440
  )
1381
1441
  expect(doc.match(/fix as suggested/g)).toHaveLength(1)
1382
- expect(unansweredNotes(doc)).toEqual([{ title: "a", line: 5, reason: "tick exactly one box" }])
1442
+ expect(unansweredNotes(doc)).toEqual([{ title: "a", line: 7, reason: "tick exactly one box" }])
1383
1443
  })
1384
1444
 
1385
1445
  it("leaves out the notes already answered", () => {
@@ -1392,7 +1452,7 @@ describe("composedReview", () => {
1392
1452
  noise: [],
1393
1453
  answered: new Set(["a"]),
1394
1454
  })
1395
- expect(doc).not.toContain("## a\n")
1455
+ expect(doc).not.toContain("# a\n")
1396
1456
  expect(doc).toContain("## a (2)\n\n2")
1397
1457
  })
1398
1458
  })
@@ -1607,3 +1667,17 @@ describe("rankedReview", () => {
1607
1667
  expect(unranked(unranked(review))).toBe(unranked(review))
1608
1668
  })
1609
1669
  })
1670
+
1671
+ describe("impact prompts", () => {
1672
+ it.each([
1673
+ ["triage", () => renderText(() => designTriagePrompt("abc123"))],
1674
+ ["design.impact", () => renderText(() => designImpactPrompt())],
1675
+ ])("[AC-2] the %s prompt asks for a `## Impact` section", (_n, prompt) => {
1676
+ expect(prompt()).toContain("`## Impact`")
1677
+ })
1678
+
1679
+ it("[AC-3] the triage prompt names where a return lap reads the human's Impact edit", () => {
1680
+ const p = renderText(() => designTriagePrompt("abc123")).replace(/\s+/g, " ")
1681
+ expect(p).toContain("`git log -1 -p -- .gtd/REQUIREMENTS.md`")
1682
+ })
1683
+ })