@pmelab/gtd 36.0.0 → 38.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 +30 -9
- package/claude/hooks/context.ts +66 -0
- package/claude/hooks/drive.ts +3 -0
- package/claude/hooks/register.tsx +69 -6
- package/dist/chunks/{reserved-BAv71I4i.mjs → reserved-sfMdy7v7.mjs} +83 -22
- package/dist/chunks/{tail-kar1zhOb.mjs → tail-Cgca5Gp-.mjs} +1 -1
- package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
- package/dist/gtd.bundle.mjs +498 -56
- package/dist/testkit.mjs +115 -3
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +277 -58
- 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 +355 -75
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +10 -3
- package/src/cli/Cli.ts +76 -1
- package/src/cli/Program.test.ts +1 -0
- package/src/cli/Program.ts +28 -0
- package/src/cli/ToolCommands.ts +3 -0
- package/src/edge/Edge.ts +11 -0
- package/src/flows/runtime.ts +11 -0
- package/src/replay/Replay.ts +51 -2
- package/src/testkit/Layers.ts +1 -0
- package/src/testkit/steps.ts +35 -0
- package/src/wire/BeatDocument.ts +12 -2
- package/src/wire/BeatStatus.ts +3 -0
- package/src/wire/ContextLimit.fixture.ts +30 -0
- package/src/wire/ContextLimit.test.ts +96 -0
- package/src/wire/ContextLimit.ts +169 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/src/workflow/Workflow.ts +9 -1
- package/src/workflows/feature/context-limit.feature +37 -0
- package/src/workflows/feature/coverage.ts +22 -8
- package/src/workflows/feature/criteria.test.ts +17 -1
- package/src/workflows/feature/criteria.ts +26 -3
- package/src/workflows/feature/feature.ts +69 -16
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor-absorbs.feature +12 -5
- package/src/workflows/feature/refactor-first.feature +11 -2
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +269 -0
- package/src/workflows/feature/refactor-review.feature +45 -1
- package/src/workflows/feature/refactor.ts +9 -3
- package/src/workflows/feature/seam.test.ts +108 -3
- package/src/workflows/feature/seam.ts +169 -31
- 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/fences.ts +5 -0
- package/src/workflows/shared/options.test.ts +11 -2
- package/src/workflows/shared/options.ts +19 -4
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/text.ts +129 -28
- package/src/workflows/shared/vars.ts +1 -0
- package/src/workflows/shared/workflows.steps.ts +13 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a change with more criteria than the cap is built in coarse slices
|
|
3
|
+
|
|
4
|
+
After design, criteria within sliceMaxCriteria are one slice, built as
|
|
5
|
+
before. Above it design.slicing groups them in .gtd/SLICES.md, at most the
|
|
6
|
+
cap per slice, and each slice runs in order — naive pass, implement, quality
|
|
7
|
+
lap — from where the previous one landed, as slices feature-1, feature-2, …
|
|
8
|
+
|
|
9
|
+
Background:
|
|
10
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
11
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
12
|
+
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
13
|
+
And an environment variable "GTD_SLICEMAXCRITERIA" set to "2"
|
|
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
|
+
# More arithmetic
|
|
22
|
+
|
|
23
|
+
subtract, multiply and divide.
|
|
24
|
+
"""
|
|
25
|
+
And step "unwind" changes nothing
|
|
26
|
+
And step "start-gate.check" changes nothing
|
|
27
|
+
And step "design.gate.answer" changes nothing
|
|
28
|
+
And step "seam.check" changes nothing
|
|
29
|
+
And step "implement.applying" runs its script
|
|
30
|
+
And step "implement.health.check" changes nothing
|
|
31
|
+
And step "implement.closing" runs its script
|
|
32
|
+
|
|
33
|
+
Scenario: criteria within the cap are one unnumbered slice with no slicing turn
|
|
34
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
35
|
+
"""
|
|
36
|
+
# Requirements
|
|
37
|
+
|
|
38
|
+
## Acceptance Criteria
|
|
39
|
+
|
|
40
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
41
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
42
|
+
"""
|
|
43
|
+
And step "seam.naive" writes "src/calc.test.ts" with:
|
|
44
|
+
"""
|
|
45
|
+
it("[AC-1] subtracts", () => {})
|
|
46
|
+
it("[AC-2] multiplies", () => {})
|
|
47
|
+
"""
|
|
48
|
+
And step "seam.naive" is prompted with "Read `.gtd/REQUIREMENTS.md`"
|
|
49
|
+
And judge step "implement.coverage" answers:
|
|
50
|
+
"""
|
|
51
|
+
[{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
|
|
52
|
+
"""
|
|
53
|
+
When the process is driven
|
|
54
|
+
Then the process rests at "idle"
|
|
55
|
+
And the commit subjects end with:
|
|
56
|
+
"""
|
|
57
|
+
gtd(human): design.gate.answer → seam.naive
|
|
58
|
+
gtd(agent): seam.naive → seam.check
|
|
59
|
+
gtd(check): seam.check → implement.applying
|
|
60
|
+
gtd(check): implement.applying → implement.health.check
|
|
61
|
+
gtd(check): implement.health.check → implement.coverage
|
|
62
|
+
gtd(judge): implement.coverage → implement.closing
|
|
63
|
+
gtd(check): implement.closing → idle
|
|
64
|
+
"""
|
|
65
|
+
And the output lines of gtd "slices" start with:
|
|
66
|
+
"""
|
|
67
|
+
feature
|
|
68
|
+
"""
|
|
69
|
+
And the output of gtd "slices" does not contain "feature-"
|
|
70
|
+
And no commit body contains "This slice builds"
|
|
71
|
+
|
|
72
|
+
Scenario: criteria above the cap are sliced and built slice by slice
|
|
73
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
74
|
+
"""
|
|
75
|
+
# Requirements
|
|
76
|
+
|
|
77
|
+
## Acceptance Criteria
|
|
78
|
+
|
|
79
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
80
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
81
|
+
- AC-3: divide(a, b) returns a / b.
|
|
82
|
+
"""
|
|
83
|
+
And step "design.slicing" is prompted with "at most 2 per slice"
|
|
84
|
+
And step "design.slicing" writes ".gtd/SLICES.md" with:
|
|
85
|
+
"""
|
|
86
|
+
# Slices
|
|
87
|
+
|
|
88
|
+
## Subtract and divide
|
|
89
|
+
|
|
90
|
+
- AC-1
|
|
91
|
+
- AC-3
|
|
92
|
+
|
|
93
|
+
## Multiply
|
|
94
|
+
|
|
95
|
+
- AC-2
|
|
96
|
+
"""
|
|
97
|
+
And step "seam.naive", when prompted with "This slice builds AC-1, AC-3 only", writes "src/sub.test.ts" with:
|
|
98
|
+
"""
|
|
99
|
+
it("[AC-1] subtracts", () => {})
|
|
100
|
+
it("[AC-3] divides", () => {})
|
|
101
|
+
"""
|
|
102
|
+
And step "seam.naive", when prompted with "This slice builds AC-2 only", writes "src/mul.test.ts" with:
|
|
103
|
+
"""
|
|
104
|
+
it("[AC-2] multiplies", () => {})
|
|
105
|
+
"""
|
|
106
|
+
And judge step "implement.coverage", when its questions contain "AC-3", answers:
|
|
107
|
+
"""
|
|
108
|
+
[{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-3","answer":"yes","p":0.9}]
|
|
109
|
+
"""
|
|
110
|
+
And judge step "implement.coverage", when its questions contain "AC-2", answers:
|
|
111
|
+
"""
|
|
112
|
+
[{"id":"AC-2","answer":"yes","p":0.9}]
|
|
113
|
+
"""
|
|
114
|
+
When the process is driven
|
|
115
|
+
Then the process rests at "idle"
|
|
116
|
+
And the commit subjects end with:
|
|
117
|
+
"""
|
|
118
|
+
gtd(agent): design.triage → design.gate.answer
|
|
119
|
+
gtd(human): design.gate.answer → design.slicing
|
|
120
|
+
gtd(agent): design.slicing → seam.naive
|
|
121
|
+
gtd(agent): seam.naive → seam.check
|
|
122
|
+
gtd(check): seam.check → implement.applying
|
|
123
|
+
gtd(check): implement.applying → implement.health.check
|
|
124
|
+
gtd(check): implement.health.check → implement.coverage
|
|
125
|
+
gtd(judge): implement.coverage → implement.closing
|
|
126
|
+
gtd(check): implement.closing → seam.naive
|
|
127
|
+
gtd(agent): seam.naive → seam.check
|
|
128
|
+
gtd(check): seam.check → implement.applying
|
|
129
|
+
gtd(check): implement.applying → implement.health.check
|
|
130
|
+
gtd(check): implement.health.check → implement.coverage
|
|
131
|
+
gtd(judge): implement.coverage → implement.closing
|
|
132
|
+
gtd(check): implement.closing → idle
|
|
133
|
+
"""
|
|
134
|
+
And the output lines of gtd "slices" start with:
|
|
135
|
+
"""
|
|
136
|
+
feature-1
|
|
137
|
+
feature-2
|
|
138
|
+
"""
|
|
139
|
+
And slice "feature-2" begins where slice "feature-1" ends
|
|
140
|
+
And "src/sub.test.ts" contains "[AC-3] divides"
|
|
141
|
+
And "src/mul.test.ts" contains "[AC-2] multiplies"
|
|
142
|
+
And ".gtd/REQUIREMENTS.md" does not exist
|
|
143
|
+
And ".gtd/SLICES.md" does not exist
|
|
144
|
+
|
|
145
|
+
Scenario: a slice over the cap is refused
|
|
146
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
147
|
+
"""
|
|
148
|
+
# Requirements
|
|
149
|
+
|
|
150
|
+
## Acceptance Criteria
|
|
151
|
+
|
|
152
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
153
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
154
|
+
- AC-3: divide(a, b) returns a / b.
|
|
155
|
+
"""
|
|
156
|
+
And step "design.slicing" writes ".gtd/SLICES.md" with:
|
|
157
|
+
"""
|
|
158
|
+
# Slices
|
|
159
|
+
|
|
160
|
+
## Everything
|
|
161
|
+
|
|
162
|
+
- AC-1
|
|
163
|
+
- AC-2
|
|
164
|
+
- AC-3
|
|
165
|
+
"""
|
|
166
|
+
Then driving the process is refused with "more than 2 criteria in: Everything"
|
|
167
|
+
|
|
168
|
+
Scenario: a criterion in no slice is refused
|
|
169
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
170
|
+
"""
|
|
171
|
+
# Requirements
|
|
172
|
+
|
|
173
|
+
## Acceptance Criteria
|
|
174
|
+
|
|
175
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
176
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
177
|
+
- AC-3: divide(a, b) returns a / b.
|
|
178
|
+
"""
|
|
179
|
+
And step "design.slicing" writes ".gtd/SLICES.md" with:
|
|
180
|
+
"""
|
|
181
|
+
# Slices
|
|
182
|
+
|
|
183
|
+
## Subtract
|
|
184
|
+
|
|
185
|
+
- AC-1
|
|
186
|
+
|
|
187
|
+
## Multiply
|
|
188
|
+
|
|
189
|
+
- AC-2
|
|
190
|
+
"""
|
|
191
|
+
Then driving the process is refused with "no slice for: AC-3"
|
|
192
|
+
|
|
193
|
+
Scenario: a slicing turn may not change the acceptance criteria
|
|
194
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
195
|
+
"""
|
|
196
|
+
# Requirements
|
|
197
|
+
|
|
198
|
+
## Acceptance Criteria
|
|
199
|
+
|
|
200
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
201
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
202
|
+
- AC-3: divide(a, b) returns a / b.
|
|
203
|
+
"""
|
|
204
|
+
And step "design.slicing" writes ".gtd/REQUIREMENTS.md" with:
|
|
205
|
+
"""
|
|
206
|
+
# Requirements
|
|
207
|
+
|
|
208
|
+
## Acceptance Criteria
|
|
209
|
+
|
|
210
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
211
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
212
|
+
"""
|
|
213
|
+
And step "design.slicing" writes ".gtd/SLICES.md" with:
|
|
214
|
+
"""
|
|
215
|
+
# Slices
|
|
216
|
+
|
|
217
|
+
## Subtract
|
|
218
|
+
|
|
219
|
+
- AC-1
|
|
220
|
+
|
|
221
|
+
## Multiply
|
|
222
|
+
|
|
223
|
+
- AC-2
|
|
224
|
+
"""
|
|
225
|
+
Then driving the process is refused with "wrote outside its write access"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { sliceCriteria, slicesOf } from "./slicing.js"
|
|
3
|
+
|
|
4
|
+
describe("slicesOf", () => {
|
|
5
|
+
it("reads each level-2 heading's ID'd items, in order", () => {
|
|
6
|
+
expect(
|
|
7
|
+
slicesOf("# Slices\n\n## Parse\n\n- AC-1: parses\n- AC-3\n\n## Print\n\n- AC-2 prints\n"),
|
|
8
|
+
).toEqual([
|
|
9
|
+
{ title: "Parse", ids: ["AC-1", "AC-3"] },
|
|
10
|
+
{ title: "Print", ids: ["AC-2"] },
|
|
11
|
+
])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("keeps an item not led by an ID as an empty one, and skips fenced lines", () => {
|
|
15
|
+
expect(slicesOf("## One\n\n```md\n## Two\n- AC-9\n```\n- AC-10\n- see AC-1\n")).toEqual([
|
|
16
|
+
{ title: "One", ids: ["AC-10", ""] },
|
|
17
|
+
])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("ends a slice at any other heading", () => {
|
|
21
|
+
expect(
|
|
22
|
+
slicesOf("## One\n\n- AC-1\n\n### Why\n\n- shares the lexer\n\n# Later\n\n- AC-5\n"),
|
|
23
|
+
).toEqual([{ title: "One", ids: ["AC-1"] }])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("reads no slice from a file without level-2 headings", () => {
|
|
27
|
+
expect(slicesOf("# Slices\n\n- AC-1\n")).toEqual([])
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe("sliceCriteria", () => {
|
|
32
|
+
const slices = [
|
|
33
|
+
{ title: "A", ids: ["AC-1", "AC-3"] },
|
|
34
|
+
{ title: "B", ids: ["AC-2"] },
|
|
35
|
+
{ title: "C", ids: ["AC-4"] },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
it("keeps an earlier slice to its own IDs", () => {
|
|
39
|
+
expect(sliceCriteria(slices, 1)).toEqual({ only: ["AC-2"] })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("gives the last slice every ID no earlier slice builds", () => {
|
|
43
|
+
expect(sliceCriteria(slices, 2)).toEqual({ except: ["AC-1", "AC-3", "AC-2"] })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("leaves a single slice unsliced", () => {
|
|
47
|
+
expect(sliceCriteria([slices[0]!], 0)).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { read, refuse, scope, vars } from "../../flows/index.js"
|
|
2
|
+
import { HEADING, ITEM, markFenced } from "../shared/fences.js"
|
|
3
|
+
import { planner, REQUIREMENTS, SLICES } from "../shared/steps.js"
|
|
4
|
+
import * as t from "../shared/text.js"
|
|
5
|
+
import { defaults } from "../shared/vars.js"
|
|
6
|
+
import { criterionIds, type CriteriaSlice } from "./criteria.js"
|
|
7
|
+
|
|
8
|
+
/** One slice of the change: the criterion IDs it builds, in the order `.gtd/SLICES.md` lists them. */
|
|
9
|
+
export interface Slice {
|
|
10
|
+
readonly title: string
|
|
11
|
+
readonly ids: readonly string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ID = /^(AC-\d+)(?![\w-])/
|
|
15
|
+
|
|
16
|
+
/** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
|
|
17
|
+
export const sliceCap = (): number => {
|
|
18
|
+
const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria!
|
|
19
|
+
if (!/^[1-9]\d*$/.test(value))
|
|
20
|
+
refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`)
|
|
21
|
+
return Number(value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
|
|
25
|
+
export const slicesOf = (text: string): readonly Slice[] => {
|
|
26
|
+
const slices: { title: string; ids: string[] }[] = []
|
|
27
|
+
// Only items right under a `##` heading: another heading ends the slice.
|
|
28
|
+
let open = false
|
|
29
|
+
for (const { line, fenced } of markFenced(text)) {
|
|
30
|
+
if (fenced) continue
|
|
31
|
+
const heading = HEADING.exec(line)
|
|
32
|
+
if (heading !== null) {
|
|
33
|
+
open = heading[1] === "##"
|
|
34
|
+
if (open) slices.push({ title: heading[2]!, ids: [] })
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
const item = ITEM.exec(line)
|
|
38
|
+
if (item !== null && open) slices.at(-1)!.ids.push(ID.exec(item[1]!)?.[1] ?? "")
|
|
39
|
+
}
|
|
40
|
+
return slices
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const twice = (list: readonly string[]): string[] => [
|
|
44
|
+
...new Set(list.filter((id, i) => list.indexOf(id) !== i)),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
|
|
48
|
+
const sliceFault = (
|
|
49
|
+
slices: readonly Slice[],
|
|
50
|
+
ids: readonly string[],
|
|
51
|
+
cap: number,
|
|
52
|
+
): string | undefined => {
|
|
53
|
+
const listed = slices.flatMap((s) => s.ids)
|
|
54
|
+
if (slices.length === 0) return `no "## " slice`
|
|
55
|
+
const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title)
|
|
56
|
+
if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`
|
|
57
|
+
if (listed.includes("")) return "a list item not led by a criterion ID"
|
|
58
|
+
const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title)
|
|
59
|
+
if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`
|
|
60
|
+
const repeated = twice(listed)
|
|
61
|
+
if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`
|
|
62
|
+
const unknown = listed.filter((id) => !ids.includes(id))
|
|
63
|
+
if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`
|
|
64
|
+
const missing = ids.filter((id) => !listed.includes(id))
|
|
65
|
+
if (missing.length > 0) return `no slice for: ${missing.join(", ")}`
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
|
|
70
|
+
export const requireSlices = (ids: readonly string[], cap: number): readonly Slice[] => {
|
|
71
|
+
const slices = slicesOf(read(SLICES) ?? "")
|
|
72
|
+
const why = sliceFault(slices, ids, cap)
|
|
73
|
+
if (why !== undefined)
|
|
74
|
+
refuse(
|
|
75
|
+
`gtd land: slices: ${SLICES} has ${why} — list each slice as a "## <title>" heading over "- AC-<n>" items, every criterion of ${REQUIREMENTS} in exactly one slice, at most ${cap} per slice`,
|
|
76
|
+
)
|
|
77
|
+
return slices
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The slices to build, in order. Criteria within `sliceMaxCriteria` are one
|
|
82
|
+
* slice and no turn; above it `design.slicing` groups them.
|
|
83
|
+
*/
|
|
84
|
+
export const slicing = async (): Promise<readonly Slice[]> => {
|
|
85
|
+
const all = criterionIds(read(REQUIREMENTS) ?? "")
|
|
86
|
+
const cap = sliceCap()
|
|
87
|
+
if (all.length <= cap) return [{ title: "", ids: all }]
|
|
88
|
+
return scope("design", async () => {
|
|
89
|
+
await t.agentWithSkills("slicing", t.slicingPrompt(cap), {
|
|
90
|
+
label: "Slicing the change",
|
|
91
|
+
file: SLICES,
|
|
92
|
+
model: planner(),
|
|
93
|
+
system: t.designSystem(),
|
|
94
|
+
})
|
|
95
|
+
return requireSlices(all, cap)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
|
|
100
|
+
export const sliceCriteria = (slices: readonly Slice[], i: number): CriteriaSlice | undefined =>
|
|
101
|
+
slices.length === 1
|
|
102
|
+
? undefined
|
|
103
|
+
: i < slices.length - 1
|
|
104
|
+
? { only: slices[i]!.ids }
|
|
105
|
+
: { except: slices.slice(0, -1).flatMap((s) => s.ids) }
|
package/src/workflows/index.ts
CHANGED
|
@@ -17,3 +17,8 @@ export const markFenced = (text: string): readonly { line: string; fenced: boole
|
|
|
17
17
|
return { line, fenced }
|
|
18
18
|
})
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
/** A markdown heading: its `#` run, then its text. */
|
|
22
|
+
export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/
|
|
23
|
+
/** A list item, its checkbox dropped: its text. */
|
|
24
|
+
export const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/
|
|
@@ -8,8 +8,9 @@ const { defaults, envDefaults, feature, fix, review, ShippedOptions } = workflow
|
|
|
8
8
|
const { flagShipped } = options
|
|
9
9
|
|
|
10
10
|
describe("the shipped workflow factories", () => {
|
|
11
|
-
it("take exactly the settings the shipped workflows declare as options", () => {
|
|
12
|
-
|
|
11
|
+
it("take exactly the settings the shipped workflows declare as options, and contextLimit", () => {
|
|
12
|
+
const settings = Object.keys(ShippedOptions.fields).filter((name) => name !== "contextLimit")
|
|
13
|
+
expect(settings.sort()).toEqual(
|
|
13
14
|
[...Object.keys(defaults), ...Object.keys(envDefaults)]
|
|
14
15
|
.filter((name) => !name.startsWith("flag_"))
|
|
15
16
|
.sort(),
|
|
@@ -23,6 +24,14 @@ describe("the shipped workflow factories", () => {
|
|
|
23
24
|
expect(defaults).toHaveProperty(`flag_${name}`, defaultOn ? "1" : "0")
|
|
24
25
|
})
|
|
25
26
|
|
|
27
|
+
it("keep contextLimit out of the settings", () => {
|
|
28
|
+
const built = configure(feature({ contextLimit: { hard: 0.8 } }))
|
|
29
|
+
expect(built).toMatchObject({ ok: true })
|
|
30
|
+
const build = built?.ok === true ? built.build : undefined
|
|
31
|
+
expect(build?.defaults).not.toHaveProperty("contextLimit")
|
|
32
|
+
expect(build?.envDefaults).not.toHaveProperty("contextLimit")
|
|
33
|
+
})
|
|
34
|
+
|
|
26
35
|
it("declare a seamAllow default a factory option overrides", () => {
|
|
27
36
|
expect(configure(feature({ seamAllow: "docs/**" }))).toMatchObject({
|
|
28
37
|
ok: true,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type WorkflowDoors,
|
|
7
7
|
type WorkflowFactory,
|
|
8
8
|
refuse,
|
|
9
|
+
scope,
|
|
9
10
|
vars,
|
|
10
11
|
} from "../../flows/index.js"
|
|
11
12
|
import { defaultFlagRegistry, defaults, envDefaults } from "./vars.js"
|
|
@@ -20,12 +21,20 @@ export const ShippedOptions = Schema.Struct({
|
|
|
20
21
|
reviewSeverityMinP: setting,
|
|
21
22
|
refactorFlagPattern: setting,
|
|
22
23
|
seamAllow: setting,
|
|
24
|
+
sliceMaxCriteria: setting,
|
|
23
25
|
qualityReviews: setting,
|
|
24
26
|
refactorQualityReviews: setting,
|
|
25
27
|
testCommand: setting,
|
|
26
28
|
plannerModel: setting,
|
|
27
29
|
coderModel: setting,
|
|
28
30
|
flagMode: Schema.optionalWith(Schema.Boolean, { exact: true }),
|
|
31
|
+
contextLimit: Schema.optionalWith(
|
|
32
|
+
Schema.Struct({
|
|
33
|
+
soft: Schema.optionalWith(Schema.Number, { exact: true }),
|
|
34
|
+
hard: Schema.optionalWith(Schema.Number, { exact: true }),
|
|
35
|
+
}),
|
|
36
|
+
{ exact: true },
|
|
37
|
+
),
|
|
29
38
|
})
|
|
30
39
|
|
|
31
40
|
export type ShippedOptions = Schema.Schema.Encoded<typeof ShippedOptions>
|
|
@@ -50,6 +59,12 @@ export const flagModeOn = (): boolean => {
|
|
|
50
59
|
return value === "true"
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
// Not a setting: a factory option is known at load, so the scope wraps the
|
|
63
|
+
// whole flow without reading a setting before its first step. The engine
|
|
64
|
+
// refuses limits it cannot use when it reaches an agent step.
|
|
65
|
+
const limited = (flow: Flow, context: ShippedOptions["contextLimit"]): Flow =>
|
|
66
|
+
context === undefined ? flow : () => scope({ context }, flow)
|
|
67
|
+
|
|
53
68
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
54
69
|
export const shipped = (
|
|
55
70
|
flow: Flow,
|
|
@@ -58,8 +73,8 @@ export const shipped = (
|
|
|
58
73
|
): WorkflowFactory<ShippedOptions> =>
|
|
59
74
|
defineWorkflow(
|
|
60
75
|
Schema.standardSchemaV1(ShippedOptions, { onExcessProperty: "error", errors: "all" }),
|
|
61
|
-
(options) => ({
|
|
62
|
-
flow,
|
|
76
|
+
({ contextLimit, ...options }) => ({
|
|
77
|
+
flow: limited(flow, contextLimit),
|
|
63
78
|
defaults: over(defaults, options),
|
|
64
79
|
envDefaults: over(envDefaults, options),
|
|
65
80
|
doors,
|
|
@@ -77,10 +92,10 @@ export const flagShipped = (
|
|
|
77
92
|
): WorkflowFactory<FlagOptions> =>
|
|
78
93
|
defineWorkflow(
|
|
79
94
|
Schema.standardSchemaV1(FlagOptions, { onExcessProperty: "error", errors: "all" }),
|
|
80
|
-
(options) => {
|
|
95
|
+
({ contextLimit, ...options }) => {
|
|
81
96
|
const flagDefaults = { ...defaults, flag: "", flagRegistry: defaultFlagRegistry }
|
|
82
97
|
return {
|
|
83
|
-
flow,
|
|
98
|
+
flow: limited(flow, contextLimit),
|
|
84
99
|
defaults: over(flagDefaults, options),
|
|
85
100
|
envDefaults: over(envDefaults, options),
|
|
86
101
|
doors: { [name]: { args: [{ name: "flag" }], vars: ({ flag }) => ({ flag: flag ?? "" }) } },
|
|
@@ -12,7 +12,9 @@ export const ESCALATION = ".gtd/ESCALATION.md"
|
|
|
12
12
|
export const REQUIREMENTS = ".gtd/REQUIREMENTS.md"
|
|
13
13
|
export const REVIEW = ".gtd/REVIEW.md"
|
|
14
14
|
export const REFACTOR = ".gtd/REFACTOR.md"
|
|
15
|
+
export const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md"
|
|
15
16
|
export const QUALITY = ".gtd/QUALITY.md"
|
|
17
|
+
export const SLICES = ".gtd/SLICES.md"
|
|
16
18
|
|
|
17
19
|
export const planner = (): string => env.plannerModel ?? ""
|
|
18
20
|
export const coder = (): string => env.coderModel ?? ""
|