@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.
Files changed (41) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -0
  3. package/dist/chunks/{reserved-zAfoJtR7.mjs → reserved-CfQ8eurM.mjs} +85 -17
  4. package/dist/chunks/{tail-BRC4DGdV.mjs → tail-C7yaWvaS.mjs} +1 -1
  5. package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
  6. package/dist/gtd.bundle.mjs +1 -1
  7. package/dist/testkit.mjs +1 -1
  8. package/dist/wire.mjs +1 -1
  9. package/dist/workflows/cleanup.mjs +2 -2
  10. package/dist/workflows/feature.mjs +318 -76
  11. package/dist/workflows/fix.mjs +2 -2
  12. package/dist/workflows/review.mjs +2 -2
  13. package/dist/workflows/rollout.mjs +1 -1
  14. package/dist/workflows.mjs +419 -109
  15. package/package.json +1 -1
  16. package/src/testkit/steps.ts +39 -0
  17. package/src/wire/ContextLimit.ts +1 -1
  18. package/src/workflows/feature/continuation-cap.feature +65 -0
  19. package/src/workflows/feature/continuation-rebuild.feature +88 -0
  20. package/src/workflows/feature/continuation-slices.feature +64 -0
  21. package/src/workflows/feature/continuation.feature +205 -0
  22. package/src/workflows/feature/coverage.ts +14 -6
  23. package/src/workflows/feature/criteria.test.ts +17 -1
  24. package/src/workflows/feature/criteria.ts +37 -3
  25. package/src/workflows/feature/feature.ts +64 -15
  26. package/src/workflows/feature/index.ts +1 -0
  27. package/src/workflows/feature/refactor.ts +64 -30
  28. package/src/workflows/feature/seam.ts +157 -48
  29. package/src/workflows/feature/slicing-cap.feature +14 -0
  30. package/src/workflows/feature/slicing.feature +225 -0
  31. package/src/workflows/feature/slicing.test.ts +49 -0
  32. package/src/workflows/feature/slicing.ts +105 -0
  33. package/src/workflows/index.ts +1 -0
  34. package/src/workflows/shared/continuation.ts +92 -0
  35. package/src/workflows/shared/fences.ts +5 -0
  36. package/src/workflows/shared/options.ts +3 -0
  37. package/src/workflows/shared/steps.ts +2 -0
  38. package/src/workflows/shared/text.test.ts +6 -2
  39. package/src/workflows/shared/text.ts +153 -22
  40. package/src/workflows/shared/vars.ts +6 -0
  41. package/src/workflows/shared/workflows.steps.ts +13 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "37.0.0",
3
+ "version": "38.1.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -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
  })
@@ -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 { criteriaOf, requireCriteriaFrozen } from "./criteria.js"
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 criteria = (criteriaOf(requirements) ?? []).filter((c) => c.id !== "")
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
+ }