@pmelab/gtd 40.0.0 → 42.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 (58) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +41 -30
  3. package/dist/chunks/{reserved-Qt5FM6Ia.mjs → reserved-D7MNY9QJ.mjs} +66 -18
  4. package/dist/chunks/{steering-DVILanVf.mjs → steering-oCDdQlpP.mjs} +1 -1
  5. package/dist/chunks/tail-D2o2h5cc.mjs +246 -0
  6. package/dist/flows.mjs +11 -1
  7. package/dist/gtd.bundle.mjs +12 -2
  8. package/dist/steering.mjs +1 -1
  9. package/dist/testkit.mjs +15 -3
  10. package/dist/web/main.js +1 -1
  11. package/dist/workflow-kit.mjs +30 -12
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +44 -24
  14. package/dist/workflows/fix.mjs +3 -3
  15. package/dist/workflows/review.mjs +2 -2
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +282 -51
  18. package/package.json +1 -1
  19. package/src/flows/scripts.ts +15 -0
  20. package/src/steering/review.test.ts +2 -20
  21. package/src/steering/review.ts +2 -1
  22. package/src/testkit/FlowScripts.test.ts +17 -0
  23. package/src/testkit/FlowScripts.ts +15 -0
  24. package/src/testkit/steps.ts +27 -0
  25. package/src/workflow-kit/health.ts +42 -13
  26. package/src/workflows/feature/coverage.feature +3 -3
  27. package/src/workflows/feature/fast-path.feature +2 -2
  28. package/src/workflows/feature/feature.test.ts +2 -37
  29. package/src/workflows/feature/feature.ts +12 -14
  30. package/src/workflows/feature/flag-mode.feature +2 -2
  31. package/src/workflows/feature/lens-review.feature +234 -0
  32. package/src/workflows/feature/one-step-land.feature +2 -2
  33. package/src/workflows/feature/quality-unresolved.feature +47 -32
  34. package/src/workflows/feature/refactor-absorbs.feature +3 -3
  35. package/src/workflows/feature/refactor-first.feature +5 -5
  36. package/src/workflows/feature/refactor-rebuild.feature +7 -7
  37. package/src/workflows/feature/refactor-review.feature +19 -100
  38. package/src/workflows/feature/refactor.test.ts +12 -27
  39. package/src/workflows/feature/refactor.ts +0 -1
  40. package/src/workflows/feature/review-gate.feature +6 -6
  41. package/src/workflows/feature/review.test.ts +29 -25
  42. package/src/workflows/feature/review.ts +53 -10
  43. package/src/workflows/feature/seam-rejection.feature +2 -2
  44. package/src/workflows/feature/seam.feature +2 -2
  45. package/src/workflows/feature/seam.test.ts +1 -1
  46. package/src/workflows/feature/slicing.feature +6 -6
  47. package/src/workflows/index.ts +1 -0
  48. package/src/workflows/shared/health.test.ts +30 -0
  49. package/src/workflows/shared/health.ts +12 -9
  50. package/src/workflows/shared/lenses.test.ts +39 -0
  51. package/src/workflows/shared/lenses.ts +176 -0
  52. package/src/workflows/shared/options.ts +1 -0
  53. package/src/workflows/shared/progress.feature +124 -0
  54. package/src/workflows/shared/tail.ts +19 -4
  55. package/src/workflows/shared/text.test.ts +68 -1
  56. package/src/workflows/shared/text.ts +126 -13
  57. package/src/workflows/shared/vars.ts +1 -0
  58. package/dist/chunks/tail-tAHx_AHe.mjs +0 -112
@@ -12,6 +12,7 @@ import {
12
12
  restoreScript,
13
13
  revertScript,
14
14
  showScript,
15
+ writeScript,
15
16
  } from "../flows/index.js"
16
17
  import type { InMemRepo } from "./InMemRepo.js"
17
18
  import type { ScriptedCommand } from "./Layers.js"
@@ -60,6 +61,19 @@ const recognizeMove = (repo: InMemRepo, script: string): FlowScriptResult | unde
60
61
  return ok()
61
62
  }
62
63
 
64
+ const recognizeWrite = (repo: InMemRepo, script: string): FlowScriptResult | undefined => {
65
+ const lines = shellLines(script.split("\n"))
66
+ if (!lines[3]?.startsWith("printf '%s' ")) return undefined
67
+ const [path] = words(lines[2] ?? "")
68
+ const [, content, to] = words(lines[3])
69
+ const remove = words(lines.find((l) => l.startsWith("rm -rf -- ")) ?? "")
70
+ if (path === undefined || content === undefined || to !== path) return undefined
71
+ if (writeScript(path, content, remove) !== script) return undefined
72
+ repo.writeFile(path, content)
73
+ removeAll(repo, remove)
74
+ return ok()
75
+ }
76
+
63
77
  const recognizeShow = (repo: InMemRepo, script: string): FlowScriptResult | undefined => {
64
78
  const line = script.split("\n").find((l) => l.startsWith("git show ")) ?? ""
65
79
  const [source, to] = words(line)
@@ -356,6 +370,7 @@ export const applyFlowScript = (
356
370
  recognizeRemove(repo, script) ??
357
371
  recognizeMove(repo, script) ??
358
372
  recognizeShow(repo, script) ??
373
+ recognizeWrite(repo, script) ??
359
374
  recognizePatch(repo, script) ??
360
375
  recognizeRestore(repo, script) ??
361
376
  recognizeCheck(repo, commands, script)
@@ -127,6 +127,19 @@ Given("the file {string} is deleted", (world: TestkitWorld, path: string) => {
127
127
  world.repo().remove(path)
128
128
  })
129
129
 
130
+ // A human's edit in place, so a scenario need not restate a file gtd wrote.
131
+ Given(
132
+ "the text {string} in {string} is replaced with {string}",
133
+ (world: TestkitWorld, text: string, path: string, replacement: string) => {
134
+ const content = world.repo().read(path) ?? ""
135
+ assert.ok(content.includes(text), `${path}: ${content}`)
136
+ world.repo().write(
137
+ path,
138
+ content.replace(text, () => replacement),
139
+ )
140
+ },
141
+ )
142
+
130
143
  Given("the working tree is committed as {string}", (world: TestkitWorld, message: string) => {
131
144
  world.repo().commit(message)
132
145
  })
@@ -341,6 +354,20 @@ Then("{string} contains {string}", (world: TestkitWorld, path: string, text: str
341
354
  assert.ok(world.repo().read(path)?.includes(text), `${path}: ${world.repo().read(path)}`)
342
355
  })
343
356
 
357
+ Then("{string} contains:", (world: TestkitWorld, path: string, text: string) => {
358
+ assert.ok(world.repo().read(path)?.includes(text), `${path}: ${world.repo().read(path)}`)
359
+ })
360
+
361
+ Then(
362
+ "{string} contains the short hash of the commit {string}",
363
+ (world: TestkitWorld, path: string, subject: string) => {
364
+ const repo = world.repo()
365
+ const hash = repo.hashes()[repo.subjects().lastIndexOf(subject)]
366
+ assert.ok(hash !== undefined, `no commit "${subject}"`)
367
+ assert.ok(repo.read(path)?.includes(hash.slice(0, 7)), `${path}: ${repo.read(path)}`)
368
+ },
369
+ )
370
+
344
371
  Then("{string} does not contain {string}", (world: TestkitWorld, path: string, text: string) => {
345
372
  const content = world.repo().read(path)
346
373
  assert.ok(content !== undefined && !content.includes(text), `${path}: ${content}`)
@@ -32,6 +32,8 @@ export const untilSettled = async (
32
32
  }
33
33
  }
34
34
 
35
+ export type RetryVerdict = "identical" | "progress"
36
+
35
37
  export interface Health {
36
38
  /** One counted run of the suite; resolves `true` when green. */
37
39
  readonly check: () => Promise<boolean>
@@ -39,8 +41,8 @@ export interface Health {
39
41
  readonly report: string
40
42
  readonly fixCap: number
41
43
  readonly fixesSoFar?: number
42
- /** Whether two consecutive red reports are the same failure; `true` escalates at once. */
43
- readonly sameFailure: (previous: string, current: string) => Promise<boolean>
44
+ /** How a red report compares to the previous round's: `identical` escalates at once, `progress` at the cap earns one extra fix. */
45
+ readonly compare: (previous: string, current: string) => Promise<RetryVerdict | undefined>
44
46
  /** Shared with other callers in the same run of red checks. */
45
47
  readonly escalations: EscalationCount
46
48
  readonly escalation: EscalationSteps
@@ -58,13 +60,42 @@ const recheckReport = async (health: Health): Promise<true | string | undefined>
58
60
  return (await health.recheck()) || stripStamp(read(health.report) ?? "")
59
61
  }
60
62
 
63
+ /** Escalate unless the re-check passes: `true` then, else the report the next round compares against. */
64
+ const escalated = async (health: Health, current: string): Promise<true | string> => {
65
+ const rechecked = await recheckReport(health)
66
+ if (rechecked === true) {
67
+ health.escalations.rounds = 0
68
+ return true
69
+ }
70
+ await escalation(health.escalations, health.escalation)
71
+ return rechecked ?? current
72
+ }
73
+
74
+ const nextMove = async (
75
+ health: Health,
76
+ fixes: number,
77
+ cap: number,
78
+ previous: string | undefined,
79
+ current: string,
80
+ ): Promise<"extend" | "escalate" | undefined> => {
81
+ const capped = fixes >= cap
82
+ // Past a raised cap the loop escalates whatever the judge would answer.
83
+ if (capped && cap > health.fixCap) return "escalate"
84
+ const verdict = previous === undefined ? undefined : await health.compare(previous, current)
85
+ if (capped && verdict === "progress") return "extend"
86
+ return verdict === "identical" || capped ? "escalate" : undefined
87
+ }
88
+
61
89
  /**
62
90
  * Loop until the suite is green: check, and on red run `fix` — escalating
63
- * first once `fixCap` fixes are spent, or `sameFailure` calls the failure
64
- * identical to the previous round's, unless `recheck` passes.
91
+ * first once `fixCap` fixes are spent, or `compare` calls the failure
92
+ * identical to the previous round's, unless `recheck` passes. A round that
93
+ * reaches the cap while `compare` calls it progress raises the cap by one fix,
94
+ * once per run of fixes between escalations.
65
95
  */
66
96
  export const healthy = async (fix: () => Promise<void>, health: Health): Promise<void> => {
67
97
  let fixes = health.fixesSoFar ?? 0
98
+ let cap = health.fixCap
68
99
  let previous: string | undefined
69
100
  for (;;) {
70
101
  if (await health.check()) {
@@ -72,17 +103,15 @@ export const healthy = async (fix: () => Promise<void>, health: Health): Promise
72
103
  return
73
104
  }
74
105
  const current = stripStamp(read(health.report) ?? "")
75
- const stuck = previous !== undefined && (await health.sameFailure(previous, current))
106
+ const move = await nextMove(health, fixes, cap, previous, current)
76
107
  previous = current
77
- if (stuck || fixes >= health.fixCap) {
78
- const rechecked = await recheckReport(health)
79
- if (rechecked === true) {
80
- health.escalations.rounds = 0
81
- return
82
- }
83
- previous = rechecked ?? previous
84
- await escalation(health.escalations, health.escalation)
108
+ if (move === "extend") cap++
109
+ else if (move === "escalate") {
110
+ const after = await escalated(health, current)
111
+ if (after === true) return
112
+ previous = after
85
113
  fixes = 0
114
+ cap = health.fixCap
86
115
  }
87
116
  fixes++
88
117
  await fix()
@@ -61,13 +61,13 @@ Feature: a feature is done only when every acceptance criterion has a passing te
61
61
  [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
62
62
  """
63
63
  When the process is driven
64
- Then the process rests at "build.review.reviewing"
64
+ Then the process rests at "build.review.composing"
65
65
  And the commit subjects end with:
66
66
  """
67
67
  gtd(check): implement.applying → implement.health.check
68
68
  gtd(check): implement.health.check → implement.coverage
69
69
  gtd(judge): implement.coverage → implement.closing
70
- gtd(check): implement.closing → build.review.reviewing
70
+ gtd(check): implement.closing → build.review.composing
71
71
  """
72
72
  And the commit "gtd(judge): implement.coverage → implement.closing" has a body containing "Gtd-Mark: covers=AC-1"
73
73
  And the commit "gtd(judge): implement.coverage → implement.closing" has a body containing "Gtd-Mark: covers=AC-2"
@@ -144,7 +144,7 @@ Feature: a feature is done only when every acceptance criterion has a passing te
144
144
  """
145
145
  And step "implement.uncovered" changes nothing
146
146
  When the process is driven
147
- Then the process rests at "build.review.reviewing"
147
+ Then the process rests at "build.review.composing"
148
148
  And the commit "gtd(human): implement.uncovered → implement.closing" has a body containing "Gtd-Mark: covers=AC-1"
149
149
  And the commit "gtd(human): implement.uncovered → implement.closing" has a body containing "Gtd-Mark: uncovered=AC-2"
150
150
 
@@ -64,7 +64,7 @@ Feature: a trivial change takes the same few beats at either risk tier
64
64
  """
65
65
  And step "implement.applying" runs its script
66
66
  When the process is driven
67
- Then the process rests at "build.review.reviewing"
67
+ Then the process rests at "build.review.composing"
68
68
  And "src/greeting.ts" contains "Hello, world"
69
69
  And the commit subjects end with:
70
70
  """
@@ -80,7 +80,7 @@ Feature: a trivial change takes the same few beats at either risk tier
80
80
  gtd(check): implement.health.check → implement.coverage
81
81
  gtd(judge): implement.coverage → implement.closing
82
82
  gtd(check): implement.closing → build.quality.correctness.reviewing
83
- gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
83
+ gtd(agent): build.quality.correctness.reviewing → build.review.composing
84
84
  """
85
85
  And the output of gtd "summary" <summary> "needs a human reviewer"
86
86
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { runsItsScript, testRepo, type Actor } from "../../testkit/index.js"
2
+ import { runsItsScript, testRepo } from "../../testkit/index.js"
3
3
  import { CONFIG_SCAFFOLD } from "../../install/index.js"
4
4
  import { reviewDoc } from "../shared/config.fixture.js"
5
5
 
@@ -75,10 +75,7 @@ describe("afterTail feedback lap", () => {
75
75
  `}`,
76
76
  ].join("\n")
77
77
 
78
- const prompts: string[] = []
79
-
80
78
  const feedbackLap = async () => {
81
- prompts.length = 0
82
79
  const repo = testRepo({
83
80
  workflow: TAIL,
84
81
  env: {
@@ -93,10 +90,7 @@ describe("afterTail feedback lap", () => {
93
90
  await repo.drive({
94
91
  "review-gate.check": () => undefined,
95
92
  "build.health.check": () => undefined,
96
- "build.review.reviewing": (beat, r) => {
97
- prompts.push(beat.content)
98
- r.write(".gtd/REVIEW.md", review())
99
- },
93
+ "build.review.composing": (_, r) => r.write(".gtd/REVIEW.md", review()),
100
94
  })
101
95
  repo.write(".gtd/REVIEW.md", review("also export multiply"))
102
96
  await repo.land()
@@ -137,35 +131,6 @@ describe("afterTail feedback lap", () => {
137
131
  expect(rest.content).not.toContain("write the change fresh")
138
132
  expect(repo.subjects().some((subject) => subject.includes("seam."))).toBe(false)
139
133
  })
140
-
141
- it("the first review says everything since base, the one after feedback only what is new", async () => {
142
- const repo = await feedbackLap()
143
- expect(prompts[0]).toContain("it covers everything since the base")
144
- expect(prompts[0]).not.toContain("only what is new since the previous review")
145
- const writes: Record<string, () => [string, string]> = {
146
- "design.triage": () => [
147
- ".gtd/REQUIREMENTS.md",
148
- "# Requirements\n\n- multiply, designed\n\n## Acceptance Criteria\n\n- AC-1: done\n",
149
- ],
150
- "implement.building": () => ["src/calc.ts", `${CALC}// again\n`],
151
- }
152
- const actors = new Proxy({} as Record<string, Actor>, {
153
- get: (_, state: string): Actor | undefined =>
154
- state === "build.review.await-review" || state === "idle"
155
- ? undefined
156
- : (beat, r) => {
157
- if (state === "implement.coverage") return '[{"id":"AC-1","answer":"yes","p":0.9}]'
158
- if (state === "build.review.reviewing") {
159
- prompts.push(beat.content)
160
- r.write(".gtd/REVIEW.md", review())
161
- } else if (writes[state]) r.write(...writes[state]())
162
- else if (state.endsWith("closing") || state === "re-unwind") r.runScript(beat.content)
163
- },
164
- })
165
- await repo.drive(actors, { maxBeats: 80 })
166
- expect(prompts[1]).toContain("it covers only what is new since the previous review")
167
- expect(prompts[1]).not.toContain("everything since the base")
168
- })
169
134
  })
170
135
 
171
136
  describe("an in-flight 28.0.0 naive-pass process", () => {
@@ -9,7 +9,7 @@ import {
9
9
  slice,
10
10
  start,
11
11
  } from "../../flows/index.js"
12
- import { gate, healthy, type EscalationCount } from "../shared/health.js"
12
+ import { gate, healthy } from "../shared/health.js"
13
13
  import { flagModeOn, shipped } from "../shared/options.js"
14
14
  import { FEEDBACK, fixCheck, QUALITY, REQUIREMENTS } from "../shared/steps.js"
15
15
  import { refusePendingHandOff, tail } from "../shared/tail.js"
@@ -135,8 +135,8 @@ const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> =
135
135
  })
136
136
  }
137
137
 
138
- // The lap's findings the fix cap left must not reach the review, nor the next process's lap,
139
- // which appends to the file. Empty counts as resolved, as the lap reads it.
138
+ // Between slices nothing reviews the lap's leftovers, and the next lap appends to the file.
139
+ // Empty counts as resolved, as the lap reads it.
140
140
  const settleQuality = async (): Promise<void> => {
141
141
  let rested = false
142
142
  while ((read(QUALITY) ?? "").length > 0) {
@@ -151,15 +151,13 @@ const settleQuality = async (): Promise<void> => {
151
151
  if (rested) await healthy(() => fixCheck())
152
152
  }
153
153
 
154
- /** The quality lap, then the human review since `base`. */
155
- const reviewedTail = (base: string, lensBase: string, lap?: "feedback"): Promise<ReviewOutcome> =>
156
- tail(
157
- async (escalations: EscalationCount) => {
158
- await settleQuality()
159
- return review(base, escalations, { lap })
160
- },
161
- { green: true, lensBase },
162
- )
154
+ /** The triaged quality lap, then the human review since `base`. */
155
+ const reviewedTail = (base: string, lensBase: string): Promise<ReviewOutcome> =>
156
+ tail((escalations, report) => review(base, escalations, { report }), {
157
+ green: true,
158
+ lensBase,
159
+ triage: true,
160
+ })
163
161
 
164
162
  // seam.naive / seam.judge never run on a feedback lap: its design → implement is built unjudged
165
163
  // and unrefactored, held to every criterion's coverage since the process start.
@@ -181,7 +179,7 @@ const featureFeedback = async (
181
179
  })
182
180
  const tier = riskTierOn() ? recordRisk() : undefined
183
181
  closed = await implementing({ tier, amend: true })
184
- outcome = await reviewedTail(outcome.base, lensBase, "feedback")
182
+ outcome = await reviewedTail(outcome.base, lensBase)
185
183
  }
186
184
  }
187
185
 
@@ -201,7 +199,7 @@ export const afterTail = async (first: ReviewOutcome): Promise<void> => {
201
199
  await design(outcome.base)
202
200
  const tier = riskTierOn() ? recordRisk() : undefined
203
201
  await implement({ tier, amend: true })
204
- outcome = await buildTail(outcome.base, { green: true, lap: "feedback" })
202
+ outcome = await buildTail(outcome.base, { green: true })
205
203
  }
206
204
  }
207
205
 
@@ -67,7 +67,7 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
67
67
  """
68
68
  And step "build.quality.correctness.reviewing" changes nothing
69
69
  When the process is driven
70
- Then the process rests at "build.review.reviewing"
70
+ Then the process rests at "build.review.composing"
71
71
  And "src/calc.ts" contains "subtract"
72
72
  And "src/calc.test.ts" contains "[AC-1] subtracts"
73
73
  And the commit subjects end with:
@@ -85,5 +85,5 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
85
85
  gtd(check): implement.health.check → implement.coverage
86
86
  gtd(judge): implement.coverage → implement.closing
87
87
  gtd(check): implement.closing → build.quality.correctness.reviewing
88
- gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
88
+ gtd(agent): build.quality.correctness.reviewing → build.review.composing
89
89
  """
@@ -0,0 +1,234 @@
1
+ @inmem
2
+ Feature: the quality lenses are the only agentic review
3
+
4
+ A judge classifies each `## ` finding the lenses wrote to `.gtd/QUALITY.md`:
5
+ a mechanical one is fixed by the fixer, noise is dropped with its `p` kept in
6
+ a trailer, and a design one becomes a note in `.gtd/REVIEW.md`. gtd writes
7
+ that file itself — no reviewer turn runs — and lists, without tick boxes,
8
+ what was fixed automatically and what was dropped as noise.
9
+
10
+ Background:
11
+ Given the shipped workflows, as gtd install scaffolds them
12
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
13
+ And an environment variable "GTD_FLAGMODE" set to "false"
14
+ And a file "src/calc.ts" with:
15
+ """
16
+ export const add = (a: number, b: number) => a + b
17
+ """
18
+ And the working tree is committed as "feat: add calculator"
19
+ And a file "TODO.md" with:
20
+ """
21
+ # Add subtract
22
+
23
+ subtract(a, b) returns the difference.
24
+ """
25
+ And step "unwind" changes nothing
26
+ And step "start-gate.check" changes nothing
27
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
28
+ """
29
+ # Requirements
30
+
31
+ ## Concerns
32
+
33
+ ### Subtract
34
+
35
+ - `subtract(a, b)` returns `a - b`.
36
+
37
+ ## Acceptance Criteria
38
+
39
+ - AC-1: subtract(a, b) returns a - b.
40
+ """
41
+ And step "design.gate.answer" changes nothing
42
+ And step "seam.naive" writes "src/calc.test.ts" with:
43
+ """
44
+ it("[AC-1] subtracts", () => {})
45
+ """
46
+ And step "seam.naive" writes "src/calc.ts" with:
47
+ """
48
+ export const add = (a: number, b: number) => a + b
49
+ export const subtract = (a: number, b: number) => a - b
50
+ """
51
+ And step "seam.check" changes nothing
52
+ And step "implement.applying" runs its script
53
+ And step "implement.health.check" changes nothing
54
+ And step "implement.closing" runs its script
55
+ And judge step "implement.coverage" answers:
56
+ """
57
+ [{"id":"AC-1","answer":"yes","p":0.9}]
58
+ """
59
+ And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
60
+ """
61
+ # Quality
62
+
63
+ ## subtract has no negative test
64
+
65
+ Add a test subtracting a larger number.
66
+
67
+ ## add could be a method
68
+
69
+ A class would read better.
70
+
71
+ ## AC-1 ignores floating point
72
+
73
+ Should subtract round? The criterion does not say.
74
+ """
75
+ And step "build.quality.sorting" runs its script
76
+ And step "build.review.composing" runs its script
77
+
78
+ Scenario: a mechanical finding is fixed and listed, noise dropped and listed, a design one noted
79
+ Given judge step "build.quality.triage" answers:
80
+ """
81
+ [{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"noise","p":0.85},{"id":"Q3","answer":"design","p":0.8}]
82
+ """
83
+ And step "build.fix.quality.fixing" writes "src/calc.test.ts" with:
84
+ """
85
+ it("[AC-1] subtracts", () => {})
86
+ it("subtracts a larger number", () => {})
87
+ """
88
+ And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
89
+ And step "build.health.check" changes nothing
90
+ When the process is driven
91
+ Then the process rests at "build.review.await-review"
92
+ And ".gtd/QUALITY.md" does not exist
93
+ And the commit "gtd(judge): build.quality.triage → build.quality.sorting" has a body containing "Gtd-Mark: noise=Q2 p=0.85 add could be a method"
94
+ And ".gtd/REVIEW.md" contains:
95
+ """
96
+ ## AC-1 ignores floating point
97
+
98
+ Should subtract round? The criterion does not say.
99
+
100
+ ## Fixed automatically
101
+
102
+ - subtract has no negative test (
103
+ """
104
+ And ".gtd/REVIEW.md" contains the short hash of the commit "gtd(agent): build.fix.quality.fixing → build.health.check"
105
+ And ".gtd/REVIEW.md" contains:
106
+ """
107
+ ## Dropped as noise
108
+
109
+ - add could be a method (p=0.85)
110
+ """
111
+ And ".gtd/REVIEW.md" does not contain "[ ]"
112
+ And the commit subjects end with:
113
+ """
114
+ gtd(check): implement.closing → build.quality.correctness.reviewing
115
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
116
+ gtd(judge): build.quality.triage → build.quality.sorting
117
+ gtd(check): build.quality.sorting → build.fix.quality.fixing
118
+ gtd(agent): build.fix.quality.fixing → build.health.check
119
+ gtd(check): build.health.check → build.review.composing
120
+ gtd(check): build.review.composing → build.review.await-review
121
+ """
122
+
123
+ Scenario: an unsure verdict makes a finding a note, never a fix or a drop
124
+ Given judge step "build.quality.triage" answers:
125
+ """
126
+ [{"id":"Q1","answer":"mechanical","p":0.5},{"id":"Q2","answer":"noise","p":0.6}]
127
+ """
128
+ When the process is driven
129
+ Then the process rests at "build.review.await-review"
130
+ And ".gtd/REVIEW.md" contains "## subtract has no negative test"
131
+ And ".gtd/REVIEW.md" contains "## add could be a method"
132
+ And ".gtd/REVIEW.md" contains "## AC-1 ignores floating point"
133
+ And ".gtd/REVIEW.md" does not contain "Fixed automatically"
134
+ And no commit body contains "Gtd-Mark: noise="
135
+ And the commit subjects end with:
136
+ """
137
+ gtd(judge): build.quality.triage → build.quality.sorting
138
+ gtd(check): build.quality.sorting → build.review.composing
139
+ gtd(check): build.review.composing → build.review.await-review
140
+ """
141
+
142
+ Scenario: a mechanical finding the fix cap leaves becomes a note
143
+ Given judge step "build.quality.triage" answers:
144
+ """
145
+ [{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"mechanical","p":0.9},{"id":"Q3","answer":"mechanical","p":0.9}]
146
+ """
147
+ And step "build.fix.quality.fixing" writes ".gtd/QUALITY.md" with:
148
+ """
149
+ # Quality
150
+
151
+ ## add could be a method
152
+
153
+ A class would read better.
154
+ """
155
+ And step "build.health.check" changes nothing
156
+ When the process is driven
157
+ Then the process rests at "build.review.await-review"
158
+ And ".gtd/QUALITY.md" does not exist
159
+ And ".gtd/REVIEW.md" contains:
160
+ """
161
+ ## add could be a method
162
+
163
+ _The fixer left this unresolved._
164
+
165
+ A class would read better.
166
+ """
167
+ And ".gtd/REVIEW.md" contains "- subtract has no negative test ("
168
+ And ".gtd/REVIEW.md" contains "- AC-1 ignores floating point ("
169
+ And the commit subjects end with:
170
+ """
171
+ gtd(judge): build.quality.triage → build.fix.quality.fixing
172
+ gtd(agent): build.fix.quality.fixing
173
+ gtd(agent): build.fix.quality.fixing
174
+ gtd(agent): build.fix.quality.fixing → build.health.check
175
+ gtd(check): build.health.check → build.review.composing
176
+ gtd(check): build.review.composing → build.review.await-review
177
+ """
178
+
179
+ Scenario: a fixer that renames a chunk it leaves counts nothing fixed
180
+ Given judge step "build.quality.triage" answers:
181
+ """
182
+ [{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"mechanical","p":0.9},{"id":"Q3","answer":"mechanical","p":0.9}]
183
+ """
184
+ And step "build.fix.quality.fixing" writes ".gtd/QUALITY.md" with:
185
+ """
186
+ # Quality
187
+
188
+ ## null check in parse()
189
+
190
+ Still open.
191
+ """
192
+ And step "build.health.check" changes nothing
193
+ When the process is driven
194
+ Then the process rests at "build.review.await-review"
195
+ And ".gtd/REVIEW.md" contains "## null check in parse()"
196
+ And ".gtd/REVIEW.md" contains "## subtract has no negative test"
197
+ And ".gtd/REVIEW.md" contains "check whether it was fixed"
198
+ And ".gtd/REVIEW.md" does not contain "Fixed automatically"
199
+
200
+ Scenario: prose the fixer leaves outside every chunk does not hide its fixes
201
+ Given judge step "build.quality.triage" answers:
202
+ """
203
+ [{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"mechanical","p":0.9},{"id":"Q3","answer":"mechanical","p":0.9}]
204
+ """
205
+ And step "build.fix.quality.fixing" writes ".gtd/QUALITY.md" with:
206
+ """
207
+ # Quality
208
+
209
+ All fixed.
210
+ """
211
+ And step "build.health.check" changes nothing
212
+ When the process is driven
213
+ Then the process rests at "build.review.await-review"
214
+ And ".gtd/REVIEW.md" contains "- subtract has no negative test ("
215
+ And ".gtd/REVIEW.md" does not contain "check whether it was fixed"
216
+
217
+ Scenario: signing off a composed review ends the process
218
+ Given judge step "build.quality.triage" answers:
219
+ """
220
+ [{"id":"Q1","answer":"noise","p":0.9},{"id":"Q2","answer":"noise","p":0.9},{"id":"Q3","answer":"noise","p":0.9}]
221
+ """
222
+ And step "build.review.closing" runs its script
223
+ When the process is driven
224
+ Then the process rests at "build.review.await-review"
225
+ When gtd lands
226
+ And the process is driven
227
+ Then the process rests at "idle"
228
+ And ".gtd/REVIEW.md" does not exist
229
+ And the commit subjects end with:
230
+ """
231
+ gtd(check): build.review.composing → build.review.await-review
232
+ gtd(human): build.review.await-review → build.review.closing
233
+ gtd(check): build.review.closing → idle
234
+ """
@@ -64,7 +64,7 @@ Feature: a naive pass the judge rules land is itself the implementation
64
64
 
65
65
  Scenario: a land judgment applies the naive pass and runs from idle to the review gate with no rebuild
66
66
  When the process is driven
67
- Then the process rests at "build.review.reviewing"
67
+ Then the process rests at "build.review.composing"
68
68
  And "src/greeting.ts" contains "Hello, world"
69
69
  And "src/acceptance.test.ts" contains "[AC-1] holds"
70
70
  And the commit subjects end with:
@@ -81,7 +81,7 @@ Feature: a naive pass the judge rules land is itself the implementation
81
81
  gtd(check): implement.health.check → implement.coverage
82
82
  gtd(judge): implement.coverage → implement.closing
83
83
  gtd(check): implement.closing → build.quality.correctness.reviewing
84
- gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
84
+ gtd(agent): build.quality.correctness.reviewing → build.review.composing
85
85
  """
86
86
  And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
87
87
  And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"