@pmelab/gtd 42.0.0 → 44.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 (62) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +96 -48
  3. package/claude/hooks/sketch.ts +1 -1
  4. package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-DJk_6-ju.mjs} +75 -46
  5. package/dist/chunks/review-X7Jfz0dY.mjs +249 -0
  6. package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
  7. package/dist/chunks/{tail-D2o2h5cc.mjs → tail-5w5mxGTq.mjs} +3 -3
  8. package/dist/gtd.bundle.mjs +488 -30
  9. package/dist/steering.mjs +2 -2
  10. package/dist/testkit.mjs +447 -27
  11. package/dist/web/main.js +42 -13
  12. package/dist/workflow-kit.mjs +61 -25
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +7 -290
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +27 -7
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +287 -161
  19. package/package.json +1 -1
  20. package/src/cli/Cli.test.ts +33 -1
  21. package/src/cli/Cli.ts +70 -6
  22. package/src/cli/History.ts +15 -0
  23. package/src/cli/Judges.test.ts +373 -0
  24. package/src/cli/Judges.ts +624 -0
  25. package/src/cli/Metrics.ts +3 -16
  26. package/src/cli/Program.test.ts +3 -1
  27. package/src/cli/Program.ts +9 -0
  28. package/src/steering/FORMATS.md +4 -4
  29. package/src/steering/index.ts +12 -1
  30. package/src/steering/review.test.ts +107 -0
  31. package/src/steering/review.ts +159 -9
  32. package/src/testkit/EmittedScriptRecognizer.ts +0 -14
  33. package/src/testkit/steps.ts +4 -0
  34. package/src/workflow-kit/review.test.ts +170 -26
  35. package/src/workflow-kit/review.ts +95 -19
  36. package/src/workflows/feature/coverage.ts +1 -1
  37. package/src/workflows/feature/feature.test.ts +8 -0
  38. package/src/workflows/feature/feature.ts +2 -2
  39. package/src/workflows/feature/index.ts +1 -1
  40. package/src/workflows/feature/lens-review.feature +16 -4
  41. package/src/workflows/feature/refactor.ts +1 -1
  42. package/src/workflows/feature/review-answers.feature +179 -0
  43. package/src/workflows/feature/seam.ts +1 -1
  44. package/src/workflows/feature/steps.test.ts +2 -76
  45. package/src/workflows/feature/steps.ts +5 -48
  46. package/src/workflows/fix/fix.feature +20 -21
  47. package/src/workflows/index.ts +1 -1
  48. package/src/workflows/review/review-door.feature +149 -0
  49. package/src/workflows/review/review.test.ts +1 -73
  50. package/src/workflows/review/review.ts +34 -6
  51. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
  52. package/src/workflows/shared/access.test.ts +2 -11
  53. package/src/workflows/shared/access.ts +0 -2
  54. package/src/workflows/{feature → shared}/review.test.ts +9 -61
  55. package/src/workflows/{feature → shared}/review.ts +40 -48
  56. package/src/workflows/shared/skills.test.ts +2 -0
  57. package/src/workflows/shared/skills.ts +2 -0
  58. package/src/workflows/shared/steps.test.ts +64 -2
  59. package/src/workflows/shared/steps.ts +40 -1
  60. package/src/workflows/shared/tail.ts +2 -2
  61. package/src/workflows/shared/text.test.ts +40 -89
  62. package/src/workflows/shared/text.ts +63 -54
@@ -0,0 +1,179 @@
1
+ @inmem
2
+ Feature: every note in the review is answered with one tick
3
+
4
+ Each note gtd writes to `.gtd/REVIEW.md` ends in three boxes — fix as
5
+ suggested, won't fix, or the human's own answer written over the
6
+ placeholder. The gate refuses to land until every note holds exactly one
7
+ tick. A fix goes to the nit batch, a won't-fix is dropped and recorded in
8
+ the gate's commit, an own answer is judged like any other note. Nothing is
9
+ deferred to `.gtd/TODO.md`.
10
+
11
+ Background:
12
+ Given the shipped workflows, as gtd install scaffolds them
13
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
14
+ And an environment variable "GTD_FLAGMODE" set to "false"
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
+ # Add subtract
23
+
24
+ subtract(a, b) returns the difference.
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
+ ## Concerns
33
+
34
+ ### Subtract
35
+
36
+ - `subtract(a, b)` returns `a - b`.
37
+
38
+ ## Acceptance Criteria
39
+
40
+ - AC-1: subtract(a, b) returns a - b.
41
+ """
42
+ And step "design.gate.answer" changes nothing
43
+ And step "seam.naive" writes "src/calc.test.ts" with:
44
+ """
45
+ it("[AC-1] subtracts", () => {})
46
+ """
47
+ And step "seam.naive" writes "src/calc.ts" with:
48
+ """
49
+ export const add = (a: number, b: number) => a + b
50
+ export const subtract = (a: number, b: number) => a - b
51
+ """
52
+ And step "seam.check" changes nothing
53
+ And step "implement.applying" runs its script
54
+ And step "implement.health.check" changes nothing
55
+ And step "implement.closing" runs its script
56
+ And judge step "implement.coverage" answers:
57
+ """
58
+ [{"id":"AC-1","answer":"yes","p":0.9}]
59
+ """
60
+ And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
61
+ """
62
+ # Quality
63
+
64
+ ## subtract has no negative test
65
+
66
+ Add a test subtracting a larger number.
67
+
68
+ ## add could be a method
69
+
70
+ A class would read better.
71
+
72
+ ## AC-1 ignores floating point
73
+
74
+ Should subtract round? The criterion does not say.
75
+ """
76
+ And step "build.quality.sorting" runs its script
77
+ And step "build.review.composing" runs its script
78
+ And judge step "build.quality.triage" answers:
79
+ """
80
+ [{"id":"Q1","answer":"noise","p":0.9},{"id":"Q2","answer":"noise","p":0.9},{"id":"Q3","answer":"design","p":0.9}]
81
+ """
82
+ And step "build.review.closing" runs its script
83
+
84
+ Scenario: the note ends in its three answer boxes
85
+ When the process is driven
86
+ Then the process rests at "build.review.await-review"
87
+ And ".gtd/REVIEW.md" contains:
88
+ """
89
+ ## AC-1 ignores floating point
90
+
91
+ Should subtract round? The criterion does not say.
92
+
93
+ Tick exactly one:
94
+
95
+ - [ ] fix as suggested
96
+ - [ ] won't fix
97
+ - [ ] _your answer_
98
+ """
99
+
100
+ Scenario: a sign-off with the note unanswered is refused
101
+ When the process is driven
102
+ Then landing is refused with "answer-completeness: 1 note(s) in .gtd/REVIEW.md not answered"
103
+ And the process rests at "build.review.await-review"
104
+
105
+ Scenario: two ticks on one note are refused
106
+ When the process is driven
107
+ And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with "- [x] fix as suggested"
108
+ And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
109
+ Then landing is refused with "tick exactly one box per note"
110
+
111
+ Scenario: a ticked answer still holding the placeholder is refused
112
+ When the process is driven
113
+ And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] _your answer_"
114
+ Then landing is refused with "answer-completeness"
115
+
116
+ Scenario: won't fix drops the note, records it, and signs off
117
+ When the process is driven
118
+ And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
119
+ And gtd lands
120
+ And the process is driven
121
+ Then the process rests at "idle"
122
+ And the commit "gtd(human): build.review.await-review → build.review.closing" has a body containing "Gtd-Mark: wontfix=AC-1 ignores floating point"
123
+ And ".gtd/TODO.md" does not exist
124
+ And no commit body contains "build.review.triage"
125
+
126
+ Scenario: fix as suggested goes to the nit batch unjudged, and the note is not asked again
127
+ Given step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
128
+ """
129
+ export const add = (a: number, b: number) => a + b
130
+ export const subtract = (a: number, b: number) => Math.round(a - b)
131
+ """
132
+ And step "build.review.fix.nits.fixing" is prompted with "Should subtract round?"
133
+ And step "build.health.check" changes nothing
134
+ When the process is driven
135
+ And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with "- [x] fix as suggested"
136
+ And gtd lands
137
+ And the process is driven
138
+ Then the process rests at "build.review.await-review"
139
+ And no commit body contains "build.review.triage"
140
+ And ".gtd/REVIEW.md" contains "## Fixed from your notes"
141
+ And ".gtd/REVIEW.md" does not contain "AC-1 ignores floating point"
142
+ And ".gtd/TODO.md" does not exist
143
+ When gtd lands
144
+ And the process is driven
145
+ Then the process rests at "idle"
146
+
147
+ Scenario: an own answer is judged like a note
148
+ Given judge step "build.review.triage" answers:
149
+ """
150
+ [{"id":"answer-1","answer":"nit","p":0.9}]
151
+ """
152
+ And step "build.review.fix.nits.fixing" is prompted with "round half up"
153
+ And step "build.health.check" changes nothing
154
+ And step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
155
+ """
156
+ export const add = (a: number, b: number) => a + b
157
+ export const subtract = (a: number, b: number) => Math.round(a - b)
158
+ """
159
+ When the process is driven
160
+ And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] round half up"
161
+ And gtd lands
162
+ And the process is driven
163
+ Then the process rests at "build.review.await-review"
164
+ And the commit subjects end with:
165
+ """
166
+ gtd(human): build.review.await-review → build.review.triage
167
+ gtd(judge): build.review.triage → build.review.fix.nits.fixing
168
+ gtd(agent): build.review.fix.nits.fixing → build.health.check
169
+ gtd(check): build.health.check → build.review.closing
170
+ gtd(check): build.review.closing → build.review.composing
171
+ gtd(check): build.review.composing → build.review.await-review
172
+ """
173
+ And ".gtd/REVIEW.md" does not contain "AC-1 ignores floating point"
174
+
175
+ Scenario: deleting a note's boxes instead of answering is refused
176
+ When the process is driven
177
+ And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with ""
178
+ And the text "Tick exactly one:" in ".gtd/REVIEW.md" is replaced with ""
179
+ Then landing is refused with "its answer boxes were deleted"
@@ -45,7 +45,7 @@ import {
45
45
  refactorStrays,
46
46
  reviewedRefactor,
47
47
  } from "./refactor.js"
48
- import { isCode } from "./review.js"
48
+ import { isCode } from "../shared/review.js"
49
49
  import { flagModeOn } from "../shared/options.js"
50
50
  import type { RiskTier } from "./tier.js"
51
51
 
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { describe, expect, it } from "vitest"
3
- import { installContext, type ScopeOptions, type StepRequest } from "../../flows/index.js"
4
- import { captureStep, fixtureContext } from "../shared/text.fixture.js"
3
+ import type { StepRequest } from "../../flows/index.js"
4
+ import { captureStep } from "../shared/text.fixture.js"
5
5
  import * as steps from "./steps.js"
6
6
 
7
7
  const capture = (
@@ -49,80 +49,6 @@ describe("the feature workflow's own steps declare skills — a step's rendered
49
49
  expect(closed).toContain("already closed")
50
50
  expect(closed).not.toContain("exactly one `- A:` reply")
51
51
  })
52
-
53
- it("answerReviewQuestions answers inline in .gtd/REVIEW.md as the reviewer, with its bundled skills", async () => {
54
- const request = await capture(
55
- () =>
56
- steps.answerReviewQuestions([{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "why?" }]),
57
- "build",
58
- )
59
- if (request.kind !== "agent") throw new Error("unreachable")
60
- expect(request.name).toBe("review.answer-review-questions")
61
- expect(request.options).toMatchObject({
62
- file: ".gtd/REVIEW.md",
63
- mode: "review",
64
- })
65
- expect(request.prompt).toContain("code-review-and-quality")
66
- expect(request.prompt).toContain("note-1 — calc ./a.ts#1-1")
67
- expect(request.prompt).toContain("`A: ` line")
68
- expect(request.prompt).toContain("gtd check review .gtd/REVIEW.md")
69
- })
70
-
71
- it("fixNits fixes every nit in one turn and leaves .gtd/REVIEW.md alone", async () => {
72
- const request = await capture(
73
- () =>
74
- steps.fixNits([
75
- { id: "note-1", anchor: "calc ./a.ts#1-1", text: "typo" },
76
- { id: "note-2", anchor: "calc ./a.ts#2-2", text: "semicolon" },
77
- ]),
78
- "build",
79
- )
80
- if (request.kind !== "agent") throw new Error("unreachable")
81
- expect(request.name).toBe("review.fix.nits.fixing")
82
- expect(request.prompt).toContain("incremental-implementation, code-simplification")
83
- expect(request.prompt).toContain("typo")
84
- expect(request.prompt).toContain("semicolon")
85
- expect(request.prompt).toContain("Leave `.gtd/REVIEW.md` untouched")
86
- })
87
-
88
- it("fixNits with access runs in a review.fix.nits scope, the step named fixing inside it", async () => {
89
- const scopes: ScopeOptions[] = []
90
- const steps_: StepRequest[] = []
91
- const access = { write: ["src/a.ts", ".gtd/**"] }
92
- installContext(
93
- fixtureContext(
94
- { scope: "refactor.build" },
95
- {
96
- step: (request) => {
97
- steps_.push(request)
98
- return Promise.resolve()
99
- },
100
- pushScope: (s) => void scopes.push(s),
101
- popScope: () => undefined,
102
- changesSince: () => [],
103
- },
104
- ),
105
- )
106
- try {
107
- await steps.fixNits([], access)
108
- } finally {
109
- installContext(undefined)
110
- }
111
- expect(scopes).toEqual([{ name: "review.fix.nits", access }])
112
- expect(steps_.map((r) => ("name" in r ? r.name : ""))).toEqual(["fixing"])
113
- })
114
-
115
- it("deferNotes rests at build.review.defer as a script writing .gtd/TODO.md", async () => {
116
- const request = await capture(
117
- () =>
118
- steps.deferNotes([{ id: "note-1", anchor: "a", text: "x", label: "could" }], "abcdef123"),
119
- "build",
120
- )
121
- if (request.kind !== "run") throw new Error("unreachable")
122
- expect(request.name).toBe("review.defer")
123
- expect(request.body).toContain("## Deferred review notes (abcdef1)")
124
- expect(request.options).toMatchObject({ file: ".gtd/TODO.md" })
125
- })
126
52
  })
127
53
 
128
54
  describe("the feature workflow's planning steps run on the planner model", () => {
@@ -1,8 +1,11 @@
1
- import { human, run, scope, type ScopeAccess } from "../../flows/index.js"
1
+ import { human } from "../../flows/index.js"
2
2
  import type { CollectRound } from "../../workflow-kit/index.js"
3
3
  import * as t from "../shared/text.js"
4
4
  import { riskTierOn } from "./tier.js"
5
- import { planner, REFACTOR, REQUIREMENTS, REVIEW } from "../shared/steps.js"
5
+ import { planner, REFACTOR, REQUIREMENTS } from "../shared/steps.js"
6
+
7
+ // Kept on the published feature entry.
8
+ export { answerReviewQuestions, collecting, fixNits } from "../shared/steps.js"
6
9
 
7
10
  /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
8
11
  export const triage = (base: string): Promise<void> =>
@@ -23,45 +26,6 @@ export const answerProductQuestions = (): Promise<void> =>
23
26
  acceptClean: true,
24
27
  })
25
28
 
26
- /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
27
- export const answerReviewQuestions = (notes: readonly t.NoteInput[]): Promise<void> =>
28
- t.agentWithSkills("review.answer-review-questions", t.buildReviewAnswerQuestionsPrompt(notes), {
29
- label: "Answering your questions",
30
- file: REVIEW,
31
- mode: "review",
32
- model: planner(),
33
- system: t.reviewerSystem(),
34
- })
35
-
36
- /** Fix every `nit` note in one turn. */
37
- export const fixNits = (
38
- notes: readonly t.NoteInput[],
39
- access?: ScopeAccess,
40
- rules?: string,
41
- ): Promise<void> => {
42
- const fixing = (name: string): Promise<void> =>
43
- t.agentWithSkills(name, t.buildReviewFixNitsPrompt(notes, rules), {
44
- label: "Fixing your nits",
45
- file: REVIEW,
46
- model: planner(),
47
- system: t.reviewerSystem(),
48
- })
49
- return access === undefined
50
- ? fixing("review.fix.nits.fixing")
51
- : scope({ name: "review.fix.nits", access }, () => fixing("fixing"))
52
- }
53
-
54
- /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
55
- export const collecting = (capture: string): Promise<void> =>
56
- t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture), {
57
- label: "Collecting your feedback",
58
- file: REQUIREMENTS,
59
- mode: "qa",
60
- model: planner(),
61
- system: t.reviewerSystem(),
62
- allowEmpty: true,
63
- })
64
-
65
29
  /** Turn the refactor review round `capture` describes into `.gtd/REFACTOR.md`, or change nothing. */
66
30
  export const refactorCollecting = (
67
31
  capture: string,
@@ -79,10 +43,3 @@ export const refactorCollecting = (
79
43
  allowEmpty: true,
80
44
  },
81
45
  )
82
-
83
- /** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
84
- export const deferNotes = (notes: readonly t.NoteInput[], round: string): Promise<void> =>
85
- run("review.defer", t.deferNotesScript(notes, round), {
86
- label: "Deferring could-notes to TODO.md",
87
- file: ".gtd/TODO.md",
88
- })
@@ -1,8 +1,7 @@
1
- Feature: fix fixes the notes review hands it
1
+ Feature: fix fixes the notes a hand-off leaves it
2
2
 
3
- `review` and `fix` meet only in `.gtd/REVIEW.md`: a review the human leaves
4
- a note in ends with the reviewer's copy kept beside it, and `gtd door fix`
5
- picks both up.
3
+ A hand-off is `.gtd/REVIEW.md` with the human's notes beside the reviewer's
4
+ own `.gtd/REVIEW.orig.md`; `gtd door fix` picks both up.
6
5
 
7
6
  Background:
8
7
  Given the shipped workflows, as gtd install scaffolds them
@@ -13,10 +12,8 @@ Feature: fix fixes the notes review hands it
13
12
  """
14
13
  And the working tree is committed as "feat: add calculator"
15
14
 
16
- Scenario: a note left in review is fixed, and both review files are removed
17
- Given step "review-gate.check" changes nothing
18
- And step "build.health.check" changes nothing
19
- And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
15
+ Scenario: a handed-off note is fixed, and both review files are removed
16
+ Given a file ".gtd/REVIEW.orig.md" with:
20
17
  """
21
18
  # Review: abc1234
22
19
 
@@ -27,11 +24,7 @@ Feature: fix fixes the notes review hands it
27
24
  - [ ] ./src/calc.ts#1-1
28
25
  new add function
29
26
  """
30
- When gtd opens door "review" with "HEAD~1"
31
- And the process is driven
32
- Then the process rests at "build.review.await-review"
33
-
34
- Given a file ".gtd/REVIEW.md" with:
27
+ And a file ".gtd/REVIEW.md" with:
35
28
  """
36
29
  # Review: abc1234
37
30
 
@@ -42,21 +35,27 @@ Feature: fix fixes the notes review hands it
42
35
  - [ ] ./src/calc.ts#1-1
43
36
  new add function — rename it to sum
44
37
  """
45
- And step "build.review.handing-off" runs its script
46
- When gtd lands
47
- And the process is driven
48
- Then the process rests at "idle"
49
- And ".gtd/REVIEW.orig.md" contains "new add function"
38
+ And the working tree is committed as "chore: hand off a review"
39
+ And step "build.health.check" changes nothing
40
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
41
+ """
42
+ # Review: abc1234
43
+
44
+ <!-- base: 0000000 -->
45
+
46
+ ## calc
50
47
 
51
- Given step "notes.fixing" writes "src/calc.ts" with:
48
+ - [ ] ./src/calc.ts#1-1
49
+ new add function
50
+ """
51
+ And step "notes.fixing" writes "src/calc.ts" with:
52
52
  """
53
53
  export const sum = (a: number, b: number) => a + b
54
54
  """
55
55
  And step "notes.closing" runs its script
56
56
  When gtd opens door "fix"
57
57
  Then the resting step's content contains "rename it to sum"
58
- # The fix goes to its own review, which the same scripted reviewer writes
59
- # afresh — the handed-off review itself is gone, as the reviewer's copy shows.
58
+ # The fix goes to its own review, which the scripted reviewer writes afresh.
60
59
  When the process is driven
61
60
  Then the process rests at "build.review.await-review"
62
61
  And the commit subjects end with:
@@ -32,7 +32,7 @@ export {
32
32
  type BuildTailOptions,
33
33
  type FeedbackTarget,
34
34
  type ReviewOutcome,
35
- } from "./feature/review.js"
35
+ } from "./shared/review.js"
36
36
  export * from "./feature/feature.js"
37
37
  export { base, review, reviewFlow } from "./review/review.js"
38
38
  export { fix, fixFlow, fixReviewNotes } from "./fix/fix.js"
@@ -0,0 +1,149 @@
1
+ @inmem
2
+ Feature: the review door fixes its notes in the same process
3
+
4
+ `gtd door review` runs the feature's review over everything since its base:
5
+ the triaged quality lap writes `.gtd/REVIEW.md`, the human answers each note,
6
+ and what they ask for is fixed in place under the health loop before the
7
+ review comes back. A sign-off ends the process; nothing is left for `fix`.
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 step "review-gate.check" changes nothing
18
+ And step "build.health.check" changes nothing
19
+ And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
20
+ """
21
+ # Quality
22
+
23
+ ## add has no test
24
+
25
+ Add a test for add.
26
+ """
27
+ And step "build.quality.sorting" runs its script
28
+ And judge step "build.quality.triage" answers:
29
+ """
30
+ [{"id":"Q1","answer":"design","p":0.9}]
31
+ """
32
+ And step "build.review.composing" runs its script
33
+ And step "build.review.closing" runs its script
34
+
35
+ Scenario: the review is composed from the lap's notes, with answer boxes
36
+ When gtd opens door "review" with "HEAD~1"
37
+ And the process is driven
38
+ Then the process rests at "build.review.await-review"
39
+ And the commit subjects end with:
40
+ """
41
+ gtd(human): review-gate.check
42
+ gtd(check): review-gate.check → build.quality.correctness.reviewing
43
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
44
+ gtd(judge): build.quality.triage → build.quality.sorting
45
+ gtd(check): build.quality.sorting → build.review.composing
46
+ gtd(check): build.review.composing → build.review.await-review
47
+ """
48
+ And ".gtd/REVIEW.md" contains:
49
+ """
50
+ ## add has no test
51
+
52
+ Add a test for add.
53
+
54
+ Tick exactly one:
55
+
56
+ - [ ] fix as suggested
57
+ - [ ] won't fix
58
+ - [ ] _your answer_
59
+ """
60
+
61
+ Scenario: a won't-fix signs off, and no review is left for fix
62
+ When gtd opens door "review" with "HEAD~1"
63
+ And the process is driven
64
+ And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
65
+ And gtd lands
66
+ And the process is driven
67
+ Then the process rests at "idle"
68
+ And ".gtd/REVIEW.md" does not exist
69
+ And ".gtd/REVIEW.orig.md" does not exist
70
+
71
+ Scenario: a note ticked fix as suggested is fixed in place, then reviewed again
72
+ Given step "build.review.fix.nits.fixing" writes "src/calc.test.ts" with:
73
+ """
74
+ it("adds", () => {})
75
+ """
76
+ When gtd opens door "review" with "HEAD~1"
77
+ And the process is driven
78
+ And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with "- [x] fix as suggested"
79
+ And gtd lands
80
+ And the process is driven
81
+ Then the process rests at "build.review.await-review"
82
+ And the commit subjects end with:
83
+ """
84
+ gtd(human): build.review.await-review → build.review.fix.nits.fixing
85
+ gtd(agent): build.review.fix.nits.fixing → build.health.check
86
+ gtd(check): build.health.check → build.review.closing
87
+ gtd(check): build.review.closing → build.review.composing
88
+ gtd(check): build.review.composing → build.review.await-review
89
+ """
90
+ And ".gtd/REVIEW.md" contains "## Fixed from your notes"
91
+ And "src/calc.test.ts" contains "adds"
92
+ When gtd lands
93
+ And the process is driven
94
+ Then the process rests at "idle"
95
+ And ".gtd/REVIEW.md" does not exist
96
+ And ".gtd/REVIEW.orig.md" does not exist
97
+
98
+ Scenario: an own answer asking for a change is fixed in place, then reviewed again
99
+ Given judge step "build.review.triage" answers:
100
+ """
101
+ [{"id":"answer-1","answer":"edit","p":0.9}]
102
+ """
103
+ And step "build.review.collecting" writes ".gtd/REQUIREMENTS.md" with:
104
+ """
105
+ # Requirements
106
+
107
+ ## Test add with negatives
108
+
109
+ add(-1, -2) returns -3.
110
+ """
111
+ And step "feedback.fixing" is prompted with ".gtd/REQUIREMENTS.md"
112
+ And step "feedback.fixing" writes "src/calc.test.ts" with:
113
+ """
114
+ it("adds negatives", () => {})
115
+ """
116
+ And step "re-unwind" runs its script
117
+ And step "feedback.closing" runs its script
118
+ When gtd opens door "review" with "HEAD~1"
119
+ And the process is driven
120
+ And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] test negatives too"
121
+ And gtd lands
122
+ And the process is driven
123
+ Then the process rests at "build.review.await-review"
124
+ And the commit subjects end with:
125
+ """
126
+ gtd(human): build.review.await-review → build.review.triage
127
+ gtd(judge): build.review.triage → build.review.closing
128
+ gtd(check): build.review.closing → build.review.collecting
129
+ gtd(agent): build.review.collecting → re-unwind
130
+ gtd(check): re-unwind → feedback.fixing
131
+ gtd(agent): feedback.fixing → feedback.closing
132
+ gtd(check): feedback.closing → build.health.check
133
+ gtd(check): build.health.check → build.quality.correctness.reviewing
134
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
135
+ gtd(judge): build.quality.triage → build.quality.sorting
136
+ gtd(check): build.quality.sorting → build.review.composing
137
+ gtd(check): build.review.composing → build.review.await-review
138
+ """
139
+ And ".gtd/REQUIREMENTS.md" does not exist
140
+ And "src/calc.test.ts" contains "adds negatives"
141
+ And ".gtd/REVIEW.md" does not contain "test negatives too"
142
+
143
+ Scenario: a leftover requirements file is refused before any step
144
+ Given a file ".gtd/REQUIREMENTS.md" with:
145
+ """
146
+ # Requirements
147
+ """
148
+ And the working tree is committed as "chore: leftover requirements"
149
+ Then opening door "review" is refused with ".gtd/REQUIREMENTS.md is left over"
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { runsItsScript, testRepo, type TestRepo } from "../../testkit/index.js"
2
+ import { 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
  import { base } from "./review.js"
@@ -7,79 +7,7 @@ import { base } from "./review.js"
7
7
  const REVIEW = ".gtd/REVIEW.md"
8
8
  const ORIG = ".gtd/REVIEW.orig.md"
9
9
 
10
- /** A repository with one commit to review, the review run until the human holds `.gtd/REVIEW.md`. */
11
- const reviewed = async (): Promise<TestRepo> => {
12
- const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_QUALITYREVIEWS: "" } })
13
- repo.write("src/calc.ts", "export const add = (a: number, b: number) => a + b\n")
14
- repo.commit("feat: add calculator")
15
- await repo.door("review", "HEAD~1")
16
- const rest = await repo.drive({
17
- "review-gate.check": () => undefined,
18
- "build.health.check": () => undefined,
19
- "build.review.reviewing": (beat, r) => {
20
- expect(beat.file).toBe(REVIEW)
21
- r.write(REVIEW, reviewDoc("new add function"))
22
- },
23
- })
24
- expect(rest).toMatchObject({ state: "build.review.await-review", actor: "human" })
25
- return repo
26
- }
27
-
28
10
  describe("the review workflow", () => {
29
- it("checks, reviews and hands the review to the human", async () => {
30
- const repo = await reviewed()
31
- expect(repo.subjects().slice(2)).toEqual([
32
- "gtd(human): review-gate.check",
33
- "gtd(check): review-gate.check → build.health.check",
34
- "gtd(check): build.health.check → build.review.reviewing",
35
- "gtd(agent): build.review.reviewing → build.review.await-review",
36
- ])
37
- })
38
-
39
- it("closes a review the human signs off", async () => {
40
- const repo = await reviewed()
41
- repo.write(REVIEW, reviewDoc("new add function").replace("- [ ]", "- [x]"))
42
- await repo.land()
43
- const rest = await repo.drive({ "build.review.closing": runsItsScript })
44
- expect(rest.state).toBe("idle")
45
- expect(repo.subjects().slice(-2)).toEqual([
46
- "gtd(human): build.review.await-review → build.review.closing",
47
- "gtd(check): build.review.closing → idle",
48
- ])
49
- expect(repo.read(REVIEW)).toBeUndefined()
50
- })
51
-
52
- it("never lands a review the review format rejects", async () => {
53
- const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_QUALITYREVIEWS: "" } })
54
- repo.write("src/calc.ts", "export const add = (a: number, b: number) => a + b\n")
55
- repo.commit("feat: add calculator")
56
- await repo.door("review", "HEAD~1")
57
- await expect(
58
- repo.drive({
59
- "review-gate.check": () => undefined,
60
- "build.health.check": () => undefined,
61
- "build.review.reviewing": (_, r) => r.write(REVIEW, "not a review\n"),
62
- }),
63
- ).rejects.toThrow(/validating \.gtd\/REVIEW\.md/)
64
- expect(repo.subjects().at(-1)).toBe("gtd(check): build.health.check → build.review.reviewing")
65
- })
66
-
67
- it("ends with the review and the reviewer's copy kept when the human leaves a note", async () => {
68
- const repo = await reviewed()
69
- repo.write(REVIEW, reviewDoc("new add function — rename it to sum"))
70
- await repo.land()
71
- const rest = await repo.drive({
72
- "build.review.handing-off": runsItsScript,
73
- })
74
- expect(rest.state).toBe("idle")
75
- expect(repo.subjects().slice(-2)).toEqual([
76
- "gtd(human): build.review.await-review → build.review.handing-off",
77
- "gtd(check): build.review.handing-off → idle",
78
- ])
79
- expect(repo.read(REVIEW)).toContain("rename it to sum")
80
- expect(repo.read(ORIG)).toBe(reviewDoc("new add function"))
81
- })
82
-
83
11
  it("refuses a pending hand-off before requesting any step, the gate's check included", async () => {
84
12
  const repo = testRepo({
85
13
  workflow: CONFIG_SCAFFOLD,