@pmelab/gtd 27.3.0 → 27.5.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 +22 -1
- package/dist/chunks/{Schema-CyImoHTj.mjs → Schema-DO5ttJaI.mjs} +1 -1
- package/dist/chunks/{reserved-BPdVZkgv.mjs → reserved-DM5W_LAt.mjs} +64 -3
- package/dist/chunks/{tail-iLAMoigl.mjs → tail-6gvLXXcg.mjs} +1 -1
- package/dist/chunks/{wire-BY1S-obW.mjs → wire-C12dahrH.mjs} +6 -4
- package/dist/flows.mjs +27 -2
- package/dist/gtd.bundle.mjs +218 -32
- package/dist/testkit.mjs +358 -37
- package/dist/wire.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +140 -64
- 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 +346 -107
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +49 -1
- package/src/cli/Cli.ts +25 -8
- package/src/cli/History.ts +5 -0
- package/src/cli/Program.ts +1 -0
- package/src/edge/Edge.test.ts +205 -1
- package/src/edge/Edge.ts +105 -12
- package/src/emit/GitScript.test.ts +39 -3
- package/src/emit/GitScript.ts +25 -1
- package/src/emit/index.ts +2 -0
- package/src/flags/index.test.ts +14 -1
- package/src/flags/index.ts +14 -3
- package/src/flows/helpers.ts +34 -2
- package/src/flows/runtime.ts +7 -0
- package/src/install/Install.ts +2 -1
- package/src/platform/Git.test.ts +25 -1
- package/src/platform/Git.ts +30 -0
- package/src/platform/index.ts +1 -0
- package/src/replay/Format.test.ts +16 -1
- package/src/replay/Replay.test.ts +91 -0
- package/src/replay/Replay.ts +22 -2
- package/src/replay/Trailers.test.ts +110 -1
- package/src/replay/Trailers.ts +37 -1
- package/src/replay/index.ts +2 -0
- package/src/replay/judgeDiffShape.test.ts +43 -0
- package/src/replay/seamGate.test.ts +62 -0
- package/src/step/LandStep.ts +1 -0
- package/src/step/planStart.ts +3 -2
- package/src/step/planStep.test.ts +32 -0
- package/src/step/planStep.ts +3 -1
- package/src/testkit/EmittedScriptRecognizer.test.ts +27 -0
- package/src/testkit/EmittedScriptRecognizer.ts +12 -0
- package/src/testkit/FakeGitOperations.ts +5 -0
- package/src/testkit/GitTiers.ts +158 -0
- package/src/testkit/InMemRepo.ts +38 -3
- package/src/testkit/TestRepo.ts +3 -0
- package/src/testkit/steps.ts +18 -0
- package/src/wire/BeatDocument.test.ts +25 -0
- package/src/wire/BeatDocument.ts +4 -2
- package/src/wire/BeatPlain.test.ts +1 -0
- package/src/wire/BeatStatus.test.ts +2 -0
- package/src/wire/BeatStatus.ts +13 -2
- package/src/wire/WireSchema.test.ts +10 -0
- package/src/wire/types.ts +8 -0
- package/src/workflow/Workflow.ts +1 -0
- package/src/workflow/index.ts +8 -1
- package/src/workflow/vars.test.ts +23 -1
- package/src/workflow/vars.ts +24 -0
- package/src/workflows/feature/feature.ts +9 -2
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/seam-pin.feature +52 -0
- package/src/workflows/feature/seam.feature +387 -0
- package/src/workflows/feature/seam.test.ts +88 -0
- package/src/workflows/feature/seam.ts +110 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/options.test.ts +12 -1
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/skills.test.ts +20 -2
- package/src/workflows/shared/skills.ts +6 -0
- package/src/workflows/shared/text.test.ts +59 -0
- package/src/workflows/shared/text.ts +86 -0
- package/src/workflows/shared/vars.ts +4 -0
- package/src/workflows/shared/workflows.steps.ts +39 -1
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: the naive pass and seam judge replace architecture behind GTD_FLAG_NAIVE_PASS
|
|
3
|
+
|
|
4
|
+
With the flag on, after design a naive pass is set aside under a kept ref, the
|
|
5
|
+
judge reads its diff, and the process either implements straight away, refactors
|
|
6
|
+
the seam first behind a human approval, or retries the naive pass. With the flag
|
|
7
|
+
off nothing changes: architecture and packages run as before.
|
|
8
|
+
|
|
9
|
+
Background:
|
|
10
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
11
|
+
And the gtd config ends with:
|
|
12
|
+
"""
|
|
13
|
+
export const config = {
|
|
14
|
+
skills: {
|
|
15
|
+
implement: ["my-tdd", "my-runbook"],
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
"""
|
|
19
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
20
|
+
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
21
|
+
And a file "src/calc.ts" with:
|
|
22
|
+
"""
|
|
23
|
+
export const add = (a: number, b: number) => a + b
|
|
24
|
+
"""
|
|
25
|
+
And a file "src/calc.test.ts" with:
|
|
26
|
+
"""
|
|
27
|
+
import { expect, it } from "vitest"
|
|
28
|
+
it("adds", () => expect(1 + 1).toBe(2))
|
|
29
|
+
"""
|
|
30
|
+
And the working tree is committed as "feat: add calculator"
|
|
31
|
+
And a file "TODO.md" with:
|
|
32
|
+
"""
|
|
33
|
+
# Add subtract
|
|
34
|
+
|
|
35
|
+
subtract(a, b) returns the difference.
|
|
36
|
+
"""
|
|
37
|
+
And step "unwind" changes nothing
|
|
38
|
+
And step "start-gate.check" changes nothing
|
|
39
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
40
|
+
"""
|
|
41
|
+
# Requirements
|
|
42
|
+
|
|
43
|
+
## Concerns
|
|
44
|
+
|
|
45
|
+
### Subtract
|
|
46
|
+
|
|
47
|
+
- `subtract(a, b)` returns `a - b`.
|
|
48
|
+
"""
|
|
49
|
+
And step "design.gate.answer" changes nothing
|
|
50
|
+
|
|
51
|
+
Scenario: a land judgment goes straight to the implementation with no seam gate or refactor
|
|
52
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
53
|
+
"""
|
|
54
|
+
export const add = (a: number, b: number) => a + b
|
|
55
|
+
export const subtract = (a: number, b: number) => a - b
|
|
56
|
+
"""
|
|
57
|
+
And judge step "seam.judge" answers:
|
|
58
|
+
"""
|
|
59
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
60
|
+
"""
|
|
61
|
+
When the process is driven
|
|
62
|
+
Then the process rests at "implement.building"
|
|
63
|
+
And the last commit body contains "Gtd-Judge:"
|
|
64
|
+
And the commit subjects end with:
|
|
65
|
+
"""
|
|
66
|
+
chore: initial commit
|
|
67
|
+
feat: add calculator
|
|
68
|
+
gtd(human): idle → unwind
|
|
69
|
+
gtd(check): unwind → start-gate.check
|
|
70
|
+
gtd(check): start-gate.check → design.triage
|
|
71
|
+
gtd(agent): design.triage → design.gate.answer
|
|
72
|
+
gtd(human): design.gate.answer → seam.naive
|
|
73
|
+
gtd(agent): seam.naive → seam.judge
|
|
74
|
+
gtd(judge): seam.judge → implement.building
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
Scenario: config skills for the implement scope reach the implement step
|
|
78
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
79
|
+
"""
|
|
80
|
+
export const add = (a: number, b: number) => a + b
|
|
81
|
+
export const subtract = (a: number, b: number) => a - b
|
|
82
|
+
"""
|
|
83
|
+
And judge step "seam.judge" answers:
|
|
84
|
+
"""
|
|
85
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
86
|
+
"""
|
|
87
|
+
When the process is driven
|
|
88
|
+
Then the process rests at "implement.building"
|
|
89
|
+
And the resting step's content contains "my-tdd, my-runbook"
|
|
90
|
+
|
|
91
|
+
Scenario: the naive pass lands as a marker commit and its turn stays off the branch
|
|
92
|
+
Given step "seam.naive" 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
|
+
"""
|
|
97
|
+
When the process is driven
|
|
98
|
+
Then the process rests at "seam.judge"
|
|
99
|
+
And the last commit subject is "gtd(agent): seam.naive → seam.judge"
|
|
100
|
+
And the last commit body contains "Gtd-Kept:"
|
|
101
|
+
And "src/calc.ts" does not contain "subtract"
|
|
102
|
+
|
|
103
|
+
Scenario: the implementation is one build turn, a green check, then the requirements are closed out
|
|
104
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
105
|
+
"""
|
|
106
|
+
export const add = (a: number, b: number) => a + b
|
|
107
|
+
export const subtract = (a: number, b: number) => a - b
|
|
108
|
+
"""
|
|
109
|
+
And judge step "seam.judge" answers:
|
|
110
|
+
"""
|
|
111
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
112
|
+
"""
|
|
113
|
+
And step "implement.building" writes "src/calc.ts" with:
|
|
114
|
+
"""
|
|
115
|
+
export const add = (a: number, b: number) => a + b
|
|
116
|
+
export const subtract = (a: number, b: number) => a - b
|
|
117
|
+
export const SUBTRACT_ENABLED = process.env.GTD_FLAG_SUBTRACT === "1"
|
|
118
|
+
"""
|
|
119
|
+
And step "implement.health.check" changes nothing
|
|
120
|
+
And step "implement.closing" runs its script
|
|
121
|
+
When the process is driven
|
|
122
|
+
Then the process rests at "build.review.reviewing"
|
|
123
|
+
And the commit subjects end with:
|
|
124
|
+
"""
|
|
125
|
+
gtd(judge): seam.judge → implement.building
|
|
126
|
+
gtd(agent): implement.building → implement.health.check
|
|
127
|
+
gtd(check): implement.health.check → implement.closing
|
|
128
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
129
|
+
"""
|
|
130
|
+
And ".gtd/REQUIREMENTS.md" does not exist
|
|
131
|
+
|
|
132
|
+
Scenario: a naive pass modifying a test file and adding a new one skips the judge
|
|
133
|
+
Given step "seam.naive" writes "src/subtract.ts" with:
|
|
134
|
+
"""
|
|
135
|
+
export const subtract = (a: number, b: number) => a - b
|
|
136
|
+
"""
|
|
137
|
+
And step "seam.naive" writes "src/calc.test.ts" with:
|
|
138
|
+
"""
|
|
139
|
+
import { expect, it } from "vitest"
|
|
140
|
+
it("adds", () => expect(2 + 2).toBe(4))
|
|
141
|
+
"""
|
|
142
|
+
When the process is driven
|
|
143
|
+
Then the process rests at "implement.building"
|
|
144
|
+
And the commit subjects end with:
|
|
145
|
+
"""
|
|
146
|
+
gtd(human): design.gate.answer → seam.naive
|
|
147
|
+
gtd(agent): seam.naive → implement.building
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
Scenario: a naive pass adding a glob-named file does not hide a modified sibling
|
|
151
|
+
Given step "seam.naive" writes "src/calc*.ts" with:
|
|
152
|
+
"""
|
|
153
|
+
export const subtract = (a: number, b: number) => a - b
|
|
154
|
+
"""
|
|
155
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
156
|
+
"""
|
|
157
|
+
export const add = (a: number, b: number) => a + b
|
|
158
|
+
export const subtract = (a: number, b: number) => a - b
|
|
159
|
+
"""
|
|
160
|
+
And judge step "seam.judge" answers:
|
|
161
|
+
"""
|
|
162
|
+
[{"id":"shape","answer":"reject","p":0.9}]
|
|
163
|
+
"""
|
|
164
|
+
When the process is driven
|
|
165
|
+
Then the process rests at "seam.steer"
|
|
166
|
+
And the resting step's content contains "src/calc.ts"
|
|
167
|
+
And the resting step's content does not contain "src/calc*.ts"
|
|
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
|
+
Scenario: an edit at the steer gate runs the naive pass again with a fresh retry budget
|
|
237
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
238
|
+
"""
|
|
239
|
+
export const add = (a: number, b: number) => b + a
|
|
240
|
+
"""
|
|
241
|
+
And judge step "seam.judge" answers:
|
|
242
|
+
"""
|
|
243
|
+
[{"id":"shape","answer":"reject","p":0.9}]
|
|
244
|
+
"""
|
|
245
|
+
When the process is driven
|
|
246
|
+
Then the process rests at "seam.steer"
|
|
247
|
+
Given a file ".gtd/REQUIREMENTS.md" with:
|
|
248
|
+
"""
|
|
249
|
+
# Requirements
|
|
250
|
+
|
|
251
|
+
## Concerns
|
|
252
|
+
|
|
253
|
+
### Subtract
|
|
254
|
+
|
|
255
|
+
- `subtract(a, b)` returns `a - b`, in a new module.
|
|
256
|
+
"""
|
|
257
|
+
When gtd lands
|
|
258
|
+
Then the process rests at "seam.naive"
|
|
259
|
+
And the resting step's content does not contain "was rejected"
|
|
260
|
+
When the process is driven
|
|
261
|
+
Then the process rests at "seam.steer"
|
|
262
|
+
And the commit subjects end with:
|
|
263
|
+
"""
|
|
264
|
+
gtd(human): seam.steer → seam.naive
|
|
265
|
+
gtd(agent): seam.naive → seam.judge
|
|
266
|
+
gtd(judge): seam.judge → seam.naive
|
|
267
|
+
gtd(agent): seam.naive → seam.judge
|
|
268
|
+
gtd(judge): seam.judge → seam.naive
|
|
269
|
+
gtd(agent): seam.naive → seam.judge
|
|
270
|
+
gtd(judge): seam.judge → seam.steer
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
Scenario: a clean landing at the steer gate overrules, with no seam gate and no refactor
|
|
274
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
275
|
+
"""
|
|
276
|
+
export const add = (a: number, b: number) => b + a
|
|
277
|
+
"""
|
|
278
|
+
And judge step "seam.judge" answers:
|
|
279
|
+
"""
|
|
280
|
+
[{"id":"shape","answer":"reject","p":0.9}]
|
|
281
|
+
"""
|
|
282
|
+
When the process is driven
|
|
283
|
+
Then the process rests at "seam.steer"
|
|
284
|
+
When gtd lands
|
|
285
|
+
Then the process rests at "implement.building"
|
|
286
|
+
And the commit subjects end with:
|
|
287
|
+
"""
|
|
288
|
+
gtd(judge): seam.judge → seam.steer
|
|
289
|
+
gtd(human): seam.steer → implement.building
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
Scenario: review feedback re-runs design and the whole seam phase
|
|
293
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
294
|
+
"""
|
|
295
|
+
export const add = (a: number, b: number) => b + a
|
|
296
|
+
"""
|
|
297
|
+
And judge step "seam.judge" answers:
|
|
298
|
+
"""
|
|
299
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
300
|
+
"""
|
|
301
|
+
And step "implement.building" writes "src/calc.ts" with:
|
|
302
|
+
"""
|
|
303
|
+
export const add = (a: number, b: number) => a + b
|
|
304
|
+
export const SUBTRACT = process.env.GTD_FLAG_SUBTRACT === "1"
|
|
305
|
+
"""
|
|
306
|
+
And step "implement.health.check" changes nothing
|
|
307
|
+
And step "implement.closing" runs its script
|
|
308
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
309
|
+
"""
|
|
310
|
+
# Review: abc1234
|
|
311
|
+
|
|
312
|
+
<!-- base: 0000000 -->
|
|
313
|
+
|
|
314
|
+
## calc
|
|
315
|
+
|
|
316
|
+
- [ ] ./src/calc.ts#1-1
|
|
317
|
+
new add function
|
|
318
|
+
"""
|
|
319
|
+
When the process is driven
|
|
320
|
+
Then the process rests at "build.review.await-review"
|
|
321
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
322
|
+
"""
|
|
323
|
+
# Review: abc1234
|
|
324
|
+
|
|
325
|
+
<!-- base: 0000000 -->
|
|
326
|
+
|
|
327
|
+
## calc
|
|
328
|
+
|
|
329
|
+
- [ ] ./src/calc.ts#1-1
|
|
330
|
+
new add function[^why]
|
|
331
|
+
|
|
332
|
+
[^why]:
|
|
333
|
+
- H: also export multiply
|
|
334
|
+
"""
|
|
335
|
+
And step "build.review.collecting" writes ".gtd/REQUIREMENTS.md" with:
|
|
336
|
+
"""
|
|
337
|
+
# Requirements
|
|
338
|
+
|
|
339
|
+
## Concerns
|
|
340
|
+
|
|
341
|
+
### Multiply
|
|
342
|
+
|
|
343
|
+
- Export `multiply()`.
|
|
344
|
+
"""
|
|
345
|
+
And step "build.review.collecting" writes ".gtd/REVIEW.md" with:
|
|
346
|
+
"""
|
|
347
|
+
# Review: abc1234
|
|
348
|
+
|
|
349
|
+
<!-- base: 0000000 -->
|
|
350
|
+
|
|
351
|
+
## calc
|
|
352
|
+
|
|
353
|
+
- [ ] ./src/calc.ts#1-1
|
|
354
|
+
new add function[^why]
|
|
355
|
+
|
|
356
|
+
[^why]:
|
|
357
|
+
- H: also export multiply
|
|
358
|
+
- A: noted in the requirements.
|
|
359
|
+
"""
|
|
360
|
+
When gtd lands
|
|
361
|
+
And the process is driven
|
|
362
|
+
Then the process rests at "build.review.await-review"
|
|
363
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
364
|
+
"""
|
|
365
|
+
# Review: abc1234
|
|
366
|
+
|
|
367
|
+
<!-- base: 0000000 -->
|
|
368
|
+
|
|
369
|
+
## calc
|
|
370
|
+
|
|
371
|
+
- [ ] ./src/calc.ts#1-1
|
|
372
|
+
new add function
|
|
373
|
+
"""
|
|
374
|
+
And step "build.review.closing" runs its script
|
|
375
|
+
And step "re-unwind" runs its script
|
|
376
|
+
When gtd lands
|
|
377
|
+
And the process is driven
|
|
378
|
+
Then the process rests at "implement.building"
|
|
379
|
+
And the commit subjects end with:
|
|
380
|
+
"""
|
|
381
|
+
gtd(check): re-unwind → design.triage
|
|
382
|
+
gtd(agent): design.triage → design.gate.answer
|
|
383
|
+
gtd(human): design.gate.answer → seam.naive
|
|
384
|
+
gtd(agent): seam.naive → seam.judge
|
|
385
|
+
gtd(judge): seam.judge → implement.building
|
|
386
|
+
gtd(agent): implement.building
|
|
387
|
+
"""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
installContext,
|
|
4
|
+
type Change,
|
|
5
|
+
type FlowContext,
|
|
6
|
+
type KeptTip,
|
|
7
|
+
type StepRequest,
|
|
8
|
+
} from "../../flows/index.js"
|
|
9
|
+
import { globMatches } from "../../wire/index.js"
|
|
10
|
+
import { fixtureContext } from "../shared/text.fixture.js"
|
|
11
|
+
import { judgeSeam, naivePass } from "./seam.js"
|
|
12
|
+
import { captureStep } from "../shared/text.fixture.js"
|
|
13
|
+
|
|
14
|
+
afterEach(() => installContext(undefined))
|
|
15
|
+
|
|
16
|
+
const change = (path: string, status: Change["status"]): Change => ({
|
|
17
|
+
path,
|
|
18
|
+
status,
|
|
19
|
+
before: status === "added" ? undefined : "a",
|
|
20
|
+
after: status === "deleted" ? undefined : "b",
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const tip = (...changes: Change[]): KeptTip =>
|
|
24
|
+
({
|
|
25
|
+
hash: "abc",
|
|
26
|
+
changes: Object.assign([...changes], { paths: [], get: () => undefined }),
|
|
27
|
+
}) as KeptTip
|
|
28
|
+
|
|
29
|
+
describe("judgeSeam", () => {
|
|
30
|
+
const judgeRequests = (kept: KeptTip, vars: Record<string, string> = {}) => {
|
|
31
|
+
const requests: StepRequest[] = []
|
|
32
|
+
installContext({
|
|
33
|
+
...fixtureContext({ vars }),
|
|
34
|
+
matches: globMatches,
|
|
35
|
+
step: async (request: StepRequest) => {
|
|
36
|
+
requests.push(request)
|
|
37
|
+
return { answers: { shape: { answer: "land", p: 0.9 } }, truncated: [] }
|
|
38
|
+
},
|
|
39
|
+
} as unknown as FlowContext)
|
|
40
|
+
return judgeSeam(kept).then(() => requests)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it.each([
|
|
44
|
+
["src/foo*bar.ts", "src/foobar.ts"],
|
|
45
|
+
["src/**", "src/deep/foobar.ts"],
|
|
46
|
+
])("added %s does not cover modified %s", async (added, sibling) => {
|
|
47
|
+
const requests = await judgeRequests(tip(change(added, "added"), change(sibling, "modified")))
|
|
48
|
+
const request = requests[0]
|
|
49
|
+
if (request?.kind !== "judge") throw new Error("expected a judge step")
|
|
50
|
+
expect(JSON.parse(request.evidence.strays as string)).toEqual([sibling])
|
|
51
|
+
expect(JSON.parse(request.evidence.inAllowlist as string)).toContain(added)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("splits seamAllow on commas, trims entries and drops blanks", async () => {
|
|
55
|
+
const requests = await judgeRequests(tip(change("src/x.ts", "modified")), {
|
|
56
|
+
seamAllow: " a/** , ,b.ts,, ",
|
|
57
|
+
})
|
|
58
|
+
const request = requests[0]
|
|
59
|
+
if (request?.kind !== "judge") throw new Error("expected a judge step")
|
|
60
|
+
expect(JSON.parse(request.evidence.allow as string)).toEqual(["a/**", "b.ts"])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("exempts only added paths: a deleted path stays a stray", async () => {
|
|
64
|
+
const requests = await judgeRequests(tip(change("src/gone.ts", "deleted")))
|
|
65
|
+
const request = requests[0]
|
|
66
|
+
if (request?.kind !== "judge") throw new Error("expected a judge step")
|
|
67
|
+
expect(JSON.parse(request.evidence.strays as string)).toEqual(["src/gone.ts"])
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("naivePass", () => {
|
|
72
|
+
const capture = (rejection?: { strays: string[]; p: number }) =>
|
|
73
|
+
captureStep(() => naivePass(rejection), { scope: "seam" })
|
|
74
|
+
|
|
75
|
+
it("keeps its turn under the step name seam.naive", async () => {
|
|
76
|
+
const request = await capture()
|
|
77
|
+
if (request.kind !== "agent") throw new Error("unreachable")
|
|
78
|
+
expect(request.name).toBe("naive")
|
|
79
|
+
expect(request.options).toMatchObject({ keep: true })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("carries the rejection into the retry prompt", async () => {
|
|
83
|
+
const request = await capture({ strays: ["src/core.ts"], p: 0.8 })
|
|
84
|
+
if (request.kind !== "agent") throw new Error("unreachable")
|
|
85
|
+
expect(request.prompt).toContain("src/core.ts")
|
|
86
|
+
expect(request.prompt).toContain("0.8")
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
changes,
|
|
3
|
+
human,
|
|
4
|
+
judgeDiffShape,
|
|
5
|
+
kept,
|
|
6
|
+
removeScript,
|
|
7
|
+
run,
|
|
8
|
+
scope,
|
|
9
|
+
seamGate,
|
|
10
|
+
vars,
|
|
11
|
+
type DiffShapeResult,
|
|
12
|
+
type KeptTip,
|
|
13
|
+
} from "../../flows/index.js"
|
|
14
|
+
import { healthy } from "../shared/health.js"
|
|
15
|
+
import { coder, FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
|
|
16
|
+
import * as t from "../shared/text.js"
|
|
17
|
+
import { defaults } from "../shared/vars.js"
|
|
18
|
+
import { refactorFirst } from "./refactor.js"
|
|
19
|
+
|
|
20
|
+
/** Naive-pass retries after a `reject`; the next `reject` rests at `seam.steer`. */
|
|
21
|
+
const NAIVE_RETRIES = 2
|
|
22
|
+
|
|
23
|
+
/** A change to an existing file outside the allowlist is a stray even when the task needs it: the judge decides. */
|
|
24
|
+
export const judgeSeam = (tip: KeptTip): Promise<DiffShapeResult> =>
|
|
25
|
+
judgeDiffShape("judge", {
|
|
26
|
+
changes: tip.changes,
|
|
27
|
+
allow: (vars.seamAllow ?? defaults.seamAllow!)
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((glob) => glob.trim())
|
|
30
|
+
.filter((glob) => glob !== ""),
|
|
31
|
+
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const naivePass = (rejection?: t.SeamRejection): Promise<void> =>
|
|
35
|
+
t.agentWithSkills("naive", t.naivePassPrompt(rejection), {
|
|
36
|
+
label: rejection === undefined ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
37
|
+
keep: true,
|
|
38
|
+
model: coder(),
|
|
39
|
+
system: t.builderSystem(),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const implement = (reference: string): Promise<void> =>
|
|
43
|
+
scope("implement", async () => {
|
|
44
|
+
await t.agentWithSkills("building", t.implementBuildingPrompt(reference), {
|
|
45
|
+
label: "Implementing",
|
|
46
|
+
model: coder(),
|
|
47
|
+
system: t.builderSystem(),
|
|
48
|
+
})
|
|
49
|
+
await healthy(() =>
|
|
50
|
+
t.agentWithSkills("fix.suite.fixing", t.implementFixPrompt(), {
|
|
51
|
+
label: "Fixing the check",
|
|
52
|
+
file: FEEDBACK,
|
|
53
|
+
model: coder(),
|
|
54
|
+
system: t.builderSystem(),
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
interface Decision {
|
|
61
|
+
readonly reference: string
|
|
62
|
+
/** Present only on `refactor-first`: the refactor runs before the implementation. */
|
|
63
|
+
readonly shape?: DiffShapeResult
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const decide = (): Promise<Decision> =>
|
|
67
|
+
scope("seam", async () => {
|
|
68
|
+
let rejection: t.SeamRejection | undefined
|
|
69
|
+
let retries = 0
|
|
70
|
+
for (;;) {
|
|
71
|
+
await naivePass(rejection)
|
|
72
|
+
const tip = kept("naive")!
|
|
73
|
+
const shape = await judgeSeam(tip)
|
|
74
|
+
if (shape.choice === "land") return { reference: tip.hash }
|
|
75
|
+
if (shape.choice === "refactor-first") {
|
|
76
|
+
await seamGate("approve", shape)
|
|
77
|
+
return { reference: tip.hash, shape }
|
|
78
|
+
}
|
|
79
|
+
rejection = { strays: shape.strays, p: shape.judgment?.answers.shape?.p ?? 0 }
|
|
80
|
+
if (retries < NAIVE_RETRIES) {
|
|
81
|
+
retries++
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
await human("steer", {
|
|
85
|
+
message: t.seamSteerMessage(rejection),
|
|
86
|
+
label: "Steer or overrule the rejected naive pass",
|
|
87
|
+
file: REQUIREMENTS,
|
|
88
|
+
acceptClean: true,
|
|
89
|
+
})
|
|
90
|
+
// Any edit steers, with a fresh budget and no stale rejection; clean overrules.
|
|
91
|
+
if (changes().length === 0) return { reference: tip.hash }
|
|
92
|
+
rejection = undefined
|
|
93
|
+
retries = 0
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
export const seamBuild = async (): Promise<void> => {
|
|
98
|
+
const { reference, shape } = await decide()
|
|
99
|
+
if (shape !== undefined && !(await refactorFirst(shape, { reference }))) {
|
|
100
|
+
// Unreadable strays were never judged, and no refactor runs: a person reads the diff.
|
|
101
|
+
await scope("seam", () =>
|
|
102
|
+
human("by-hand", {
|
|
103
|
+
message: t.seamByHandMessage(shape.strays, shape.unreadable, reference),
|
|
104
|
+
label: "Reviewing unreadable files by hand",
|
|
105
|
+
acceptClean: true,
|
|
106
|
+
}),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
await implement(reference)
|
|
110
|
+
}
|
package/src/workflows/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from "./feature/steps.js"
|
|
|
23
23
|
export * from "./feature/planning.js"
|
|
24
24
|
export * from "./feature/packages.js"
|
|
25
25
|
export * from "./feature/refactor.js"
|
|
26
|
+
export * from "./feature/seam.js"
|
|
26
27
|
export { buildTail, type ReviewOutcome } from "./feature/review.js"
|
|
27
28
|
export * from "./feature/feature.js"
|
|
28
29
|
export { base, review, reviewFlow } from "./review/review.js"
|
|
@@ -9,10 +9,21 @@ const { flagShipped } = options
|
|
|
9
9
|
describe("the shipped workflow factories", () => {
|
|
10
10
|
it("take exactly the settings the shipped workflows declare as options", () => {
|
|
11
11
|
expect(Object.keys(ShippedOptions.fields).sort()).toEqual(
|
|
12
|
-
[...Object.keys(defaults), ...Object.keys(envDefaults)]
|
|
12
|
+
[...Object.keys(defaults), ...Object.keys(envDefaults)]
|
|
13
|
+
.filter((name) => name !== "flag_naive_pass")
|
|
14
|
+
.sort(),
|
|
13
15
|
)
|
|
14
16
|
})
|
|
15
17
|
|
|
18
|
+
it("declare seamAllow and flag_naive_pass defaults; flag_naive_pass is no option", () => {
|
|
19
|
+
expect(defaults.flag_naive_pass).toBe("0")
|
|
20
|
+
expect(Object.keys(ShippedOptions.fields)).not.toContain("flag_naive_pass")
|
|
21
|
+
expect(configure(feature({ seamAllow: "docs/**" }))).toMatchObject({
|
|
22
|
+
ok: true,
|
|
23
|
+
build: { defaults: { seamAllow: "docs/**", flag_naive_pass: "0" } },
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
16
27
|
it("lay each option over the shipped setting of its own kind", () => {
|
|
17
28
|
const result = configure(feature({ qualityReviews: "correctness", testCommand: "npm run ci" }))
|
|
18
29
|
expect(result).toMatchObject({
|
|
@@ -29,6 +29,10 @@ describe("the bundled workflow's skills export", () => {
|
|
|
29
29
|
"refactor",
|
|
30
30
|
"refactor.fix.suite",
|
|
31
31
|
"refactor.health",
|
|
32
|
+
"seam",
|
|
33
|
+
"implement",
|
|
34
|
+
"implement.fix.suite",
|
|
35
|
+
"implement.health",
|
|
32
36
|
"build.quality.correctness",
|
|
33
37
|
"build.quality.owasp-security",
|
|
34
38
|
"build.quality.ponytail-review",
|
|
@@ -39,8 +43,18 @@ describe("the bundled workflow's skills export", () => {
|
|
|
39
43
|
)
|
|
40
44
|
})
|
|
41
45
|
|
|
42
|
-
it("keys no group scope", () => {
|
|
43
|
-
for (const group of [
|
|
46
|
+
it("keys no group scope or non-agent step", () => {
|
|
47
|
+
for (const group of [
|
|
48
|
+
"packages.item.fix",
|
|
49
|
+
"build.fix",
|
|
50
|
+
"build.review.fix",
|
|
51
|
+
"build.quality",
|
|
52
|
+
"implement.fix",
|
|
53
|
+
"seam.judge",
|
|
54
|
+
"seam.approve",
|
|
55
|
+
"seam.steer",
|
|
56
|
+
"seam.by-hand",
|
|
57
|
+
]) {
|
|
44
58
|
expect(bundled).not.toHaveProperty(group)
|
|
45
59
|
}
|
|
46
60
|
})
|
|
@@ -63,6 +77,10 @@ describe("the bundled workflow's skills export", () => {
|
|
|
63
77
|
refactor: ["ponytail"],
|
|
64
78
|
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
65
79
|
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
80
|
+
seam: ["incremental-implementation"],
|
|
81
|
+
implement: ["test-driven-development", "incremental-implementation"],
|
|
82
|
+
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
83
|
+
"implement.health": ["debugging-and-error-recovery"],
|
|
66
84
|
})
|
|
67
85
|
})
|
|
68
86
|
|
|
@@ -29,6 +29,12 @@ export const skills = (
|
|
|
29
29
|
refactor: ["ponytail"],
|
|
30
30
|
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
31
31
|
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
32
|
+
// Declared whether or not `naive_pass` is on, so a config keyed on them keeps
|
|
33
|
+
// loading when the flag flips off.
|
|
34
|
+
seam: ["incremental-implementation"],
|
|
35
|
+
implement: ["test-driven-development", "incremental-implementation"],
|
|
36
|
+
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
37
|
+
"implement.health": ["debugging-and-error-recovery"],
|
|
32
38
|
...lensScopes(
|
|
33
39
|
"build.quality",
|
|
34
40
|
lensesOf(vars.qualityReviews ?? ""),
|