@pmelab/gtd 45.2.0 → 47.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -30
  3. package/dist/chunks/{reserved-FWrjy7zo.mjs → reserved-DF1HD6jL.mjs} +42 -48
  4. package/dist/chunks/{review-Dhl4vvDe.mjs → review-Bv40kZu9.mjs} +9 -23
  5. package/dist/chunks/{tail-B-Es6Hwj.mjs → tail-D89fU_wf.mjs} +68 -39
  6. package/dist/gtd.bundle.mjs +5 -7
  7. package/dist/testkit.mjs +5 -7
  8. package/dist/workflows/cleanup.mjs +2 -2
  9. package/dist/workflows/feature.mjs +26 -22
  10. package/dist/workflows/fix.mjs +2 -2
  11. package/dist/workflows/review.mjs +14 -4
  12. package/dist/workflows/rollout.mjs +1 -1
  13. package/dist/workflows.mjs +270 -429
  14. package/package.json +1 -6
  15. package/src/install/ConfigScaffold.ts +1 -2
  16. package/src/install/Install.ts +4 -5
  17. package/src/workflows/cleanup/cleanup.test.ts +11 -9
  18. package/src/workflows/feature/feature.ts +38 -28
  19. package/src/workflows/feature/lens-review.feature +3 -1
  20. package/src/workflows/feature/slice-findings.feature +198 -0
  21. package/src/workflows/fix/fix-lens-origin.feature +101 -0
  22. package/src/workflows/fix/fix-quality.feature +131 -0
  23. package/src/workflows/index.ts +1 -3
  24. package/src/workflows/review/review.ts +15 -3
  25. package/src/workflows/shared/lenses.ts +30 -9
  26. package/src/workflows/shared/options.ts +1 -21
  27. package/src/workflows/shared/review.ts +5 -27
  28. package/src/workflows/shared/skills.test.ts +0 -2
  29. package/src/workflows/shared/skills.ts +0 -3
  30. package/src/workflows/shared/tail.ts +74 -57
  31. package/src/workflows/shared/text.ts +86 -33
  32. package/src/workflows/shared/vars.ts +0 -8
  33. package/dist/workflows/tests.mjs +0 -193
  34. package/src/workflows/feature/quality-unresolved.feature +0 -121
  35. package/src/workflows/tests/index.ts +0 -6
  36. package/src/workflows/tests/tests-options.feature +0 -52
  37. package/src/workflows/tests/tests.feature +0 -266
  38. package/src/workflows/tests/tests.test.ts +0 -51
  39. package/src/workflows/tests/tests.ts +0 -270
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "45.2.0",
3
+ "version": "47.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -62,11 +62,6 @@
62
62
  "source": "./src/workflows/cleanup/index.ts",
63
63
  "default": "./dist/workflows/cleanup.mjs"
64
64
  },
65
- "./workflows/tests": {
66
- "types": "./src/workflows/tests/index.ts",
67
- "source": "./src/workflows/tests/index.ts",
68
- "default": "./dist/workflows/tests.mjs"
69
- },
70
65
  "./testkit": {
71
66
  "types": "./src/testkit/index.ts",
72
67
  "source": "./src/testkit/index.ts",
@@ -6,8 +6,7 @@ export const CONFIG_SCAFFOLD = `// The workflows gtd runs here: gtd's shipped fe
6
6
  // its own doors: \`gtd door review [base]\` and \`gtd door fix\`. Export \`rollout\`
7
7
  // from \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
8
8
  // \`@pmelab/gtd/workflows/cleanup\` to add \`gtd door rollout <flag>\` and
9
- // \`gtd door cleanup <flag>\`, and \`tests\` from \`@pmelab/gtd/workflows/tests\`
10
- // to add \`gtd door tests\`.
9
+ // \`gtd door cleanup <flag>\`.
11
10
  export {
12
11
  feature as default,
13
12
  feature,
@@ -516,11 +516,10 @@ rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
516
516
  straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
517
517
  \`gtd doors\` lists the doors a repository offers (those of the workflows its
518
518
  config exports — the bundled \`fix\` and \`review [base]\`, plus the opt-in
519
- \`rollout <flag>\` (turn a release flag default-on), \`cleanup <flag>\`
520
- (remove it) and \`tests\` (revive skipped tests), which a config adds by
521
- exporting \`rollout\` from \`@pmelab/gtd/workflows/rollout\`, \`cleanup\` from
522
- \`@pmelab/gtd/workflows/cleanup\` and \`tests\` from
523
- \`@pmelab/gtd/workflows/tests\` — plus any it declares). Refuses with
519
+ \`rollout <flag>\` (turn a release flag default-on) and \`cleanup <flag>\`
520
+ (remove it), which a config adds by exporting \`rollout\` from
521
+ \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
522
+ \`@pmelab/gtd/workflows/cleanup\` — plus any it declares). Refuses with
524
523
  \`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
525
524
  missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
526
525
 
@@ -90,6 +90,13 @@ describe("the cleanup guard over every later agent turn", () => {
90
90
  "build.health.check": () => undefined,
91
91
  }
92
92
 
93
+ // One lens finding, judged mechanical, so it reaches the fix turn.
94
+ const MECHANICAL: Actors = {
95
+ "build.quality.correctness.reviewing": (_, r) =>
96
+ r.write(".gtd/QUALITY.md", "# Quality\n\n## a finding\n\nFix it.\n"),
97
+ "build.quality.triage": () => '[{"id":"Q1","answer":"mechanical","p":0.9}]',
98
+ }
99
+
93
100
  // Without the shipped access rules, which would refuse a source write from a read-only turn first: the guard is the second line.
94
101
  const NO_ACCESS = CONFIG.replace(" access,\n", "")
95
102
 
@@ -185,15 +192,11 @@ describe("the cleanup guard over every later agent turn", () => {
185
192
  ).rejects.toThrow("src/b.ts:1 still names GTD_FLAG_LIVE")
186
193
  })
187
194
 
188
- it("refuses the first fix after escalating quality findings that re-adds the env var", async () => {
195
+ it("refuses a quality fix turn that re-adds the env var", async () => {
189
196
  await expect(
190
197
  driven(guarded("correctness", NO_ACCESS), {
191
- "build.quality.correctness.reviewing": (_, r) =>
192
- r.write(".gtd/QUALITY.md", "- a finding\n"),
193
- "build.fix.quality.fixing": () => undefined,
194
- "build.health.describe": () => undefined,
195
- "build.health.stop": () => undefined,
196
- "build.fix": (_, r) => r.write("src/b.ts", NAMED),
198
+ ...MECHANICAL,
199
+ "build.fix.quality.fixing": (_, r) => r.write("src/b.ts", NAMED),
197
200
  }),
198
201
  ).rejects.toThrow("src/b.ts:1 still names GTD_FLAG_LIVE")
199
202
  })
@@ -209,8 +212,7 @@ describe("the cleanup guard over every later agent turn", () => {
209
212
  it("refuses a quality fix turn that restores the registry entry", async () => {
210
213
  await expect(
211
214
  driven(guarded("correctness"), {
212
- "build.quality.correctness.reviewing": (_, r) =>
213
- r.write(".gtd/QUALITY.md", "- a finding\n"),
215
+ ...MECHANICAL,
214
216
  "build.fix.quality.fixing": (_, r) => r.write("src/flags/index.ts", REGISTRY),
215
217
  }),
216
218
  ).rejects.toThrow('still declares flag "live"')
@@ -9,9 +9,10 @@ import {
9
9
  slice,
10
10
  start,
11
11
  } from "../../flows/index.js"
12
- import { gate, healthy } from "../shared/health.js"
12
+ import { gate } from "../shared/health.js"
13
+ import { joinReports, type LensReport } from "../shared/lenses.js"
13
14
  import { flagModeOn, shipped } from "../shared/options.js"
14
- import { FEEDBACK, fixCheck, QUALITY, REQUIREMENTS } from "../shared/steps.js"
15
+ import { FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
15
16
  import { refusePendingHandOff, tail } from "../shared/tail.js"
16
17
  import * as t from "../shared/text.js"
17
18
  import {
@@ -36,7 +37,8 @@ import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
36
37
  // green-baseline gate → design → per slice of the criteria, a naive pass,
37
38
  // judged with `flagMode` on: it lands as the implementation, or (refactoring
38
39
  // first when the judge asks) is built fresh from → the health loop → the
39
- // quality lap → after the last slice, human review of the whole change, which
40
+ // triaged quality lap, whose leftovers are carried on → after the last slice,
41
+ // human review of the whole change and every slice's findings, which
40
42
  // signs off (the episode ends back at `idle`) or sends a design → implement
41
43
  // lap.
42
44
 
@@ -63,10 +65,12 @@ export const buildFeature = async (firstBase: string): Promise<void> => {
63
65
  const one = slices.length === 1
64
66
  // A steer may delete every criterion of a later slice: there is nothing to build there.
65
67
  const built = (i: number): boolean => sliceIds(sliceCriteria(slices, i))?.length !== 0
68
+ const reports: { slice: string; report: LensReport }[] = []
66
69
  for (const i of slices.keys()) {
67
70
  if (!built(i)) continue
68
71
  const n = one ? "" : `-${i + 1}`
69
72
  await buildSlice(i === 0 ? firstBase : head(), {
73
+ number: `${i + 1}`,
70
74
  tier,
71
75
  criteria: sliceCriteria(slices, i),
72
76
  refactor: `refactor${n}`,
@@ -76,6 +80,7 @@ export const buildFeature = async (firstBase: string): Promise<void> => {
76
80
  // Every slice but the one is built unreviewed, so the review covers them all.
77
81
  reviewBase: one ? undefined : firstBase,
78
82
  handedOut,
83
+ laps: reports,
79
84
  })
80
85
  }
81
86
  }
@@ -88,6 +93,8 @@ const gather = (handedOut: Set<string>): readonly string[] => {
88
93
  }
89
94
 
90
95
  interface SliceBuild {
96
+ /** The slice's number, from 1. */
97
+ readonly number: string
91
98
  readonly tier: RiskTier | undefined
92
99
  readonly criteria: CriteriaSlice | undefined
93
100
  readonly refactor: string
@@ -97,6 +104,8 @@ interface SliceBuild {
97
104
  readonly reviewBase?: string | undefined
98
105
  /** Every criterion ID handed out so far; the slice adds those a steer adds. */
99
106
  readonly handedOut: Set<string>
107
+ /** Each earlier slice's lap report, by slice number, for the review after the last one. */
108
+ readonly laps: { slice: string; report: LensReport }[]
100
109
  }
101
110
 
102
111
  /** A naive pass (judged only with `flagMode` on), then implement and the quality lap; after the last slice, the review. */
@@ -127,37 +136,38 @@ const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> =
127
136
  if (more) {
128
137
  // The criteria stay in the tree for the next slice; no quality fix may move them.
129
138
  const guard = () => requireCriteriaFrozen(REQUIREMENTS)
130
- await tail(settleQuality, { green: true, lensBase: base, guard, guardRests: false })
139
+ // No gate between slices: what the lap left waits for the review after the last one.
140
+ const report = await tail(async (_, report) => report, {
141
+ green: true,
142
+ lensBase: base,
143
+ guard,
144
+ guardRests: false,
145
+ })
146
+ build.laps.push({ slice: build.number, report })
131
147
  return
132
148
  }
133
149
  const reviewBase = build.reviewBase ?? base
134
- await featureFeedback(await reviewedTail(reviewBase, base), base, { closed, used })
150
+ const outcome = await reviewedTail(reviewBase, base, { slice: build.number, laps: build.laps })
151
+ await featureFeedback(outcome, base, { closed, used })
135
152
  })
136
153
  }
137
154
 
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
- const settleQuality = async (): Promise<void> => {
141
- let rested = false
142
- while ((read(QUALITY) ?? "").length > 0) {
143
- rested = true
144
- await human("quality-unresolved", {
145
- message: t.qualityUnresolvedMessage(),
146
- label: "Quality findings left unresolved",
147
- file: QUALITY,
148
- })
149
- }
150
- // The person may have fixed code by hand there; nothing else checks it before the review.
151
- if (rested) await healthy(() => fixCheck())
152
- }
153
-
154
- /** The triaged quality lap, then the human review since `base`. */
155
- const reviewedTail = (base: string, lensBase: string): Promise<ReviewOutcome> =>
156
- tail((escalations, report) => humanReview(base, escalations, { report }), {
157
- green: true,
158
- lensBase,
159
- triage: true,
160
- })
155
+ /** The quality lap of slice `last.slice`, then the human review since `base`, listing what earlier slices' `laps` left too. */
156
+ const reviewedTail = (
157
+ base: string,
158
+ lensBase: string,
159
+ last: { readonly slice: string; readonly laps: SliceBuild["laps"] } = { slice: "", laps: [] },
160
+ ): Promise<ReviewOutcome> =>
161
+ tail(
162
+ (escalations, report) =>
163
+ humanReview(base, escalations, {
164
+ report:
165
+ last.laps.length === 0
166
+ ? report
167
+ : joinReports([...last.laps, { slice: last.slice, report }]),
168
+ }),
169
+ { green: true, lensBase },
170
+ )
161
171
 
162
172
  // seam.naive / seam.judge never run on a feedback lap: its design → implement is built unjudged
163
173
  // and unrefactored, held to every criterion's coverage since the process start.
@@ -74,6 +74,7 @@ Feature: the quality lenses are the only agentic review
74
74
  Should subtract round? The criterion does not say.
75
75
  """
76
76
  And step "build.quality.sorting" runs its script
77
+ And step "build.quality.closing" runs its script
77
78
  And step "build.review.composing" runs its script
78
79
 
79
80
  Scenario: a mechanical finding is fixed and listed, noise dropped and listed, a design one noted
@@ -184,7 +185,8 @@ Feature: the quality lenses are the only agentic review
184
185
  gtd(agent): build.fix.quality.fixing
185
186
  gtd(agent): build.fix.quality.fixing
186
187
  gtd(agent): build.fix.quality.fixing → build.health.check
187
- gtd(check): build.health.check → build.review.composing
188
+ gtd(check): build.health.check → build.quality.closing
189
+ gtd(check): build.quality.closing → build.review.composing
188
190
  gtd(check): build.review.composing → build.review.await-review
189
191
  """
190
192
 
@@ -0,0 +1,198 @@
1
+ @inmem
2
+ Feature: every slice's quality lap is triaged, and its leftovers wait for the review
3
+
4
+ A judge sorts each slice's findings like the last slice's (see
5
+ `lens-review.feature`): mechanical ones are fixed, noise is dropped, design
6
+ ones are never sent to the fixer. Nothing between slices asks a person
7
+ anything — what a lap leaves is carried to the review after the last slice,
8
+ each finding naming its slice and lens.
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 an environment variable "GTD_SLICEMAXCRITERIA" set to "1"
15
+ And a file "src/calc.ts" with:
16
+ """
17
+ export const add = (a: number, b: number) => a + b
18
+ """
19
+ And the working tree is committed as "feat: add calculator"
20
+ And a file "TODO.md" with:
21
+ """
22
+ # More arithmetic
23
+
24
+ subtract and multiply.
25
+ """
26
+ And step "unwind" changes nothing
27
+ And step "start-gate.check" changes nothing
28
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
29
+ """
30
+ # Requirements
31
+
32
+ ## Acceptance Criteria
33
+
34
+ - AC-1: subtract(a, b) returns a - b.
35
+ - AC-2: multiply(a, b) returns a * b.
36
+ """
37
+ And step "design.gate.answer" changes nothing
38
+ And step "design.slicing" writes ".gtd/SLICES.md" with:
39
+ """
40
+ # Slices
41
+
42
+ ## Subtract
43
+
44
+ - AC-1
45
+
46
+ ## Multiply
47
+
48
+ - AC-2
49
+ """
50
+ And step "seam.naive", when prompted with "This slice builds AC-1 only", writes "src/sub.test.ts" with:
51
+ """
52
+ it("[AC-1] subtracts", () => {})
53
+ """
54
+ And step "seam.naive", when prompted with "This slice builds AC-2 only", writes "src/mul.test.ts" with:
55
+ """
56
+ it("[AC-2] multiplies", () => {})
57
+ """
58
+ And step "seam.check" changes nothing
59
+ And step "implement.applying" runs its script
60
+ And step "implement.health.check" changes nothing
61
+ And step "implement.closing" runs its script
62
+ And judge step "implement.coverage", when its questions contain "AC-1", answers:
63
+ """
64
+ [{"id":"AC-1","answer":"yes","p":0.9}]
65
+ """
66
+ And judge step "implement.coverage", when its questions contain "AC-2", answers:
67
+ """
68
+ [{"id":"AC-2","answer":"yes","p":0.9}]
69
+ """
70
+ And step "build.quality.sorting" runs its script
71
+ And step "build.quality.closing" runs its script
72
+ And step "build.health.check" changes nothing
73
+ And step "build.review.composing" runs its script
74
+
75
+ Scenario: a finding questioning a criterion reaches the final review, never the fixer
76
+ Given step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
77
+ """
78
+ # Quality
79
+
80
+ ## AC-1 ignores floating point
81
+
82
+ Should subtract round? The criterion does not say.
83
+ """
84
+ And judge step "build.quality.triage" answers:
85
+ """
86
+ [{"id":"Q1","answer":"design","p":0.9}]
87
+ """
88
+ When the process is driven
89
+ Then the process rests at "build.review.await-review"
90
+ And no commit body contains "build.fix.quality.fixing"
91
+ And ".gtd/QUALITY.md" does not exist
92
+ And ".gtd/REVIEW.md" contains:
93
+ """
94
+ ## AC-1 ignores floating point
95
+
96
+ _From slice 1, lens correctness._
97
+
98
+ Should subtract round? The criterion does not say.
99
+ """
100
+ And ".gtd/REVIEW.md" contains:
101
+ """
102
+ ## AC-1 ignores floating point (2)
103
+
104
+ _From slice 2, lens correctness._
105
+ """
106
+ And the commit subjects end with:
107
+ """
108
+ gtd(check): implement.closing → build.quality.correctness.reviewing
109
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
110
+ gtd(judge): build.quality.triage → build.quality.sorting
111
+ gtd(check): build.quality.sorting → seam.naive
112
+ gtd(agent): seam.naive → seam.check
113
+ gtd(check): seam.check → implement.applying
114
+ gtd(check): implement.applying → implement.health.check
115
+ gtd(check): implement.health.check → implement.coverage
116
+ gtd(judge): implement.coverage → implement.closing
117
+ gtd(check): implement.closing → build.quality.correctness.reviewing
118
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
119
+ gtd(judge): build.quality.triage → build.quality.sorting
120
+ gtd(check): build.quality.sorting → build.review.composing
121
+ gtd(check): build.review.composing → build.review.await-review
122
+ """
123
+
124
+ Scenario: a mechanical finding the fix cap leaves in an early slice is carried, never escalated
125
+ Given step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
126
+ """
127
+ # Quality
128
+
129
+ ## subtract has no negative test
130
+
131
+ Add a test subtracting a larger number.
132
+ """
133
+ And judge step "build.quality.triage" answers:
134
+ """
135
+ [{"id":"Q1","answer":"mechanical","p":0.9}]
136
+ """
137
+ And step "build.fix.quality.fixing" changes nothing
138
+ When the process is driven
139
+ Then the process rests at "build.review.await-review"
140
+ And no commit body contains "build.health.describe"
141
+ And ".gtd/REVIEW.md" contains:
142
+ """
143
+ ## subtract has no negative test
144
+
145
+ _From slice 1, lens correctness._
146
+
147
+ _The fixer left this unresolved._
148
+ """
149
+ And the commit subjects end with:
150
+ """
151
+ gtd(check): implement.closing → build.quality.correctness.reviewing
152
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
153
+ gtd(judge): build.quality.triage → build.fix.quality.fixing
154
+ gtd(agent): build.fix.quality.fixing
155
+ gtd(agent): build.fix.quality.fixing
156
+ gtd(agent): build.fix.quality.fixing → build.health.check
157
+ gtd(check): build.health.check → build.quality.closing
158
+ gtd(check): build.quality.closing → seam.naive
159
+ gtd(agent): seam.naive → seam.check
160
+ gtd(check): seam.check → implement.applying
161
+ gtd(check): implement.applying → implement.health.check
162
+ gtd(check): implement.health.check → implement.coverage
163
+ gtd(judge): implement.coverage → implement.closing
164
+ gtd(check): implement.closing → build.quality.correctness.reviewing
165
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
166
+ gtd(judge): build.quality.triage → build.fix.quality.fixing
167
+ gtd(agent): build.fix.quality.fixing
168
+ gtd(agent): build.fix.quality.fixing
169
+ gtd(agent): build.fix.quality.fixing → build.health.check
170
+ gtd(check): build.health.check → build.quality.closing
171
+ gtd(check): build.quality.closing → build.review.composing
172
+ gtd(check): build.review.composing → build.review.await-review
173
+ """
174
+
175
+ Scenario: the final review lists every slice's fixes with their commits
176
+ Given step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
177
+ """
178
+ # Quality
179
+
180
+ ## subtract has no negative test
181
+
182
+ Add a test subtracting a larger number.
183
+ """
184
+ And judge step "build.quality.triage" answers:
185
+ """
186
+ [{"id":"Q1","answer":"mechanical","p":0.9}]
187
+ """
188
+ And step "build.fix.quality.fixing" writes "src/edge.test.ts" with:
189
+ """
190
+ it("subtracts a larger number", () => {})
191
+ """
192
+ And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
193
+ When the process is driven
194
+ Then the process rests at "build.review.await-review"
195
+ And ".gtd/REVIEW.md" contains ", slice 1, lens correctness)"
196
+ And ".gtd/REVIEW.md" contains ", slice 2, lens correctness)"
197
+ And ".gtd/REVIEW.md" contains the short hash of the commit "gtd(agent): build.fix.quality.fixing → build.health.check"
198
+ And no commit body contains "build.quality.closing"
@@ -0,0 +1,101 @@
1
+ @inmem
2
+ Feature: each finding the reviewer is handed names the lens that wrote it
3
+
4
+ Lenses append to `.gtd/QUALITY.md` in turn, so a finding belongs to the lens
5
+ whose turn added it — even when two lenses give findings the same title,
6
+ which the review then tells apart by a numbered suffix.
7
+
8
+ Background:
9
+ Given the shipped workflows, as gtd install scaffolds them
10
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness,tests"
11
+ And a file "src/calc.ts" with:
12
+ """
13
+ export const add = (a: number, b: number) => a + b
14
+ """
15
+ And the working tree is committed as "feat: add calculator"
16
+ And a file ".gtd/REVIEW.orig.md" with:
17
+ """
18
+ # Review: abc1234
19
+
20
+ <!-- base: 0000000 -->
21
+
22
+ ## calc
23
+
24
+ - [ ] ./src/calc.ts#1-1
25
+ new add function
26
+ """
27
+ And a file ".gtd/REVIEW.md" with:
28
+ """
29
+ # Review: abc1234
30
+
31
+ <!-- base: 0000000 -->
32
+
33
+ ## calc
34
+
35
+ - [ ] ./src/calc.ts#1-1
36
+ new add function — rename it to sum
37
+ """
38
+ And the working tree is committed as "chore: hand off a review"
39
+ And step "notes.fixing" writes "src/calc.ts" with:
40
+ """
41
+ export const sum = (a: number, b: number) => a + b
42
+ """
43
+ And step "notes.closing" runs its script
44
+ And step "build.health.check" changes nothing
45
+ And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
46
+ """
47
+ # Quality
48
+
49
+ ## sum is untested
50
+
51
+ From correctness.
52
+ """
53
+ And step "build.quality.tests.reviewing" writes ".gtd/QUALITY.md" with:
54
+ """
55
+ # Quality
56
+
57
+ ## sum is untested
58
+
59
+ From correctness.
60
+
61
+ ## sum is untested
62
+
63
+ From tests.
64
+ """
65
+ And step "build.quality.sorting" runs its script
66
+ And step "build.quality.closing" runs its script
67
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
68
+ """
69
+ # Review: abc1234
70
+
71
+ <!-- base: 0000000 -->
72
+
73
+ ## calc
74
+
75
+ - [ ] ./src/calc.ts#1-1
76
+ renamed to sum
77
+ """
78
+ And step "build.review.carrying" runs its script
79
+
80
+ Scenario: two lenses' findings with one title each name their own lens
81
+ Given judge step "build.quality.triage" answers:
82
+ """
83
+ [{"id":"Q1","answer":"design","p":0.9},{"id":"Q2","answer":"design","p":0.9}]
84
+ """
85
+ When gtd opens door "fix"
86
+ And the process is driven
87
+ Then the process rests at "build.review.await-review"
88
+ And ".gtd/REVIEW.md" contains:
89
+ """
90
+ ## sum is untested
91
+
92
+ _From lens correctness._
93
+
94
+ From correctness.
95
+
96
+ ## sum is untested (2)
97
+
98
+ _From lens tests._
99
+
100
+ From tests.
101
+ """
@@ -0,0 +1,131 @@
1
+ @inmem
2
+ Feature: fix's quality lap is triaged, and its leftovers go to the hand-off review
3
+
4
+ A judge sorts the lap's findings: mechanical ones are fixed for up to three
5
+ turns, noise is dropped, and design ones — with whatever the fix turns left —
6
+ are added to the reviewer's `.gtd/REVIEW.md` by gtd, never to a person's
7
+ gate before the review.
8
+
9
+ Background:
10
+ Given the shipped workflows, as gtd install scaffolds them
11
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
12
+ And a file "src/calc.ts" with:
13
+ """
14
+ export const add = (a: number, b: number) => a + b
15
+ """
16
+ And the working tree is committed as "feat: add calculator"
17
+ And a file ".gtd/REVIEW.orig.md" with:
18
+ """
19
+ # Review: abc1234
20
+
21
+ <!-- base: 0000000 -->
22
+
23
+ ## calc
24
+
25
+ - [ ] ./src/calc.ts#1-1
26
+ new add function
27
+ """
28
+ And a file ".gtd/REVIEW.md" with:
29
+ """
30
+ # Review: abc1234
31
+
32
+ <!-- base: 0000000 -->
33
+
34
+ ## calc
35
+
36
+ - [ ] ./src/calc.ts#1-1
37
+ new add function — rename it to sum
38
+ """
39
+ And the working tree is committed as "chore: hand off a review"
40
+ And step "notes.fixing" writes "src/calc.ts" with:
41
+ """
42
+ export const sum = (a: number, b: number) => a + b
43
+ """
44
+ And step "notes.closing" runs its script
45
+ And step "build.health.check" changes nothing
46
+ And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
47
+ """
48
+ # Quality
49
+
50
+ ## sum has no negative test
51
+
52
+ Add a test summing negative numbers.
53
+
54
+ ## sum should be named plus
55
+
56
+ The glossary says plus.
57
+ """
58
+ And step "build.quality.sorting" runs its script
59
+ And step "build.quality.closing" runs its script
60
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
61
+ """
62
+ # Review: abc1234
63
+
64
+ <!-- base: 0000000 -->
65
+
66
+ ## calc
67
+
68
+ - [ ] ./src/calc.ts#1-1
69
+ renamed to sum
70
+ """
71
+ And step "build.review.carrying" runs its script
72
+
73
+ Scenario: a design finding and one the fix cap left reach the reviewer, never an escalation
74
+ Given judge step "build.quality.triage" answers:
75
+ """
76
+ [{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"design","p":0.9}]
77
+ """
78
+ And step "build.fix.quality.fixing" changes nothing
79
+ When gtd opens door "fix"
80
+ And the process is driven
81
+ Then the process rests at "build.review.await-review"
82
+ And ".gtd/QUALITY.md" does not exist
83
+ And no commit body contains "build.health.describe"
84
+ And ".gtd/REVIEW.md" contains:
85
+ """
86
+ ## sum has no negative test
87
+
88
+ _From lens correctness._
89
+
90
+ _The fixer left this unresolved._
91
+ """
92
+ And ".gtd/REVIEW.md" contains:
93
+ """
94
+ ## sum should be named plus
95
+
96
+ _From lens correctness._
97
+
98
+ The glossary says plus.
99
+ """
100
+ And the commit subjects end with:
101
+ """
102
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
103
+ gtd(judge): build.quality.triage → build.quality.sorting
104
+ gtd(check): build.quality.sorting → build.fix.quality.fixing
105
+ gtd(agent): build.fix.quality.fixing
106
+ gtd(agent): build.fix.quality.fixing
107
+ gtd(agent): build.fix.quality.fixing → build.health.check
108
+ gtd(check): build.health.check → build.quality.closing
109
+ gtd(check): build.quality.closing → build.review.reviewing
110
+ gtd(agent): build.review.reviewing → build.review.carrying
111
+ gtd(check): build.review.carrying → build.review.await-review
112
+ """
113
+
114
+ Scenario: a fixed finding and a noise one are listed in the review
115
+ Given judge step "build.quality.triage" answers:
116
+ """
117
+ [{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"noise","p":0.9}]
118
+ """
119
+ And step "build.fix.quality.fixing" writes "src/calc.test.ts" with:
120
+ """
121
+ it("sums negative numbers", () => {})
122
+ """
123
+ And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
124
+ When gtd opens door "fix"
125
+ And the process is driven
126
+ Then the process rests at "build.review.await-review"
127
+ And no commit body contains "build.quality.closing"
128
+ And ".gtd/REVIEW.md" contains "## Fixed automatically"
129
+ And ".gtd/REVIEW.md" contains "- sum has no negative test ("
130
+ And ".gtd/REVIEW.md" contains the short hash of the commit "gtd(agent): build.fix.quality.fixing → build.health.check"
131
+ And ".gtd/REVIEW.md" contains "- sum should be named plus (p=0.9, lens correctness)"