@pmelab/gtd 39.0.0 → 41.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 +57 -55
- package/dist/chunks/{reserved-B6tTYjeb.mjs → reserved-Ch4LLrEj.mjs} +15 -9
- package/dist/chunks/{tail-_CZTP7Hu.mjs → tail-lY1UmJrU.mjs} +1 -1
- package/dist/workflow-kit.mjs +29 -11
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +98 -37
- 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 +140 -55
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -2
- package/src/workflow-kit/health.ts +42 -13
- package/src/workflows/feature/coverage.feature +5 -5
- package/src/workflows/feature/criteria.test.ts +20 -1
- package/src/workflows/feature/criteria.ts +24 -0
- package/src/workflows/feature/fast-path.feature +4 -4
- package/src/workflows/feature/feature.ts +82 -22
- package/src/workflows/feature/flag-mode.feature +2 -2
- package/src/workflows/feature/one-step-land.feature +4 -4
- package/src/workflows/feature/planning.ts +3 -2
- package/src/workflows/feature/quality-unresolved.feature +6 -6
- package/src/workflows/feature/refactor-absorbs.feature +3 -3
- package/src/workflows/feature/refactor-first.feature +5 -5
- package/src/workflows/feature/refactor-rebuild.feature +2 -2
- package/src/workflows/feature/refactor-review.feature +8 -8
- package/src/workflows/feature/review-gate.feature +271 -0
- package/src/workflows/feature/seam-rejection.feature +4 -4
- package/src/workflows/feature/seam.feature +2 -2
- package/src/workflows/feature/seam.ts +17 -7
- package/src/workflows/feature/slicing.feature +63 -4
- package/src/workflows/shared/health.test.ts +30 -0
- package/src/workflows/shared/health.ts +12 -9
- package/src/workflows/shared/progress.feature +124 -0
- package/src/workflows/shared/text.ts +7 -5
- package/src/workflows/review/review-door.feature +0 -121
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a feature process ends at the human review
|
|
3
|
+
|
|
4
|
+
After the last slice's quality lap the process rests at the review gate,
|
|
5
|
+
`.gtd/REVIEW.md`. A sign-off ends the process. A note asking for a change in
|
|
6
|
+
behaviour starts a feedback lap — design, then implement with no new naive
|
|
7
|
+
pass or seam judge, the coverage judge over every criterion, the quality lap
|
|
8
|
+
— and the review again. Its design keeps a built criterion's ID and numbers
|
|
9
|
+
new behaviour above every ID handed out.
|
|
10
|
+
|
|
11
|
+
Background:
|
|
12
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
13
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
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.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
61
|
+
"""
|
|
62
|
+
# Review: abc1234
|
|
63
|
+
|
|
64
|
+
<!-- base: 0000000 -->
|
|
65
|
+
|
|
66
|
+
## calc
|
|
67
|
+
|
|
68
|
+
- [ ] ./src/calc.ts#2-2
|
|
69
|
+
new subtract function
|
|
70
|
+
"""
|
|
71
|
+
And step "build.review.closing" runs its script
|
|
72
|
+
|
|
73
|
+
Scenario: a feature process ends at the review gate
|
|
74
|
+
When the process is driven
|
|
75
|
+
Then the process rests at "build.review.await-review"
|
|
76
|
+
And ".gtd/REQUIREMENTS.md" does not exist
|
|
77
|
+
And the resting step's content contains the hash of the commit "feat: add calculator"
|
|
78
|
+
And the commit subjects end with:
|
|
79
|
+
"""
|
|
80
|
+
gtd(judge): implement.coverage → implement.closing
|
|
81
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
82
|
+
gtd(agent): build.review.reviewing → build.review.await-review
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
Scenario: a sign-off ends the process at idle
|
|
86
|
+
When the process is driven
|
|
87
|
+
Then the process rests at "build.review.await-review"
|
|
88
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
89
|
+
"""
|
|
90
|
+
# Review: abc1234
|
|
91
|
+
|
|
92
|
+
<!-- base: 0000000 -->
|
|
93
|
+
|
|
94
|
+
## calc
|
|
95
|
+
|
|
96
|
+
- [x] ./src/calc.ts#2-2
|
|
97
|
+
new subtract function
|
|
98
|
+
"""
|
|
99
|
+
When gtd lands
|
|
100
|
+
And the process is driven
|
|
101
|
+
Then the process rests at "idle"
|
|
102
|
+
And ".gtd/REVIEW.md" does not exist
|
|
103
|
+
And the commit subjects end with:
|
|
104
|
+
"""
|
|
105
|
+
gtd(agent): build.review.reviewing → build.review.await-review
|
|
106
|
+
gtd(human): build.review.await-review → build.review.closing
|
|
107
|
+
gtd(check): build.review.closing → idle
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
Rule: a note asking for new behaviour runs the feedback lap
|
|
111
|
+
|
|
112
|
+
Background:
|
|
113
|
+
Given step "build.review.collecting" writes ".gtd/REQUIREMENTS.md" with:
|
|
114
|
+
"""
|
|
115
|
+
# Requirements
|
|
116
|
+
|
|
117
|
+
## Concerns
|
|
118
|
+
|
|
119
|
+
### Multiply
|
|
120
|
+
|
|
121
|
+
- Export `multiply()`.
|
|
122
|
+
"""
|
|
123
|
+
And step "build.review.collecting" writes ".gtd/REVIEW.md" with:
|
|
124
|
+
"""
|
|
125
|
+
# Review: abc1234
|
|
126
|
+
|
|
127
|
+
<!-- base: 0000000 -->
|
|
128
|
+
|
|
129
|
+
## calc
|
|
130
|
+
|
|
131
|
+
- [ ] ./src/calc.ts#2-2
|
|
132
|
+
new subtract function[^why]
|
|
133
|
+
|
|
134
|
+
[^why]:
|
|
135
|
+
- H: also export multiply
|
|
136
|
+
- A: noted in the requirements.
|
|
137
|
+
"""
|
|
138
|
+
And step "re-unwind" runs its script
|
|
139
|
+
When the process is driven
|
|
140
|
+
Then the process rests at "build.review.await-review"
|
|
141
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
142
|
+
"""
|
|
143
|
+
# Review: abc1234
|
|
144
|
+
|
|
145
|
+
<!-- base: 0000000 -->
|
|
146
|
+
|
|
147
|
+
## calc
|
|
148
|
+
|
|
149
|
+
- [ ] ./src/calc.ts#2-2
|
|
150
|
+
new subtract function[^why]
|
|
151
|
+
|
|
152
|
+
[^why]:
|
|
153
|
+
- H: also export multiply
|
|
154
|
+
"""
|
|
155
|
+
When gtd lands
|
|
156
|
+
And the process is driven
|
|
157
|
+
Then the process rests at "build.review.await-review"
|
|
158
|
+
And ".gtd/REQUIREMENTS.md" contains "multiply"
|
|
159
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
160
|
+
"""
|
|
161
|
+
# Review: abc1234
|
|
162
|
+
|
|
163
|
+
<!-- base: 0000000 -->
|
|
164
|
+
|
|
165
|
+
## calc
|
|
166
|
+
|
|
167
|
+
- [ ] ./src/calc.ts#2-2
|
|
168
|
+
new subtract function
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
Scenario: the lap keeps AC-1, adds AC-2, and covers both before the next review
|
|
172
|
+
Given step "design.triage" from now on changes nothing
|
|
173
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
174
|
+
"""
|
|
175
|
+
# Requirements
|
|
176
|
+
|
|
177
|
+
## Concerns
|
|
178
|
+
|
|
179
|
+
### Multiply
|
|
180
|
+
|
|
181
|
+
- `multiply(a, b)` returns `a * b`.
|
|
182
|
+
|
|
183
|
+
## Acceptance Criteria
|
|
184
|
+
|
|
185
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
186
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
187
|
+
"""
|
|
188
|
+
And step "implement.building" writes "src/calc.ts" with:
|
|
189
|
+
"""
|
|
190
|
+
export const add = (a: number, b: number) => a + b
|
|
191
|
+
export const subtract = (a: number, b: number) => a - b
|
|
192
|
+
export const multiply = (a: number, b: number) => a * b
|
|
193
|
+
"""
|
|
194
|
+
And step "implement.building" writes "src/multiply.test.ts" with:
|
|
195
|
+
"""
|
|
196
|
+
it("[AC-2] multiplies", () => {})
|
|
197
|
+
"""
|
|
198
|
+
And judge step "implement.coverage" answers:
|
|
199
|
+
"""
|
|
200
|
+
[{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
|
|
201
|
+
"""
|
|
202
|
+
When gtd lands
|
|
203
|
+
And the process is driven
|
|
204
|
+
Then the process rests at "build.review.await-review"
|
|
205
|
+
And ".gtd/REQUIREMENTS.md" does not exist
|
|
206
|
+
And the commit "gtd(judge): implement.coverage → implement.closing" has a body containing "Gtd-Mark: covers=AC-1"
|
|
207
|
+
And the commit subjects end with:
|
|
208
|
+
"""
|
|
209
|
+
gtd(human): build.review.await-review → build.review.closing
|
|
210
|
+
gtd(check): build.review.closing → re-unwind
|
|
211
|
+
gtd(check): re-unwind → design.triage
|
|
212
|
+
gtd(agent): design.triage → design.gate.answer
|
|
213
|
+
gtd(human): design.gate.answer → implement.building
|
|
214
|
+
gtd(agent): implement.building → implement.health.check
|
|
215
|
+
gtd(check): implement.health.check → implement.coverage
|
|
216
|
+
gtd(judge): implement.coverage → implement.closing
|
|
217
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
218
|
+
gtd(agent): build.review.reviewing → build.review.await-review
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
Scenario: a design numbering new behaviour at or below an earlier ID is refused
|
|
222
|
+
Given step "design.triage" from now on changes nothing
|
|
223
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
224
|
+
"""
|
|
225
|
+
# Requirements
|
|
226
|
+
|
|
227
|
+
## Concerns
|
|
228
|
+
|
|
229
|
+
### Multiply
|
|
230
|
+
|
|
231
|
+
- `multiply(a, b)` returns `a * b`.
|
|
232
|
+
|
|
233
|
+
## Acceptance Criteria
|
|
234
|
+
|
|
235
|
+
- AC-0: multiply(a, b) returns a * b.
|
|
236
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
237
|
+
"""
|
|
238
|
+
When gtd lands
|
|
239
|
+
Then driving the process is refused with "criteria-ids"
|
|
240
|
+
|
|
241
|
+
Scenario: the lap holds a new criterion to coverage
|
|
242
|
+
Given step "design.triage" from now on changes nothing
|
|
243
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
244
|
+
"""
|
|
245
|
+
# Requirements
|
|
246
|
+
|
|
247
|
+
## Concerns
|
|
248
|
+
|
|
249
|
+
### Multiply
|
|
250
|
+
|
|
251
|
+
- `multiply(a, b)` returns `a * b`.
|
|
252
|
+
|
|
253
|
+
## Acceptance Criteria
|
|
254
|
+
|
|
255
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
256
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
257
|
+
"""
|
|
258
|
+
And step "implement.building" writes "src/calc.ts" with:
|
|
259
|
+
"""
|
|
260
|
+
export const add = (a: number, b: number) => a + b
|
|
261
|
+
export const subtract = (a: number, b: number) => a - b
|
|
262
|
+
export const multiply = (a: number, b: number) => a * b
|
|
263
|
+
"""
|
|
264
|
+
And judge step "implement.coverage" answers:
|
|
265
|
+
"""
|
|
266
|
+
[{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"no","p":0.9}]
|
|
267
|
+
"""
|
|
268
|
+
When gtd lands
|
|
269
|
+
And the process is driven
|
|
270
|
+
Then the process rests at "implement.covering"
|
|
271
|
+
And the resting step's content contains "AC-2"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# One SDLC path per file, so a changed path reds exactly one fixture — do not merge.
|
|
2
2
|
@inmem
|
|
3
|
-
Feature: rejected naive passes walk one fixed path through the steer gate to
|
|
3
|
+
Feature: rejected naive passes walk one fixed path through the steer gate to the review gate
|
|
4
4
|
|
|
5
5
|
With GTD_FLAG_RISK_TIER off, three rejected naive
|
|
6
6
|
passes rest at the steer gate, where a human edit restarts the seam and the
|
|
@@ -40,7 +40,7 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
|
|
|
40
40
|
"""
|
|
41
41
|
And step "design.gate.answer" changes nothing
|
|
42
42
|
|
|
43
|
-
Scenario: three rejects rest at the steer gate, a steered rerun lands and runs to
|
|
43
|
+
Scenario: three rejects rest at the steer gate, a steered rerun lands and runs to the review gate
|
|
44
44
|
Given step "seam.check" changes nothing
|
|
45
45
|
And step "seam.naive" writes "src/acceptance.test.ts" with:
|
|
46
46
|
"""
|
|
@@ -100,7 +100,7 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
|
|
|
100
100
|
And step "implement.applying" runs its script
|
|
101
101
|
When gtd lands
|
|
102
102
|
And the process is driven
|
|
103
|
-
Then the process rests at "
|
|
103
|
+
Then the process rests at "build.review.reviewing"
|
|
104
104
|
And the commit subjects end with:
|
|
105
105
|
"""
|
|
106
106
|
gtd(human): idle → unwind
|
|
@@ -124,7 +124,7 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
|
|
|
124
124
|
gtd(check): implement.applying → implement.health.check
|
|
125
125
|
gtd(check): implement.health.check → implement.coverage
|
|
126
126
|
gtd(judge): implement.coverage → implement.closing
|
|
127
|
-
gtd(check): implement.closing →
|
|
127
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
128
128
|
"""
|
|
129
129
|
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
130
130
|
And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
|
|
@@ -152,7 +152,7 @@ Feature: the naive pass and seam judge decide how a feature is built
|
|
|
152
152
|
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
153
153
|
"""
|
|
154
154
|
When the process is driven
|
|
155
|
-
Then the process rests at "
|
|
155
|
+
Then the process rests at "build.review.reviewing"
|
|
156
156
|
And "src/calc.ts" contains "subtract"
|
|
157
157
|
And the commit subjects end with:
|
|
158
158
|
"""
|
|
@@ -160,7 +160,7 @@ Feature: the naive pass and seam judge decide how a feature is built
|
|
|
160
160
|
gtd(check): implement.applying → implement.health.check
|
|
161
161
|
gtd(check): implement.health.check → implement.coverage
|
|
162
162
|
gtd(judge): implement.coverage → implement.closing
|
|
163
|
-
gtd(check): implement.closing →
|
|
163
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
164
164
|
"""
|
|
165
165
|
And ".gtd/REQUIREMENTS.md" does not exist
|
|
166
166
|
|
|
@@ -283,7 +283,7 @@ export interface ImplementOptions {
|
|
|
283
283
|
/** Where the slice began: the coverage judge reads only the tests changed since. */
|
|
284
284
|
readonly base?: string | undefined
|
|
285
285
|
/** Not the last slice: `.gtd/REQUIREMENTS.md` and `.gtd/SLICES.md` stay for the next one. */
|
|
286
|
-
readonly more?: boolean | undefined
|
|
286
|
+
readonly more?: boolean | (() => boolean) | undefined
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
/** Resolves `false` when the tip no longer applies (the failure is in `.gtd/FEEDBACK.md`) or has nothing outside `.gtd/` to apply. */
|
|
@@ -351,14 +351,19 @@ export function implement(
|
|
|
351
351
|
tier?: RiskTier,
|
|
352
352
|
amend?: boolean,
|
|
353
353
|
): Promise<void>
|
|
354
|
-
export function implement(
|
|
354
|
+
export async function implement(
|
|
355
355
|
first?: string | ImplementOptions,
|
|
356
356
|
tier?: RiskTier,
|
|
357
357
|
amend?: boolean,
|
|
358
358
|
): Promise<void> {
|
|
359
|
-
|
|
360
|
-
typeof first === "object" && first !== null ? first : { reference: first, tier, amend }
|
|
361
|
-
|
|
359
|
+
await implementing(
|
|
360
|
+
typeof first === "object" && first !== null ? first : { reference: first, tier, amend },
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** `implement`, resolving with the criterion IDs the requirements listed when it closed. */
|
|
365
|
+
export const implementing = (options: ImplementOptions): Promise<readonly string[]> =>
|
|
366
|
+
scope("implement", async () => {
|
|
362
367
|
const since = head()
|
|
363
368
|
const acceptance = options.rebuild?.acceptance
|
|
364
369
|
const built = options.naive === undefined || !(await applied(options.naive))
|
|
@@ -372,9 +377,14 @@ export function implement(
|
|
|
372
377
|
built && acceptance !== undefined ? () => requireAcceptanceKept(acceptance) : undefined,
|
|
373
378
|
)
|
|
374
379
|
await requireCoverage(options.base, options.slice)
|
|
375
|
-
|
|
380
|
+
const ids = criterionIds(read(REQUIREMENTS) ?? "")
|
|
381
|
+
await closing(moreToBuild(options.more))
|
|
382
|
+
return ids
|
|
376
383
|
})
|
|
377
|
-
|
|
384
|
+
|
|
385
|
+
// Read at closing: a narrowing at `implement.oversized` may have emptied every later slice.
|
|
386
|
+
const moreToBuild = (more: ImplementOptions["more"]): boolean =>
|
|
387
|
+
typeof more === "function" ? more() : more === true
|
|
378
388
|
|
|
379
389
|
/** The last slice's closing, for a last slice a steer left with no criterion to build. */
|
|
380
390
|
export const closeOut = (): Promise<void> => scope("implement", () => closing(false))
|
|
@@ -51,7 +51,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
|
|
|
51
51
|
[{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
|
|
52
52
|
"""
|
|
53
53
|
When the process is driven
|
|
54
|
-
Then the process rests at "
|
|
54
|
+
Then the process rests at "build.review.reviewing"
|
|
55
55
|
And the commit subjects end with:
|
|
56
56
|
"""
|
|
57
57
|
gtd(human): design.gate.answer → seam.naive
|
|
@@ -60,7 +60,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
|
|
|
60
60
|
gtd(check): implement.applying → implement.health.check
|
|
61
61
|
gtd(check): implement.health.check → implement.coverage
|
|
62
62
|
gtd(judge): implement.coverage → implement.closing
|
|
63
|
-
gtd(check): implement.closing →
|
|
63
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
64
64
|
"""
|
|
65
65
|
And the output lines of gtd "slices" start with:
|
|
66
66
|
"""
|
|
@@ -112,7 +112,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
|
|
|
112
112
|
[{"id":"AC-2","answer":"yes","p":0.9}]
|
|
113
113
|
"""
|
|
114
114
|
When the process is driven
|
|
115
|
-
Then the process rests at "
|
|
115
|
+
Then the process rests at "build.review.reviewing"
|
|
116
116
|
And the commit subjects end with:
|
|
117
117
|
"""
|
|
118
118
|
gtd(agent): design.triage → design.gate.answer
|
|
@@ -129,7 +129,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
|
|
|
129
129
|
gtd(check): implement.applying → implement.health.check
|
|
130
130
|
gtd(check): implement.health.check → implement.coverage
|
|
131
131
|
gtd(judge): implement.coverage → implement.closing
|
|
132
|
-
gtd(check): implement.closing →
|
|
132
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
133
133
|
"""
|
|
134
134
|
And the output lines of gtd "slices" start with:
|
|
135
135
|
"""
|
|
@@ -223,3 +223,62 @@ Feature: a change with more criteria than the cap is built in coarse slices
|
|
|
223
223
|
- AC-2
|
|
224
224
|
"""
|
|
225
225
|
Then driving the process is refused with "wrote outside its write access"
|
|
226
|
+
|
|
227
|
+
Scenario: a steer that empties the last slice still ends at the review of the whole change
|
|
228
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
229
|
+
"""
|
|
230
|
+
# Requirements
|
|
231
|
+
|
|
232
|
+
## Acceptance Criteria
|
|
233
|
+
|
|
234
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
235
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
236
|
+
- AC-3: divide(a, b) returns a / b.
|
|
237
|
+
"""
|
|
238
|
+
And step "design.slicing" writes ".gtd/SLICES.md" with:
|
|
239
|
+
"""
|
|
240
|
+
# Slices
|
|
241
|
+
|
|
242
|
+
## Subtract and divide
|
|
243
|
+
|
|
244
|
+
- AC-1
|
|
245
|
+
- AC-3
|
|
246
|
+
|
|
247
|
+
## Multiply
|
|
248
|
+
|
|
249
|
+
- AC-2
|
|
250
|
+
"""
|
|
251
|
+
And step "seam.naive" writes "src/sub.test.ts" with:
|
|
252
|
+
"""
|
|
253
|
+
it("[AC-1] subtracts", () => {})
|
|
254
|
+
"""
|
|
255
|
+
And judge step "implement.coverage" answers:
|
|
256
|
+
"""
|
|
257
|
+
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
258
|
+
"""
|
|
259
|
+
When the process is driven
|
|
260
|
+
Then the process rests at "seam.steer"
|
|
261
|
+
Given a file ".gtd/REQUIREMENTS.md" with:
|
|
262
|
+
"""
|
|
263
|
+
# Requirements
|
|
264
|
+
|
|
265
|
+
## Acceptance Criteria
|
|
266
|
+
|
|
267
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
268
|
+
"""
|
|
269
|
+
When gtd lands
|
|
270
|
+
And the process is driven
|
|
271
|
+
Then the process rests at "build.review.reviewing"
|
|
272
|
+
And the commit subjects end with:
|
|
273
|
+
"""
|
|
274
|
+
gtd(human): seam.steer → seam.naive
|
|
275
|
+
gtd(agent): seam.naive → seam.check
|
|
276
|
+
gtd(check): seam.check → implement.applying
|
|
277
|
+
gtd(check): implement.applying → implement.health.check
|
|
278
|
+
gtd(check): implement.health.check → implement.coverage
|
|
279
|
+
gtd(judge): implement.coverage → implement.closing
|
|
280
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
281
|
+
"""
|
|
282
|
+
And ".gtd/REQUIREMENTS.md" does not exist
|
|
283
|
+
And ".gtd/SLICES.md" does not exist
|
|
284
|
+
And the output of gtd "slices" does not contain "feature-2"
|
|
@@ -116,3 +116,33 @@ describe("healthy describeGuard", () => {
|
|
|
116
116
|
).rejects.toThrow("guarded: nope")
|
|
117
117
|
})
|
|
118
118
|
})
|
|
119
|
+
|
|
120
|
+
describe("healthy at the fix cap", () => {
|
|
121
|
+
// Blank reports are not comparable; at an 8-byte budget the cut trims both.
|
|
122
|
+
it.each([
|
|
123
|
+
["could not compare", "", (n: number) => "\n".repeat(n)],
|
|
124
|
+
["saw only cut", `, judgeBudgetBytes: "8"`, (n: number) => `a long failing report ${n}\n`],
|
|
125
|
+
])("grants no extra turn on progress the judge %s", async (_, extra, report) => {
|
|
126
|
+
let runs = 0
|
|
127
|
+
const repo = testRepo({
|
|
128
|
+
workflow: [
|
|
129
|
+
`import { healthy } from "@pmelab/gtd/workflows"`,
|
|
130
|
+
`import { agent } from "@pmelab/gtd/flows"`,
|
|
131
|
+
`export const defaults = { judgeIdenticalMinP: "0.7"${extra} }`,
|
|
132
|
+
`export const held = async () => { await agent("first", "go"); await healthy(() => agent("fix", "go")) }`,
|
|
133
|
+
`export default held`,
|
|
134
|
+
].join("\n"),
|
|
135
|
+
env: { GTD_TEST_COMMAND: "true" },
|
|
136
|
+
})
|
|
137
|
+
await repo.start("held")
|
|
138
|
+
const rest = await repo.drive({
|
|
139
|
+
first: (_, r) => r.write("start", "1\n"),
|
|
140
|
+
"health.check": (_, r) => r.write(".gtd/FEEDBACK.md", report(++runs)),
|
|
141
|
+
"health.judge": () => '[{"id":"verdict","answer":"progress","p":0.9}]',
|
|
142
|
+
fix: (_, r) => r.write("f", `${runs}\n`),
|
|
143
|
+
"health.recheck": (_, r) => r.write(".gtd/FEEDBACK.md", "red again\n"),
|
|
144
|
+
})
|
|
145
|
+
expect(rest.state).toBe("health.describe")
|
|
146
|
+
expect(repo.subjects().filter((s) => s.startsWith("gtd(agent): fix")).length).toBe(3)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -105,7 +105,10 @@ export const retryQuestion = (comparable: boolean): JudgeQuestion =>
|
|
|
105
105
|
"new-failure: a materially different failure than before. progress: still red, but closer to green — the default if genuinely unsure.",
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
const
|
|
108
|
+
const compare = async (
|
|
109
|
+
previous: string,
|
|
110
|
+
current: string,
|
|
111
|
+
): Promise<kit.RetryVerdict | undefined> => {
|
|
109
112
|
const comparable = current.trim() !== "" && previous.trim() !== ""
|
|
110
113
|
const { answers, truncated } = await judge("health.judge", {
|
|
111
114
|
questions: [retryQuestion(comparable)],
|
|
@@ -113,12 +116,11 @@ const sameFailure = async (previous: string, current: string): Promise<boolean>
|
|
|
113
116
|
message: t.healthJudgeMessage(),
|
|
114
117
|
label: "Judging the retry",
|
|
115
118
|
})
|
|
116
|
-
// Evidence the budget cut can make two different reports look alike:
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
119
|
+
// Evidence the budget cut can make two different reports look alike: neither sameness nor progress.
|
|
120
|
+
if (!comparable || truncated.length > 0) return undefined
|
|
121
|
+
const minP = numeric(vars.judgeIdenticalMinP, Infinity)
|
|
122
|
+
if (answered(answers.verdict, "identical", minP)) return "identical"
|
|
123
|
+
return answered(answers.verdict, "progress", minP) ? "progress" : undefined
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
export interface HealthOptions {
|
|
@@ -153,7 +155,8 @@ const runSuite = (options: HealthOptions): Promise<boolean> =>
|
|
|
153
155
|
/**
|
|
154
156
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
155
157
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
156
|
-
* identical to the previous round's.
|
|
158
|
+
* identical to the previous round's. A judge calling it progress at the cap
|
|
159
|
+
* buys one extra fix per run of fixes. An escalation re-runs the suite once
|
|
157
160
|
* first; a green re-run ends the loop.
|
|
158
161
|
*/
|
|
159
162
|
export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}): Promise<void> =>
|
|
@@ -162,7 +165,7 @@ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}):
|
|
|
162
165
|
report: FEEDBACK,
|
|
163
166
|
fixCap: FIX_CAP,
|
|
164
167
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
165
|
-
|
|
168
|
+
compare,
|
|
166
169
|
escalations: options.escalations ?? { rounds: 0 },
|
|
167
170
|
recheck: () =>
|
|
168
171
|
recheck("health.recheck", "health.check", { sweepOnGreen: healthSweep(options) }),
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a fix loop still making progress at the cap gets one extra turn
|
|
3
|
+
|
|
4
|
+
`healthy` escalates once three fix turns are spent. When `health.judge`
|
|
5
|
+
calls that round progress — still red, but closer to green — with at least
|
|
6
|
+
`judgeIdenticalMinP` confidence, one more fix runs first, once per run of
|
|
7
|
+
fixes. The round after it escalates without asking the judge. Any other
|
|
8
|
+
verdict escalates at the cap as before.
|
|
9
|
+
|
|
10
|
+
Background:
|
|
11
|
+
Given a gtd config with:
|
|
12
|
+
"""
|
|
13
|
+
import { healthy } from "@pmelab/gtd/workflows"
|
|
14
|
+
import { agent } from "@pmelab/gtd/flows"
|
|
15
|
+
|
|
16
|
+
export const defaults = { judgeIdenticalMinP: "0.7" }
|
|
17
|
+
export const held = async () => {
|
|
18
|
+
await agent("first", "go")
|
|
19
|
+
await healthy(() => agent("fix", "go"))
|
|
20
|
+
}
|
|
21
|
+
export default held
|
|
22
|
+
"""
|
|
23
|
+
And step "first" writes "src/a.ts" with:
|
|
24
|
+
"""
|
|
25
|
+
export const a = 1
|
|
26
|
+
"""
|
|
27
|
+
And step "health.check" writes ".gtd/FEEDBACK.md" with:
|
|
28
|
+
"""
|
|
29
|
+
tests failing
|
|
30
|
+
"""
|
|
31
|
+
And step "fix" deletes ".gtd/FEEDBACK.md"
|
|
32
|
+
And step "health.recheck" writes ".gtd/FEEDBACK.md" with:
|
|
33
|
+
"""
|
|
34
|
+
tests still failing
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
Scenario: progress at the cap buys exactly one extra fix turn
|
|
38
|
+
Given judge step "health.judge" answers:
|
|
39
|
+
"""
|
|
40
|
+
[{"id":"verdict","answer":"progress","p":0.9}]
|
|
41
|
+
"""
|
|
42
|
+
When gtd starts workflow "held"
|
|
43
|
+
And the process is driven
|
|
44
|
+
Then the process rests at "health.describe"
|
|
45
|
+
And the commit subjects end with:
|
|
46
|
+
"""
|
|
47
|
+
gtd(agent): first → health.check
|
|
48
|
+
gtd(check): health.check → fix
|
|
49
|
+
gtd(agent): fix → health.check
|
|
50
|
+
gtd(check): health.check → health.judge
|
|
51
|
+
gtd(judge): health.judge → fix
|
|
52
|
+
gtd(agent): fix → health.check
|
|
53
|
+
gtd(check): health.check → health.judge
|
|
54
|
+
gtd(judge): health.judge → fix
|
|
55
|
+
gtd(agent): fix → health.check
|
|
56
|
+
gtd(check): health.check → health.judge
|
|
57
|
+
gtd(judge): health.judge → fix
|
|
58
|
+
gtd(agent): fix → health.check
|
|
59
|
+
gtd(check): health.check → health.recheck
|
|
60
|
+
gtd(check): health.recheck → health.describe
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
Scenario Outline: <verdict> at <p> escalates at the cap without an extra turn
|
|
64
|
+
Given judge step "health.judge" answers:
|
|
65
|
+
"""
|
|
66
|
+
[{"id":"verdict","answer":"<verdict>","p":<p>}]
|
|
67
|
+
"""
|
|
68
|
+
When gtd starts workflow "held"
|
|
69
|
+
And the process is driven
|
|
70
|
+
Then the process rests at "health.describe"
|
|
71
|
+
And the commit subjects end with:
|
|
72
|
+
"""
|
|
73
|
+
gtd(agent): first → health.check
|
|
74
|
+
gtd(check): health.check → fix
|
|
75
|
+
gtd(agent): fix → health.check
|
|
76
|
+
gtd(check): health.check → health.judge
|
|
77
|
+
gtd(judge): health.judge → fix
|
|
78
|
+
gtd(agent): fix → health.check
|
|
79
|
+
gtd(check): health.check → health.judge
|
|
80
|
+
gtd(judge): health.judge → fix
|
|
81
|
+
gtd(agent): fix → health.check
|
|
82
|
+
gtd(check): health.check → health.judge
|
|
83
|
+
gtd(judge): health.judge → health.recheck
|
|
84
|
+
gtd(check): health.recheck → health.describe
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
| verdict | p |
|
|
89
|
+
| new-failure | 0.9 |
|
|
90
|
+
| progress | 0.5 |
|
|
91
|
+
|
|
92
|
+
Scenario: a person's escalation stop starts a new run of fixes with its own extra turn
|
|
93
|
+
Given judge step "health.judge" answers:
|
|
94
|
+
"""
|
|
95
|
+
[{"id":"verdict","answer":"progress","p":0.9}]
|
|
96
|
+
"""
|
|
97
|
+
And step "health.describe" writes ".gtd/ESCALATION.md" with:
|
|
98
|
+
"""
|
|
99
|
+
try something else
|
|
100
|
+
"""
|
|
101
|
+
And step "health.stop" writes "NOTES.md" with:
|
|
102
|
+
"""
|
|
103
|
+
narrowed
|
|
104
|
+
"""
|
|
105
|
+
When gtd starts workflow "held"
|
|
106
|
+
And the process is driven
|
|
107
|
+
Then the process rests at "health.exhausted"
|
|
108
|
+
And the commit subjects end with:
|
|
109
|
+
"""
|
|
110
|
+
gtd(agent): health.describe → health.stop
|
|
111
|
+
gtd(human): health.stop → fix
|
|
112
|
+
gtd(agent): fix → health.check
|
|
113
|
+
gtd(check): health.check → health.judge
|
|
114
|
+
gtd(judge): health.judge → fix
|
|
115
|
+
gtd(agent): fix → health.check
|
|
116
|
+
gtd(check): health.check → health.judge
|
|
117
|
+
gtd(judge): health.judge → fix
|
|
118
|
+
gtd(agent): fix → health.check
|
|
119
|
+
gtd(check): health.check → health.judge
|
|
120
|
+
gtd(judge): health.judge → fix
|
|
121
|
+
gtd(agent): fix → health.check
|
|
122
|
+
gtd(check): health.check → health.recheck
|
|
123
|
+
gtd(check): health.recheck → health.exhausted
|
|
124
|
+
"""
|