@pmelab/gtd 27.6.0 → 27.7.1
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 +25 -14
- package/claude/hooks/assist.ts +35 -2
- package/claude/hooks/delegate.ts +22 -0
- package/claude/hooks/handoff.ts +13 -4
- package/claude/hooks/jobs.ts +67 -0
- package/claude/hooks/paste.ts +17 -0
- package/claude/hooks/register.tsx +126 -65
- package/claude/hooks/ship.ts +7 -2
- package/claude/hooks/tools.ts +32 -0
- package/claude/hooks/wording.ts +11 -0
- package/dist/chunks/{reserved-Df8GeSHw.mjs → reserved-UdSuHDUJ.mjs} +2 -2
- package/dist/chunks/{tail-DmIfEAI4.mjs → tail-fj60rQ3y.mjs} +1 -1
- package/dist/gtd.bundle.mjs +3 -3
- package/dist/testkit.mjs +30 -18
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +3 -3
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +3 -3
- package/package.json +1 -1
- package/src/cli/Cli.ts +2 -2
- package/src/testkit/EmittedScriptRecognizer.test.ts +41 -0
- package/src/testkit/EmittedScriptRecognizer.ts +16 -7
- package/src/testkit/TestRepo.test.ts +79 -1
- package/src/testkit/TestRepo.ts +17 -6
- package/src/testkit/steps.ts +44 -1
- package/src/testkit/testkit.feature +35 -0
- package/src/workflows/cleanup/cleanup-path.feature +94 -0
- package/src/workflows/cleanup/cleanup.feature +0 -29
- package/src/workflows/feature/fast-path.feature +114 -0
- package/src/workflows/feature/one-step-land.feature +108 -0
- package/src/workflows/feature/refactor-first.feature +137 -0
- package/src/workflows/feature/review.test.ts +9 -3
- package/src/workflows/feature/review.ts +3 -3
- package/src/workflows/feature/risk-tier.feature +0 -63
- package/src/workflows/feature/seam-rejection.feature +146 -0
- package/src/workflows/feature/seam.feature +0 -67
- package/src/workflows/shared/steps.ts +1 -2
- package/src/workflows/shared/tail.test.ts +18 -9
- package/src/workflows/shared/workflows.steps.ts +5 -4
|
@@ -118,69 +118,6 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
|
|
|
118
118
|
| low |
|
|
119
119
|
| high |
|
|
120
120
|
|
|
121
|
-
Scenario Outline: low and high processes produce the same commit subjects to implement.building
|
|
122
|
-
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
123
|
-
"""
|
|
124
|
-
# Requirements
|
|
125
|
-
|
|
126
|
-
## Risk Tier
|
|
127
|
-
|
|
128
|
-
<tier> — reason
|
|
129
|
-
"""
|
|
130
|
-
And step "design.gate.answer" changes nothing
|
|
131
|
-
And step "seam.naive" writes "src/calc.ts" with:
|
|
132
|
-
"""
|
|
133
|
-
export const add = (a: number, b: number) => a + b
|
|
134
|
-
export const subtract = (a: number, b: number) => a - b
|
|
135
|
-
"""
|
|
136
|
-
And judge step "seam.judge" answers:
|
|
137
|
-
"""
|
|
138
|
-
[{"id":"shape","answer":"land","p":0.9}]
|
|
139
|
-
"""
|
|
140
|
-
When the process is driven
|
|
141
|
-
Then the process rests at "implement.building"
|
|
142
|
-
And the commit subjects end with:
|
|
143
|
-
"""
|
|
144
|
-
gtd(agent): design.triage → design.gate.answer
|
|
145
|
-
gtd(human): design.gate.answer → seam.naive
|
|
146
|
-
gtd(agent): seam.naive → seam.judge
|
|
147
|
-
gtd(judge): seam.judge → implement.building
|
|
148
|
-
"""
|
|
149
|
-
|
|
150
|
-
Examples:
|
|
151
|
-
| tier |
|
|
152
|
-
| low |
|
|
153
|
-
| high |
|
|
154
|
-
|
|
155
|
-
Scenario Outline: the implementation prompt demands a flag for high and none for low
|
|
156
|
-
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
157
|
-
"""
|
|
158
|
-
# Requirements
|
|
159
|
-
|
|
160
|
-
## Risk Tier
|
|
161
|
-
|
|
162
|
-
<tier> — reason
|
|
163
|
-
"""
|
|
164
|
-
And step "design.gate.answer" changes nothing
|
|
165
|
-
And step "seam.naive" writes "src/calc.ts" with:
|
|
166
|
-
"""
|
|
167
|
-
export const add = (a: number, b: number) => a + b
|
|
168
|
-
export const subtract = (a: number, b: number) => a - b
|
|
169
|
-
"""
|
|
170
|
-
And judge step "seam.judge" answers:
|
|
171
|
-
"""
|
|
172
|
-
[{"id":"shape","answer":"land","p":0.9}]
|
|
173
|
-
"""
|
|
174
|
-
When the process is driven
|
|
175
|
-
Then the process rests at "implement.building"
|
|
176
|
-
And the resting step's content contains "<demand>"
|
|
177
|
-
And the resting step's content does not contain "<absent>"
|
|
178
|
-
|
|
179
|
-
Examples:
|
|
180
|
-
| tier | demand | absent |
|
|
181
|
-
| high | Put the new behaviour behind a new release flag | Add no release flag |
|
|
182
|
-
| low | Add no release flag — this change is low risk tier | new release flag |
|
|
183
|
-
|
|
184
121
|
Scenario: the tier comes from the requirements the final design gate lap accepted
|
|
185
122
|
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
186
123
|
"""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# One SDLC path per file, so a changed path reds exactly one fixture — do not merge.
|
|
2
|
+
@inmem
|
|
3
|
+
Feature: rejected naive passes walk one fixed path through the steer gate to idle
|
|
4
|
+
|
|
5
|
+
With GTD_FLAG_NAIVE_PASS on and GTD_FLAG_RISK_TIER off, three rejected naive
|
|
6
|
+
passes rest at the steer gate, where a human edit restarts the seam and the
|
|
7
|
+
steered rerun lands. The flags are pinned so the per-flag test runs never
|
|
8
|
+
move it off its path.
|
|
9
|
+
|
|
10
|
+
Background:
|
|
11
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
12
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
13
|
+
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
14
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
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
|
+
And step "design.gate.answer" changes nothing
|
|
39
|
+
|
|
40
|
+
Scenario: three rejects rest at the steer gate, a steered rerun lands and runs to idle
|
|
41
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
42
|
+
"""
|
|
43
|
+
export const add = (a: number, b: number) => b + a
|
|
44
|
+
"""
|
|
45
|
+
And step "implement.health.check" changes nothing
|
|
46
|
+
And step "implement.closing" runs its script
|
|
47
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
48
|
+
"""
|
|
49
|
+
# Review: abc1234
|
|
50
|
+
|
|
51
|
+
<!-- base: 0000000 -->
|
|
52
|
+
|
|
53
|
+
## calc
|
|
54
|
+
|
|
55
|
+
- [ ] ./src/calc.ts#1-2
|
|
56
|
+
subtract added
|
|
57
|
+
"""
|
|
58
|
+
And step "build.review.closing" runs its script
|
|
59
|
+
When the process is driven
|
|
60
|
+
Then the process rests at "seam.judge"
|
|
61
|
+
|
|
62
|
+
Given step "seam.naive" is prompted with "Your previous attempt was rejected"
|
|
63
|
+
And judge step "seam.judge" answers:
|
|
64
|
+
"""
|
|
65
|
+
[{"id":"shape","answer":"reject","p":0.9}]
|
|
66
|
+
"""
|
|
67
|
+
When the process is driven
|
|
68
|
+
Then the process rests at "seam.steer"
|
|
69
|
+
And the resting step's content contains "Overrule"
|
|
70
|
+
And the resting step's content contains "src/calc.ts"
|
|
71
|
+
|
|
72
|
+
Given a file ".gtd/REQUIREMENTS.md" with:
|
|
73
|
+
"""
|
|
74
|
+
# Requirements
|
|
75
|
+
|
|
76
|
+
## Concerns
|
|
77
|
+
|
|
78
|
+
### Subtract
|
|
79
|
+
|
|
80
|
+
- `subtract(a, b)` returns `a - b`, in the calculator module.
|
|
81
|
+
"""
|
|
82
|
+
And step "seam.naive" from now on changes nothing
|
|
83
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
84
|
+
"""
|
|
85
|
+
export const add = (a: number, b: number) => a + b
|
|
86
|
+
export const subtract = (a: number, b: number) => a - b
|
|
87
|
+
"""
|
|
88
|
+
And judge step "seam.judge" answers:
|
|
89
|
+
"""
|
|
90
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
91
|
+
"""
|
|
92
|
+
And step "implement.building" writes "src/calc.ts" with:
|
|
93
|
+
"""
|
|
94
|
+
export const add = (a: number, b: number) => a + b
|
|
95
|
+
export const subtract = (a: number, b: number) => a - b
|
|
96
|
+
export const SUBTRACT = true
|
|
97
|
+
"""
|
|
98
|
+
When gtd lands
|
|
99
|
+
And the process is driven
|
|
100
|
+
Then the process rests at "build.review.await-review"
|
|
101
|
+
|
|
102
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
103
|
+
"""
|
|
104
|
+
# Review: abc1234
|
|
105
|
+
|
|
106
|
+
<!-- base: 0000000 -->
|
|
107
|
+
|
|
108
|
+
## calc
|
|
109
|
+
|
|
110
|
+
- [x] ./src/calc.ts#1-2
|
|
111
|
+
subtract added
|
|
112
|
+
"""
|
|
113
|
+
When gtd lands
|
|
114
|
+
And the process is driven
|
|
115
|
+
Then the process rests at "idle"
|
|
116
|
+
And the commit subjects end with:
|
|
117
|
+
"""
|
|
118
|
+
gtd(human): idle → unwind
|
|
119
|
+
gtd(check): unwind → start-gate.check
|
|
120
|
+
gtd(check): start-gate.check → design.triage
|
|
121
|
+
gtd(agent): design.triage → design.gate.answer
|
|
122
|
+
gtd(human): design.gate.answer → seam.naive
|
|
123
|
+
gtd(agent): seam.naive → seam.judge
|
|
124
|
+
gtd(judge): seam.judge → seam.naive
|
|
125
|
+
gtd(agent): seam.naive → seam.judge
|
|
126
|
+
gtd(judge): seam.judge → seam.naive
|
|
127
|
+
gtd(agent): seam.naive → seam.judge
|
|
128
|
+
gtd(judge): seam.judge → seam.steer
|
|
129
|
+
gtd(human): seam.steer → seam.naive
|
|
130
|
+
gtd(agent): seam.naive → seam.judge
|
|
131
|
+
gtd(judge): seam.judge → implement.building
|
|
132
|
+
gtd(agent): implement.building → implement.health.check
|
|
133
|
+
gtd(check): implement.health.check → implement.closing
|
|
134
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
135
|
+
gtd(agent): build.review.reviewing → build.review.await-review
|
|
136
|
+
gtd(human): build.review.await-review → build.review.closing
|
|
137
|
+
gtd(check): build.review.closing → idle
|
|
138
|
+
"""
|
|
139
|
+
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_naive_pass=1"
|
|
140
|
+
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
141
|
+
And the commit "gtd(agent): seam.naive → seam.judge" has a body containing "Gtd-Kept:"
|
|
142
|
+
And the commit "gtd(judge): seam.judge → seam.naive" has a body containing "\"answer\":\"reject\""
|
|
143
|
+
And the commit "gtd(judge): seam.judge → seam.steer" has a body containing "\"answer\":\"reject\""
|
|
144
|
+
And the commit "gtd(judge): seam.judge → implement.building" has a body containing "\"answer\":\"land\""
|
|
145
|
+
And no commit body contains "Gtd-Approved:"
|
|
146
|
+
And no commit body contains "Gtd-Workflow:"
|
|
@@ -166,73 +166,6 @@ Feature: the naive pass and seam judge replace architecture behind GTD_FLAG_NAIV
|
|
|
166
166
|
And the resting step's content contains "src/calc.ts"
|
|
167
167
|
And the resting step's content does not contain "src/calc*.ts"
|
|
168
168
|
|
|
169
|
-
Scenario: a refactor-first judgment rests at the approval gate, then refactors, then implements
|
|
170
|
-
Given step "seam.naive" writes "src/calc.ts" with:
|
|
171
|
-
"""
|
|
172
|
-
export const add = (a: number, b: number) => a + b
|
|
173
|
-
export const subtract = (a: number, b: number) => a - b
|
|
174
|
-
"""
|
|
175
|
-
And judge step "seam.judge" answers:
|
|
176
|
-
"""
|
|
177
|
-
[{"id":"shape","answer":"refactor-first","p":0.9}]
|
|
178
|
-
"""
|
|
179
|
-
And step "refactor.deriving" writes ".gtd/REFACTOR.md" with:
|
|
180
|
-
"""
|
|
181
|
-
# Behaviour-neutral refactor
|
|
182
|
-
|
|
183
|
-
## Scope (read-only)
|
|
184
|
-
|
|
185
|
-
- `src/calc.ts` # gtd-path-exempt: scenario fixture, not a repo file
|
|
186
|
-
"""
|
|
187
|
-
And step "refactor.building" writes "src/calc.ts" with:
|
|
188
|
-
"""
|
|
189
|
-
export const add = (a: number, b: number) => a + b
|
|
190
|
-
export const op = (a: number, b: number, sign: number) => a + sign * b
|
|
191
|
-
"""
|
|
192
|
-
And step "refactor.health.check" changes nothing
|
|
193
|
-
And step "refactor.closing" runs its script
|
|
194
|
-
When the process is driven
|
|
195
|
-
Then the process rests at "seam.approve"
|
|
196
|
-
And the resting step's content contains "src/calc.ts"
|
|
197
|
-
When gtd lands
|
|
198
|
-
Then the last commit subject is "gtd(human): seam.approve → refactor.deriving"
|
|
199
|
-
And the last commit body contains "Gtd-Approved:"
|
|
200
|
-
When the process is driven
|
|
201
|
-
Then the process rests at "implement.building"
|
|
202
|
-
And the commit subjects end with:
|
|
203
|
-
"""
|
|
204
|
-
gtd(judge): seam.judge → seam.approve
|
|
205
|
-
gtd(human): seam.approve → refactor.deriving
|
|
206
|
-
gtd(check): refactor.deriving → refactor.building
|
|
207
|
-
gtd(agent): refactor.building → refactor.health.check
|
|
208
|
-
gtd(check): refactor.health.check → refactor.closing
|
|
209
|
-
gtd(check): refactor.closing → implement.building
|
|
210
|
-
"""
|
|
211
|
-
|
|
212
|
-
Scenario: three rejects retry the naive pass twice, then rest at the steer gate
|
|
213
|
-
Given step "seam.naive" writes "src/calc.ts" with:
|
|
214
|
-
"""
|
|
215
|
-
export const add = (a: number, b: number) => b + a
|
|
216
|
-
"""
|
|
217
|
-
And judge step "seam.judge" answers:
|
|
218
|
-
"""
|
|
219
|
-
[{"id":"shape","answer":"reject","p":0.9}]
|
|
220
|
-
"""
|
|
221
|
-
When the process is driven
|
|
222
|
-
Then the process rests at "seam.steer"
|
|
223
|
-
And the resting step's content contains "Overrule"
|
|
224
|
-
And the resting step's content contains "src/calc.ts"
|
|
225
|
-
And the commit subjects end with:
|
|
226
|
-
"""
|
|
227
|
-
gtd(human): design.gate.answer → seam.naive
|
|
228
|
-
gtd(agent): seam.naive → seam.judge
|
|
229
|
-
gtd(judge): seam.judge → seam.naive
|
|
230
|
-
gtd(agent): seam.naive → seam.judge
|
|
231
|
-
gtd(judge): seam.judge → seam.naive
|
|
232
|
-
gtd(agent): seam.naive → seam.judge
|
|
233
|
-
gtd(judge): seam.judge → seam.steer
|
|
234
|
-
"""
|
|
235
|
-
|
|
236
169
|
Scenario: an edit at the steer gate runs the naive pass again with a fresh retry budget
|
|
237
170
|
Given step "seam.naive" writes "src/calc.ts" with:
|
|
238
171
|
"""
|
|
@@ -117,7 +117,6 @@ export const fixRisks = (notes: readonly t.NoteInput[]): Promise<void> =>
|
|
|
117
117
|
export const awaitReview = (
|
|
118
118
|
base: string,
|
|
119
119
|
message = t.buildReviewAwaitReviewMessage(base),
|
|
120
|
-
acceptClean = false,
|
|
121
120
|
): Promise<void> =>
|
|
122
121
|
human("review.await-review", {
|
|
123
122
|
message,
|
|
@@ -125,7 +124,7 @@ export const awaitReview = (
|
|
|
125
124
|
file: REVIEW,
|
|
126
125
|
mode: "review",
|
|
127
126
|
base,
|
|
128
|
-
|
|
127
|
+
acceptClean: true,
|
|
129
128
|
})
|
|
130
129
|
|
|
131
130
|
/** The round at commit `round` left no `.gtd/REVIEW.md`. */
|
|
@@ -38,8 +38,18 @@ const handOff = async (
|
|
|
38
38
|
} = {},
|
|
39
39
|
) => {
|
|
40
40
|
const log: string[] = []
|
|
41
|
+
const gates: boolean[] = []
|
|
41
42
|
const scripts = new Map<string, string>()
|
|
42
43
|
const files = new Map<string, string>()
|
|
44
|
+
const onReviewing = () => {
|
|
45
|
+
if (log.filter((n) => n === "review.reviewing").length > 1) throw new Error("re-review")
|
|
46
|
+
if (after !== "unwritten") files.set(REVIEW, reviewDoc("new add function"))
|
|
47
|
+
}
|
|
48
|
+
const onGate = (request: StepRequest) => {
|
|
49
|
+
if (request.kind === "human") gates.push(request.options.acceptClean === true)
|
|
50
|
+
if (after === "deleted") files.delete(REVIEW)
|
|
51
|
+
else if (after !== "unwritten") files.set(REVIEW, after)
|
|
52
|
+
}
|
|
43
53
|
installContext(
|
|
44
54
|
fixtureContext(
|
|
45
55
|
{ codeThreads: extra.codeThreads ?? [] },
|
|
@@ -48,14 +58,8 @@ const handOff = async (
|
|
|
48
58
|
if (request.kind === "restart") return Promise.resolve()
|
|
49
59
|
log.push(request.name)
|
|
50
60
|
if (request.kind === "run") scripts.set(request.name, request.body)
|
|
51
|
-
if (request.name === "review.reviewing")
|
|
52
|
-
|
|
53
|
-
if (after !== "unwritten") files.set(REVIEW, reviewDoc("new add function"))
|
|
54
|
-
}
|
|
55
|
-
if (request.name === "review.await-review") {
|
|
56
|
-
if (after === "deleted") files.delete(REVIEW)
|
|
57
|
-
else if (after !== "unwritten") files.set(REVIEW, after)
|
|
58
|
-
}
|
|
61
|
+
if (request.name === "review.reviewing") onReviewing()
|
|
62
|
+
if (request.name === "review.await-review") onGate(request)
|
|
59
63
|
return Promise.resolve()
|
|
60
64
|
},
|
|
61
65
|
refuse: (message: string): never => {
|
|
@@ -96,7 +100,7 @@ const handOff = async (
|
|
|
96
100
|
() => undefined,
|
|
97
101
|
(e: unknown) => (e as Error).message,
|
|
98
102
|
)
|
|
99
|
-
return { log, scripts, refusal }
|
|
103
|
+
return { log, scripts, refusal, gates }
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
describe("the hand-off review", () => {
|
|
@@ -105,6 +109,11 @@ describe("the hand-off review", () => {
|
|
|
105
109
|
expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.closing"])
|
|
106
110
|
})
|
|
107
111
|
|
|
112
|
+
it("the review gate accepts a clean landing", async () => {
|
|
113
|
+
const run = await handOff(reviewDoc("new add function"))
|
|
114
|
+
expect(run.gates).toEqual([true])
|
|
115
|
+
})
|
|
116
|
+
|
|
108
117
|
it("a note ends the round with the review kept and the reviewer's version copied beside it", async () => {
|
|
109
118
|
const run = await handOff(reviewDoc("new add function — rename it to sum"))
|
|
110
119
|
expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.handing-off"])
|
|
@@ -35,12 +35,13 @@ Then("the last commit body contains {string}", (world: TestkitWorld, text: strin
|
|
|
35
35
|
Then(
|
|
36
36
|
"the commit {string} has a body containing {string}",
|
|
37
37
|
(world: TestkitWorld, subject: string, text: string) => {
|
|
38
|
-
|
|
38
|
+
// Every commit with the subject, so a repeated step is not judged by its first match.
|
|
39
|
+
const matching = world
|
|
39
40
|
.repo()
|
|
40
41
|
.messages()
|
|
41
|
-
.
|
|
42
|
-
assert.ok(
|
|
43
|
-
assert.ok(message.includes(text), message)
|
|
42
|
+
.filter((m) => m.split("\n")[0] === subject)
|
|
43
|
+
assert.ok(matching.length > 0, `no commit "${subject}"`)
|
|
44
|
+
for (const message of matching) assert.ok(message.includes(text), message)
|
|
44
45
|
},
|
|
45
46
|
)
|
|
46
47
|
|