@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +96 -48
- package/claude/hooks/sketch.ts +1 -1
- package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-DJk_6-ju.mjs} +75 -46
- package/dist/chunks/review-X7Jfz0dY.mjs +249 -0
- package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
- package/dist/chunks/{tail-D2o2h5cc.mjs → tail-5w5mxGTq.mjs} +3 -3
- package/dist/gtd.bundle.mjs +488 -30
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +447 -27
- package/dist/web/main.js +42 -13
- package/dist/workflow-kit.mjs +61 -25
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +7 -290
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +27 -7
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +287 -161
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +33 -1
- package/src/cli/Cli.ts +70 -6
- package/src/cli/History.ts +15 -0
- package/src/cli/Judges.test.ts +373 -0
- package/src/cli/Judges.ts +624 -0
- package/src/cli/Metrics.ts +3 -16
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +9 -0
- package/src/steering/FORMATS.md +4 -4
- package/src/steering/index.ts +12 -1
- package/src/steering/review.test.ts +107 -0
- package/src/steering/review.ts +159 -9
- package/src/testkit/EmittedScriptRecognizer.ts +0 -14
- package/src/testkit/steps.ts +4 -0
- package/src/workflow-kit/review.test.ts +170 -26
- package/src/workflow-kit/review.ts +95 -19
- package/src/workflows/feature/coverage.ts +1 -1
- package/src/workflows/feature/feature.test.ts +8 -0
- package/src/workflows/feature/feature.ts +2 -2
- package/src/workflows/feature/index.ts +1 -1
- package/src/workflows/feature/lens-review.feature +16 -4
- package/src/workflows/feature/refactor.ts +1 -1
- package/src/workflows/feature/review-answers.feature +179 -0
- package/src/workflows/feature/seam.ts +1 -1
- package/src/workflows/feature/steps.test.ts +2 -76
- package/src/workflows/feature/steps.ts +5 -48
- package/src/workflows/fix/fix.feature +20 -21
- package/src/workflows/index.ts +1 -1
- package/src/workflows/review/review-door.feature +149 -0
- package/src/workflows/review/review.test.ts +1 -73
- package/src/workflows/review/review.ts +34 -6
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
- package/src/workflows/shared/access.test.ts +2 -11
- package/src/workflows/shared/access.ts +0 -2
- package/src/workflows/{feature → shared}/review.test.ts +9 -61
- package/src/workflows/{feature → shared}/review.ts +40 -48
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +2 -0
- package/src/workflows/shared/steps.test.ts +64 -2
- package/src/workflows/shared/steps.ts +40 -1
- package/src/workflows/shared/tail.ts +2 -2
- package/src/workflows/shared/text.test.ts +40 -89
- 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 "
|
|
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
|
|
4
|
-
import { captureStep
|
|
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
|
|
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
|
|
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
|
|
1
|
+
Feature: fix fixes the notes a hand-off leaves it
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
17
|
-
Given
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
And
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
package/src/workflows/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ export {
|
|
|
32
32
|
type BuildTailOptions,
|
|
33
33
|
type FeedbackTarget,
|
|
34
34
|
type ReviewOutcome,
|
|
35
|
-
} from "./
|
|
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 {
|
|
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,
|