@pmelab/gtd 37.0.0 → 38.1.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 +15 -0
- package/dist/chunks/{reserved-zAfoJtR7.mjs → reserved-CfQ8eurM.mjs} +85 -17
- package/dist/chunks/{tail-BRC4DGdV.mjs → tail-C7yaWvaS.mjs} +1 -1
- package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
- package/dist/gtd.bundle.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +318 -76
- 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 +419 -109
- package/package.json +1 -1
- package/src/testkit/steps.ts +39 -0
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/feature/continuation-cap.feature +65 -0
- package/src/workflows/feature/continuation-rebuild.feature +88 -0
- package/src/workflows/feature/continuation-slices.feature +64 -0
- package/src/workflows/feature/continuation.feature +205 -0
- package/src/workflows/feature/coverage.ts +14 -6
- package/src/workflows/feature/criteria.test.ts +17 -1
- package/src/workflows/feature/criteria.ts +37 -3
- package/src/workflows/feature/feature.ts +64 -15
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor.ts +64 -30
- package/src/workflows/feature/seam.ts +157 -48
- package/src/workflows/feature/slicing-cap.feature +14 -0
- package/src/workflows/feature/slicing.feature +225 -0
- package/src/workflows/feature/slicing.test.ts +49 -0
- package/src/workflows/feature/slicing.ts +105 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/continuation.ts +92 -0
- package/src/workflows/shared/fences.ts +5 -0
- package/src/workflows/shared/options.ts +3 -0
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/text.test.ts +6 -2
- package/src/workflows/shared/text.ts +153 -22
- package/src/workflows/shared/vars.ts +6 -0
- package/src/workflows/shared/workflows.steps.ts +13 -0
package/package.json
CHANGED
package/src/testkit/steps.ts
CHANGED
|
@@ -26,6 +26,8 @@ export class TestkitWorld extends QuickPickleWorld {
|
|
|
26
26
|
/** Per step, what its scripted actor does, in the order the scenario declared it. */
|
|
27
27
|
actions = new Map<string, Actor[]>()
|
|
28
28
|
verdicts = new Map<string, string>()
|
|
29
|
+
/** Per judge step, a verdict for each text its questions may contain; the first match answers. */
|
|
30
|
+
conditionalVerdicts = new Map<string, [text: string, verdict: string][]>()
|
|
29
31
|
private created: TestRepo | undefined = undefined
|
|
30
32
|
|
|
31
33
|
// The repository is created on first use and keeps the config and
|
|
@@ -79,6 +81,19 @@ export class TestkitWorld extends QuickPickleWorld {
|
|
|
79
81
|
}
|
|
80
82
|
actors[step] = () => verdict
|
|
81
83
|
}
|
|
84
|
+
for (const [step, choices] of this.conditionalVerdicts) {
|
|
85
|
+
if (this.actions.has(step) || this.verdicts.has(step)) {
|
|
86
|
+
throw new Error(`step "${step}" has a conditional verdict and another script`)
|
|
87
|
+
}
|
|
88
|
+
// Matched against the pending questions, which `gtd judge` prints and the beat does not.
|
|
89
|
+
actors[step] = async (_, repo) => {
|
|
90
|
+
const { stdout } = await repo.gtd("judge")
|
|
91
|
+
const hit = choices.find(([text]) => stdout.includes(text))
|
|
92
|
+
if (hit === undefined)
|
|
93
|
+
throw new Error(`judge step "${step}" matched no verdict:\n${stdout}`)
|
|
94
|
+
return hit[1]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
82
97
|
await this.repo().drive(actors)
|
|
83
98
|
}
|
|
84
99
|
}
|
|
@@ -137,6 +152,20 @@ Given(
|
|
|
137
152
|
},
|
|
138
153
|
)
|
|
139
154
|
|
|
155
|
+
// Lets one step write differently per visit, told apart by what it is prompted with.
|
|
156
|
+
Given(
|
|
157
|
+
"step {string}, when prompted with {string}, writes {string} with:",
|
|
158
|
+
(world: TestkitWorld, step: string, text: string, path: string, content: string) => {
|
|
159
|
+
world.act(step, (beat, repo) => {
|
|
160
|
+
if (beat.content.includes(text)) repo.write(path, withNewline(content))
|
|
161
|
+
})
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
Given("step {string} deletes {string}", (world: TestkitWorld, step: string, path: string) => {
|
|
166
|
+
world.act(step, (_, repo) => repo.remove(path))
|
|
167
|
+
})
|
|
168
|
+
|
|
140
169
|
// A standing assertion like any queued action: it applies on every later visit
|
|
141
170
|
// to the step until `from now on` clears the list.
|
|
142
171
|
Given(
|
|
@@ -169,6 +198,16 @@ Given("judge step {string} answers:", (world: TestkitWorld, step: string, verdic
|
|
|
169
198
|
world.verdicts.set(step, String(verdict))
|
|
170
199
|
})
|
|
171
200
|
|
|
201
|
+
Given(
|
|
202
|
+
"judge step {string}, when its questions contain {string}, answers:",
|
|
203
|
+
(world: TestkitWorld, step: string, text: string, verdict: string) => {
|
|
204
|
+
world.conditionalVerdicts.set(step, [
|
|
205
|
+
...(world.conditionalVerdicts.get(step) ?? []),
|
|
206
|
+
[text, String(verdict)],
|
|
207
|
+
])
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
|
|
172
211
|
When("gtd starts workflow {string}", async (world: TestkitWorld, workflow: string) => {
|
|
173
212
|
await world.repo().start(workflow)
|
|
174
213
|
})
|
package/src/wire/ContextLimit.ts
CHANGED
|
@@ -156,7 +156,7 @@ export const contextHook = (input: {
|
|
|
156
156
|
if (tokens >= limit.soft * window) {
|
|
157
157
|
return {
|
|
158
158
|
kind: "soft",
|
|
159
|
-
reason: `Context limit: ${used} used, past the ${percent(limit.soft)} soft limit. Stop working now. Do not start anything new; end your turn with a short handoff note as your answer: what you touched, and what was next.`,
|
|
159
|
+
reason: `Context limit: ${used} used, past the ${percent(limit.soft)} soft limit. Stop working now. Do not start anything new; end your turn with a short handoff note as your answer: what you touched, and what was next. Where your instructions name a file for that note, write it there too.`,
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
return undefined
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: past its continuation cap a phase rests for a person
|
|
3
|
+
|
|
4
|
+
With `continuations` at 1, a naive pass stopped at its context limit gets
|
|
5
|
+
one continuation; when that one stops too, `seam.oversized` asks a person,
|
|
6
|
+
and landing there grants a fresh round.
|
|
7
|
+
|
|
8
|
+
Background:
|
|
9
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
10
|
+
And an environment variable "GTD_CONTINUATIONS" set to "1"
|
|
11
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
12
|
+
And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
|
|
13
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
14
|
+
And a file "src/calc.ts" with:
|
|
15
|
+
"""
|
|
16
|
+
export const add = (a: number, b: number) => a + b
|
|
17
|
+
"""
|
|
18
|
+
And the working tree is committed as "feat: add calculator"
|
|
19
|
+
And a file "TODO.md" with:
|
|
20
|
+
"""
|
|
21
|
+
# Add subtract and multiply
|
|
22
|
+
"""
|
|
23
|
+
And step "unwind" changes nothing
|
|
24
|
+
And step "start-gate.check" changes nothing
|
|
25
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
26
|
+
"""
|
|
27
|
+
# Requirements
|
|
28
|
+
|
|
29
|
+
## Acceptance Criteria
|
|
30
|
+
|
|
31
|
+
- AC-1: `subtract(5, 3)` returns 2.
|
|
32
|
+
- AC-2: `multiply(2, 3)` returns 6.
|
|
33
|
+
"""
|
|
34
|
+
And step "design.gate.answer" changes nothing
|
|
35
|
+
And step "seam.check" changes nothing
|
|
36
|
+
|
|
37
|
+
Scenario: past the continuation cap the naive pass rests for a person, and landing there grants a new round
|
|
38
|
+
Given step "seam.naive" writes ".gtd/HANDOFF.md" with:
|
|
39
|
+
"""
|
|
40
|
+
Roughed out subtract.
|
|
41
|
+
"""
|
|
42
|
+
And step "seam.continue.naive" writes "tests/calc.test.ts" with:
|
|
43
|
+
"""
|
|
44
|
+
it("[AC-1] subtracts", () => {})
|
|
45
|
+
"""
|
|
46
|
+
And step "seam.continue.naive" writes ".gtd/HANDOFF.md" with:
|
|
47
|
+
"""
|
|
48
|
+
Subtract is tested; multiply was next.
|
|
49
|
+
"""
|
|
50
|
+
When the process is driven
|
|
51
|
+
Then the process rests at "seam.oversized"
|
|
52
|
+
And the resting step's content contains "1 continuation ran and the last still stopped at its context limit"
|
|
53
|
+
And the resting step's content contains "still open: AC-2"
|
|
54
|
+
|
|
55
|
+
When gtd lands
|
|
56
|
+
Then the process rests at "seam.continue.naive"
|
|
57
|
+
And the resting step's content contains "Still open: AC-2."
|
|
58
|
+
And the commit subjects end with:
|
|
59
|
+
"""
|
|
60
|
+
gtd(human): design.gate.answer → seam.naive
|
|
61
|
+
gtd(agent): seam.naive → seam.continue.naive
|
|
62
|
+
gtd(agent): seam.continue.naive → seam.oversized
|
|
63
|
+
gtd(human): seam.oversized → seam.continue.naive
|
|
64
|
+
"""
|
|
65
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a rebuild stopped at its context limit is continued on the branch
|
|
3
|
+
|
|
4
|
+
The rebuild's snapshot is its own commit: the continuation finds the work
|
|
5
|
+
in the tree, and the health loop greens the whole build afterwards.
|
|
6
|
+
`seamAllow` is empty here, so the naive pass's change to an existing test
|
|
7
|
+
file is the judge's only stray and refactor-first rebuilds with no refactor.
|
|
8
|
+
|
|
9
|
+
Background:
|
|
10
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
11
|
+
And an environment variable "GTD_SEAMALLOW" set to ""
|
|
12
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
13
|
+
And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
|
|
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 a file "tests/calc.test.ts" with:
|
|
20
|
+
"""
|
|
21
|
+
it("adds", () => {})
|
|
22
|
+
"""
|
|
23
|
+
And the working tree is committed as "feat: add calculator"
|
|
24
|
+
And a file "TODO.md" with:
|
|
25
|
+
"""
|
|
26
|
+
# Add subtract and multiply
|
|
27
|
+
"""
|
|
28
|
+
And step "unwind" changes nothing
|
|
29
|
+
And step "start-gate.check" changes nothing
|
|
30
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
31
|
+
"""
|
|
32
|
+
# Requirements
|
|
33
|
+
|
|
34
|
+
## Acceptance Criteria
|
|
35
|
+
|
|
36
|
+
- AC-1: `subtract(5, 3)` returns 2.
|
|
37
|
+
- AC-2: `multiply(2, 3)` returns 6.
|
|
38
|
+
"""
|
|
39
|
+
And step "design.gate.answer" changes nothing
|
|
40
|
+
And step "seam.check" changes nothing
|
|
41
|
+
|
|
42
|
+
Scenario: a rebuild stopped at its context limit lands its snapshot and is continued on the branch
|
|
43
|
+
Given step "seam.naive" writes "src/ops.ts" with:
|
|
44
|
+
"""
|
|
45
|
+
export const subtract = (a: number, b: number) => a - b
|
|
46
|
+
export const multiply = (a: number, b: number) => a * b
|
|
47
|
+
"""
|
|
48
|
+
And step "seam.naive" writes "tests/calc.test.ts" with:
|
|
49
|
+
"""
|
|
50
|
+
it("adds", () => {})
|
|
51
|
+
it("[AC-1] subtracts", () => {})
|
|
52
|
+
it("[AC-2] multiplies", () => {})
|
|
53
|
+
"""
|
|
54
|
+
And judge step "seam.judge" answers:
|
|
55
|
+
"""
|
|
56
|
+
[{"id":"shape","answer":"refactor-first","p":0.9}]
|
|
57
|
+
"""
|
|
58
|
+
And step "implement.building" writes "tests/calc.test.ts" with:
|
|
59
|
+
"""
|
|
60
|
+
it("adds", () => {})
|
|
61
|
+
it("[AC-1] subtracts", () => {})
|
|
62
|
+
"""
|
|
63
|
+
And step "implement.building" writes ".gtd/HANDOFF.md" with:
|
|
64
|
+
"""
|
|
65
|
+
Rebuilt subtract behind the flag; multiply was next.
|
|
66
|
+
"""
|
|
67
|
+
When the process is driven
|
|
68
|
+
Then the process rests at "implement.continue.building"
|
|
69
|
+
And the resting step's content contains "Still open: AC-2."
|
|
70
|
+
And the resting step's content contains "Its work is already in the tree."
|
|
71
|
+
And the resting step's content contains "Delete `.gtd/HANDOFF.md` once you are done"
|
|
72
|
+
And "tests/calc.test.ts" contains "[AC-1] subtracts"
|
|
73
|
+
|
|
74
|
+
Given step "implement.continue.building" writes "tests/calc.test.ts" with:
|
|
75
|
+
"""
|
|
76
|
+
it("adds", () => {})
|
|
77
|
+
it("[AC-1] subtracts", () => {})
|
|
78
|
+
it("[AC-2] multiplies", () => {})
|
|
79
|
+
"""
|
|
80
|
+
And step "implement.continue.building" deletes ".gtd/HANDOFF.md"
|
|
81
|
+
When the process is driven
|
|
82
|
+
Then the process rests at "implement.health.check"
|
|
83
|
+
And the commit subjects end with:
|
|
84
|
+
"""
|
|
85
|
+
gtd(agent): implement.building → implement.continue.building
|
|
86
|
+
gtd(agent): implement.continue.building → implement.health.check
|
|
87
|
+
"""
|
|
88
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a continuation stays inside its coarse slice
|
|
3
|
+
|
|
4
|
+
A naive pass of one coarse slice that stops at its context limit is
|
|
5
|
+
continued with that slice's criteria still open, never another slice's.
|
|
6
|
+
|
|
7
|
+
Background:
|
|
8
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
9
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
10
|
+
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
11
|
+
And an environment variable "GTD_SLICEMAXCRITERIA" set to "2"
|
|
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 a file "TODO.md" with:
|
|
18
|
+
"""
|
|
19
|
+
# More arithmetic
|
|
20
|
+
|
|
21
|
+
subtract, multiply and divide.
|
|
22
|
+
"""
|
|
23
|
+
And step "unwind" changes nothing
|
|
24
|
+
And step "start-gate.check" changes nothing
|
|
25
|
+
And step "design.gate.answer" changes nothing
|
|
26
|
+
And step "seam.check" changes nothing
|
|
27
|
+
|
|
28
|
+
Scenario: the first slice's continuation is told only that slice's open criterion
|
|
29
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
30
|
+
"""
|
|
31
|
+
# Requirements
|
|
32
|
+
|
|
33
|
+
## Acceptance Criteria
|
|
34
|
+
|
|
35
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
36
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
37
|
+
- AC-3: divide(a, b) returns a / b.
|
|
38
|
+
"""
|
|
39
|
+
And step "design.slicing" writes ".gtd/SLICES.md" with:
|
|
40
|
+
"""
|
|
41
|
+
# Slices
|
|
42
|
+
|
|
43
|
+
## Subtract and divide
|
|
44
|
+
|
|
45
|
+
- AC-1
|
|
46
|
+
- AC-3
|
|
47
|
+
|
|
48
|
+
## Multiply
|
|
49
|
+
|
|
50
|
+
- AC-2
|
|
51
|
+
"""
|
|
52
|
+
And step "seam.naive" writes "src/sub.test.ts" with:
|
|
53
|
+
"""
|
|
54
|
+
it("[AC-1] subtracts", () => {})
|
|
55
|
+
"""
|
|
56
|
+
And step "seam.naive" writes ".gtd/HANDOFF.md" with:
|
|
57
|
+
"""
|
|
58
|
+
Subtract is tested; divide was next.
|
|
59
|
+
"""
|
|
60
|
+
When the process is driven
|
|
61
|
+
Then the process rests at "seam.continue.naive"
|
|
62
|
+
And the resting step's content contains "This slice builds AC-1, AC-3 only"
|
|
63
|
+
And the resting step's content contains "Still open: AC-3."
|
|
64
|
+
And the resting step's content does not contain "AC-2"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a turn stopped at its context limit is snapshotted and continued
|
|
3
|
+
|
|
4
|
+
A naive pass, refactor or build turn told to stop at its context limit
|
|
5
|
+
writes its handoff note to `.gtd/HANDOFF.md`. That turn is the snapshot —
|
|
6
|
+
the naive pass's kept tip, or the refactor's or build's own commit — and a
|
|
7
|
+
continuation in a fresh conversation (`<phase>.continue.<step>`) picks up
|
|
8
|
+
from it with the note and what is still open. After `continuations` (3) of
|
|
9
|
+
them, or `continuationStall` (2) in a row that close nothing still open, the
|
|
10
|
+
phase rests at `<phase>.oversized`.
|
|
11
|
+
|
|
12
|
+
Background:
|
|
13
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
14
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
15
|
+
And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
|
|
16
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
17
|
+
And a file "src/calc.ts" with:
|
|
18
|
+
"""
|
|
19
|
+
export const add = (a: number, b: number) => a + b
|
|
20
|
+
"""
|
|
21
|
+
And the working tree is committed as "feat: add calculator"
|
|
22
|
+
And a file "TODO.md" with:
|
|
23
|
+
"""
|
|
24
|
+
# Add subtract and multiply
|
|
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
|
+
## Acceptance Criteria
|
|
33
|
+
|
|
34
|
+
- AC-1: `subtract(5, 3)` returns 2.
|
|
35
|
+
- AC-2: `multiply(2, 3)` returns 6.
|
|
36
|
+
"""
|
|
37
|
+
And step "design.gate.answer" changes nothing
|
|
38
|
+
And step "seam.check" changes nothing
|
|
39
|
+
|
|
40
|
+
Scenario: a naive pass stopped at its context limit is continued from its kept tip with the open IDs
|
|
41
|
+
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
42
|
+
"""
|
|
43
|
+
it("[AC-1] subtracts", () => {})
|
|
44
|
+
"""
|
|
45
|
+
And step "seam.naive" writes ".gtd/HANDOFF.md" with:
|
|
46
|
+
"""
|
|
47
|
+
Wrote subtract and its test; multiply was next.
|
|
48
|
+
"""
|
|
49
|
+
When the process is driven
|
|
50
|
+
Then the process rests at "seam.continue.naive"
|
|
51
|
+
And the resting step's content contains "Your previous turn stopped at its context limit"
|
|
52
|
+
And the resting step's content contains "Still open: AC-2."
|
|
53
|
+
And the resting step's content contains "Wrote subtract and its test; multiply was next."
|
|
54
|
+
And the resting step's content contains "':(top)' ':(top,exclude).gtd/HANDOFF.md' | git apply"
|
|
55
|
+
And ".gtd/HANDOFF.md" does not exist
|
|
56
|
+
And the output of gtd "next --json" contains "\"resume\":false"
|
|
57
|
+
And the output of gtd "next --json" contains "\"memory\":\"seam.continue#"
|
|
58
|
+
|
|
59
|
+
Given step "seam.continue.naive" writes "tests/calc.test.ts" with:
|
|
60
|
+
"""
|
|
61
|
+
it("[AC-1] subtracts", () => {})
|
|
62
|
+
it("[AC-2] multiplies", () => {})
|
|
63
|
+
"""
|
|
64
|
+
When the process is driven
|
|
65
|
+
Then the process rests at "implement.applying"
|
|
66
|
+
And the commit subjects end with:
|
|
67
|
+
"""
|
|
68
|
+
gtd(human): design.gate.answer → seam.naive
|
|
69
|
+
gtd(agent): seam.naive → seam.continue.naive
|
|
70
|
+
gtd(agent): seam.continue.naive → seam.check
|
|
71
|
+
gtd(check): seam.check → implement.applying
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
Scenario: two continuations in a row that close nothing rest for a person
|
|
75
|
+
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
76
|
+
"""
|
|
77
|
+
it("[AC-1] subtracts", () => {})
|
|
78
|
+
"""
|
|
79
|
+
And step "seam.naive" writes ".gtd/HANDOFF.md" with:
|
|
80
|
+
"""
|
|
81
|
+
Subtract is tested; multiply was next.
|
|
82
|
+
"""
|
|
83
|
+
And step "seam.continue.naive" writes ".gtd/HANDOFF.md" with:
|
|
84
|
+
"""
|
|
85
|
+
Still on multiply.
|
|
86
|
+
"""
|
|
87
|
+
When the process is driven
|
|
88
|
+
Then the process rests at "seam.oversized"
|
|
89
|
+
And the resting step's content contains "the last 2 continuations closed nothing still open"
|
|
90
|
+
And the commit subjects end with:
|
|
91
|
+
"""
|
|
92
|
+
gtd(human): design.gate.answer → seam.naive
|
|
93
|
+
gtd(agent): seam.naive → seam.continue.naive
|
|
94
|
+
gtd(agent): seam.continue.naive
|
|
95
|
+
gtd(agent): seam.continue.naive → seam.oversized
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
Scenario: a refactor stopped at its context limit is run green before its continuation
|
|
99
|
+
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
100
|
+
"""
|
|
101
|
+
it("[AC-1] subtracts", () => {})
|
|
102
|
+
it("[AC-2] multiplies", () => {})
|
|
103
|
+
"""
|
|
104
|
+
And 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
|
+
export const multiply = (a: number, b: number) => a * b
|
|
109
|
+
"""
|
|
110
|
+
And judge step "seam.judge" answers:
|
|
111
|
+
"""
|
|
112
|
+
[{"id":"shape","answer":"refactor-first","p":0.9}]
|
|
113
|
+
"""
|
|
114
|
+
And step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
|
|
115
|
+
"""
|
|
116
|
+
Seam: a shared `op` in src/calc.ts.
|
|
117
|
+
"""
|
|
118
|
+
And step "refactor.deriving" runs its script
|
|
119
|
+
And step "refactor.building" writes ".gtd/HANDOFF.md" with:
|
|
120
|
+
"""
|
|
121
|
+
Started on op; nothing changed yet.
|
|
122
|
+
"""
|
|
123
|
+
And step "refactor.health.check" changes nothing
|
|
124
|
+
When the process is driven
|
|
125
|
+
Then the process rests at "refactor.continue.building"
|
|
126
|
+
And the resting step's content contains "Still open: src/calc.ts."
|
|
127
|
+
And the resting step's content contains "Started on op; nothing changed yet."
|
|
128
|
+
And the commit subjects end with:
|
|
129
|
+
"""
|
|
130
|
+
gtd(agent): refactor.building → refactor.health.check
|
|
131
|
+
gtd(check): refactor.health.check → refactor.continue.building
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
Scenario: a refactor continuation that leaves the note in place stopped again
|
|
135
|
+
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
136
|
+
"""
|
|
137
|
+
it("[AC-1] subtracts", () => {})
|
|
138
|
+
it("[AC-2] multiplies", () => {})
|
|
139
|
+
"""
|
|
140
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
141
|
+
"""
|
|
142
|
+
export const add = (a: number, b: number) => a + b
|
|
143
|
+
export const subtract = (a: number, b: number) => a - b
|
|
144
|
+
"""
|
|
145
|
+
And judge step "seam.judge" answers:
|
|
146
|
+
"""
|
|
147
|
+
[{"id":"shape","answer":"refactor-first","p":0.9}]
|
|
148
|
+
"""
|
|
149
|
+
And step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
|
|
150
|
+
"""
|
|
151
|
+
Seam: a shared `op` in src/calc.ts.
|
|
152
|
+
"""
|
|
153
|
+
And step "refactor.deriving" runs its script
|
|
154
|
+
And step "refactor.building" writes "src/calc.ts" with:
|
|
155
|
+
"""
|
|
156
|
+
export const op = (a: number, b: number, sign: number) => a + sign * b
|
|
157
|
+
export const add = (a: number, b: number) => a + b
|
|
158
|
+
"""
|
|
159
|
+
And step "refactor.building" writes ".gtd/HANDOFF.md" with:
|
|
160
|
+
"""
|
|
161
|
+
op is in; routing add through it was next.
|
|
162
|
+
"""
|
|
163
|
+
And step "refactor.health.check" changes nothing
|
|
164
|
+
And step "refactor.continue.building" writes "src/calc.ts" with:
|
|
165
|
+
"""
|
|
166
|
+
export const op = (a: number, b: number, sign: number) => a + sign * b
|
|
167
|
+
export const add = (a: number, b: number) => op(a, b, 1)
|
|
168
|
+
"""
|
|
169
|
+
When the process is driven
|
|
170
|
+
Then the process rests at "refactor.continue.building"
|
|
171
|
+
And the commit subjects end with:
|
|
172
|
+
"""
|
|
173
|
+
gtd(agent): refactor.building → refactor.health.check
|
|
174
|
+
gtd(check): refactor.health.check → refactor.continue.building
|
|
175
|
+
gtd(agent): refactor.continue.building → refactor.health.check
|
|
176
|
+
gtd(check): refactor.health.check → refactor.continue.building
|
|
177
|
+
gtd(agent): refactor.continue.building
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
Scenario: once a naive pass has continued, its retry runs in a fresh conversation, not as a continuation
|
|
181
|
+
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
182
|
+
"""
|
|
183
|
+
it("[AC-1] subtracts", () => {})
|
|
184
|
+
"""
|
|
185
|
+
And step "seam.naive" writes ".gtd/HANDOFF.md" with:
|
|
186
|
+
"""
|
|
187
|
+
Multiply was next.
|
|
188
|
+
"""
|
|
189
|
+
And step "seam.continue.naive" writes "tests/calc.test.ts" with:
|
|
190
|
+
"""
|
|
191
|
+
it("[AC-1] subtracts", () => {})
|
|
192
|
+
it("[AC-2] multiplies", () => {})
|
|
193
|
+
"""
|
|
194
|
+
And step "seam.continue.naive" writes "src/calc.ts" with:
|
|
195
|
+
"""
|
|
196
|
+
export const add = (a: number, b: number) => a - b
|
|
197
|
+
"""
|
|
198
|
+
And judge step "seam.judge" answers:
|
|
199
|
+
"""
|
|
200
|
+
[{"id":"shape","answer":"reject","p":0.9}]
|
|
201
|
+
"""
|
|
202
|
+
When the process is driven
|
|
203
|
+
Then the process rests at "seam.fresh.naive"
|
|
204
|
+
And the resting step's content contains "rejected"
|
|
205
|
+
And the output of gtd "next --json" contains "\"resume\":false"
|
|
@@ -11,7 +11,13 @@ import {
|
|
|
11
11
|
import { healthy } from "../shared/health.js"
|
|
12
12
|
import { coder, FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
|
|
13
13
|
import * as t from "../shared/text.js"
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
criteriaOf,
|
|
16
|
+
criterionIds,
|
|
17
|
+
requireCriteriaFrozen,
|
|
18
|
+
sliceIds,
|
|
19
|
+
type CriteriaSlice,
|
|
20
|
+
} from "./criteria.js"
|
|
15
21
|
import { isTestFile } from "./refactor.js"
|
|
16
22
|
import { isCode } from "./review.js"
|
|
17
23
|
|
|
@@ -51,13 +57,15 @@ const testsSince = (base: string): string =>
|
|
|
51
57
|
|
|
52
58
|
/**
|
|
53
59
|
* The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
|
|
54
|
-
* over — no passing test since `base` covers, by the judge.
|
|
60
|
+
* over, only `slice`'s when given — no passing test since `base` covers, by the judge.
|
|
55
61
|
*/
|
|
56
62
|
const uncoveredSince = async (
|
|
57
63
|
base: string,
|
|
64
|
+
slice: CriteriaSlice | undefined,
|
|
58
65
|
): Promise<{ readonly covered: string[]; readonly uncovered: string[] }> => {
|
|
59
66
|
const requirements = read(REQUIREMENTS) ?? ""
|
|
60
|
-
const
|
|
67
|
+
const own = criterionIds(requirements, slice)
|
|
68
|
+
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id))
|
|
61
69
|
if (criteria.length === 0) return { covered: [], uncovered: [] }
|
|
62
70
|
const { answers } = await judge("coverage", {
|
|
63
71
|
questions: criteria.map(t.coverageQuestion),
|
|
@@ -81,14 +89,14 @@ const record = (covered: readonly string[], uncovered: readonly string[] = []):
|
|
|
81
89
|
* sending uncovered IDs back to a covering turn; marks the IDs covered on the
|
|
82
90
|
* landing that ends it. A human may overrule once the retries are spent.
|
|
83
91
|
*/
|
|
84
|
-
export const requireCoverage = async (base = start()): Promise<void> => {
|
|
92
|
+
export const requireCoverage = async (base = start(), slice?: CriteriaSlice): Promise<void> => {
|
|
85
93
|
let retries = 0
|
|
86
94
|
for (;;) {
|
|
87
|
-
const { covered, uncovered } = await uncoveredSince(base)
|
|
95
|
+
const { covered, uncovered } = await uncoveredSince(base, slice)
|
|
88
96
|
if (uncovered.length === 0) return record(covered)
|
|
89
97
|
if (retries < COVERING_RETRIES) {
|
|
90
98
|
retries++
|
|
91
|
-
await t.agentWithSkills("covering", t.coveringPrompt(uncovered), {
|
|
99
|
+
await t.agentWithSkills("covering", t.coveringPrompt(uncovered, sliceIds(slice)), {
|
|
92
100
|
label: "Covering the acceptance criteria",
|
|
93
101
|
model: coder(),
|
|
94
102
|
system: t.builderSystem(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import type { Change } from "../../flows/index.js"
|
|
3
|
-
import { criteriaOf, statedCriteria } from "./criteria.js"
|
|
3
|
+
import { criteriaOf, criterionIds, statedCriteria } from "./criteria.js"
|
|
4
4
|
|
|
5
5
|
const added = (path: string, after: string): Change => ({
|
|
6
6
|
path,
|
|
@@ -104,3 +104,19 @@ describe("statedCriteria", () => {
|
|
|
104
104
|
).toBeUndefined()
|
|
105
105
|
})
|
|
106
106
|
})
|
|
107
|
+
|
|
108
|
+
describe("criterionIds", () => {
|
|
109
|
+
const requirements = "## Acceptance Criteria\n\n- AC-1: a\n- AC-2: b\n- AC-4: added\n- unnamed\n"
|
|
110
|
+
|
|
111
|
+
it("lists every ID'd criterion of an unsliced change", () => {
|
|
112
|
+
expect(criterionIds(requirements)).toEqual(["AC-1", "AC-2", "AC-4"])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("keeps `only` to the IDs the file still lists", () => {
|
|
116
|
+
expect(criterionIds(requirements, { only: ["AC-1", "AC-3"] })).toEqual(["AC-1"])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it("gives `except` every other ID, one added since included", () => {
|
|
120
|
+
expect(criterionIds(requirements, { except: ["AC-1", "AC-3"] })).toEqual(["AC-2", "AC-4"])
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
scope,
|
|
9
9
|
type Change,
|
|
10
10
|
} from "../../flows/index.js"
|
|
11
|
-
import { markFenced } from "../shared/fences.js"
|
|
11
|
+
import { HEADING, ITEM, markFenced } from "../shared/fences.js"
|
|
12
12
|
import { REQUIREMENTS } from "../shared/steps.js"
|
|
13
13
|
import * as t from "../shared/text.js"
|
|
14
14
|
|
|
@@ -29,8 +29,6 @@ export interface StatedCriteria {
|
|
|
29
29
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
30
30
|
const CLEAR_MIN_P = 0.8
|
|
31
31
|
|
|
32
|
-
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/
|
|
33
|
-
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/
|
|
34
32
|
const ID = /^(AC-\d+):\s*(.*)$/
|
|
35
33
|
|
|
36
34
|
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
@@ -71,6 +69,31 @@ export const criteriaOf = (text: string): readonly Criterion[] | undefined =>
|
|
|
71
69
|
return id === null ? { id: "", text: item } : { id: id[1]!, text: id[2]! }
|
|
72
70
|
})
|
|
73
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Which criteria one slice builds, resolved against the file whenever read:
|
|
74
|
+
* `only` drops an ID a steer deleted, `except` (the last slice) takes one a
|
|
75
|
+
* steer added.
|
|
76
|
+
*/
|
|
77
|
+
export interface CriteriaSlice {
|
|
78
|
+
readonly only?: readonly string[]
|
|
79
|
+
readonly except?: readonly string[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The ID'd criteria of `text`, only `slice`'s when given. */
|
|
83
|
+
export const criterionIds = (text: string, slice?: CriteriaSlice): string[] =>
|
|
84
|
+
(criteriaOf(text) ?? [])
|
|
85
|
+
.map((c) => c.id)
|
|
86
|
+
.filter(
|
|
87
|
+
(id) =>
|
|
88
|
+
id !== "" &&
|
|
89
|
+
(slice?.only === undefined || slice.only.includes(id)) &&
|
|
90
|
+
!(slice?.except ?? []).includes(id),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
/** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
|
|
94
|
+
export const sliceIds = (slice: CriteriaSlice | undefined): string[] | undefined =>
|
|
95
|
+
slice === undefined ? undefined : criterionIds(read(REQUIREMENTS) ?? "", slice)
|
|
96
|
+
|
|
74
97
|
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
75
98
|
const numbered = (items: readonly string[]): Criterion[] => {
|
|
76
99
|
const ids = items.map((item) => ID.exec(item))
|
|
@@ -166,3 +189,14 @@ export const skipDesign = (stated: StatedCriteria | undefined): Promise<boolean>
|
|
|
166
189
|
})
|
|
167
190
|
return true
|
|
168
191
|
})
|
|
192
|
+
|
|
193
|
+
/** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
|
|
194
|
+
export const requireCriteriaIfEdited = (): void => {
|
|
195
|
+
const change = changes().get(REQUIREMENTS)
|
|
196
|
+
if (change === undefined) return
|
|
197
|
+
if (change.status === "deleted")
|
|
198
|
+
refuse(
|
|
199
|
+
`gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`,
|
|
200
|
+
)
|
|
201
|
+
requireCriteria(REQUIREMENTS)
|
|
202
|
+
}
|