@pmelab/gtd 58.1.0 → 59.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 +115 -390
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
- package/dist/gtd.bundle.mjs +76 -59
- package/dist/testkit/gherkin.mjs +30 -2
- package/dist/testkit.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +31 -25
- package/src/cli/Cli.test.ts +15 -19
- package/src/cli/Cli.ts +2 -4
- package/src/cli/Program.test.ts +60 -29
- package/src/cli/Program.ts +5 -7
- package/src/edge/Edge.ts +3 -3
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/gherkin/git-control.feature +78 -0
- package/src/gherkin/index.ts +35 -1
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/testkit/TestRepo.test.ts +118 -2
- package/src/testkit/TestRepo.ts +57 -6
- package/src/testkit/index.ts +1 -0
- package/src/twins/EmittedScriptRecognizer.ts +12 -14
- package/src/twins/FlowScripts.ts +10 -9
- package/src/twins/InMemRepo.ts +26 -1
- package/src/twins/Layers.ts +12 -1
- package/src/twins/index.ts +6 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- package/src/workflows/shared/workflows.steps.ts +0 -54
|
@@ -1,527 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
-
import {
|
|
3
|
-
installContext,
|
|
4
|
-
type Change,
|
|
5
|
-
type FlowContext,
|
|
6
|
-
type KeptTip,
|
|
7
|
-
type StepRequest,
|
|
8
|
-
} from "../../flows/index.js"
|
|
9
|
-
import { globMatches } from "../../wire/index.js"
|
|
10
|
-
import { fixtureContext } from "../shared/text.fixture.js"
|
|
11
|
-
import {
|
|
12
|
-
implement,
|
|
13
|
-
judgeSeam,
|
|
14
|
-
naivePass,
|
|
15
|
-
acceptanceFault,
|
|
16
|
-
naiveAcceptance,
|
|
17
|
-
seamBuild,
|
|
18
|
-
type SeamOutcome,
|
|
19
|
-
} from "./seam.js"
|
|
20
|
-
import { captureStep } from "../shared/text.fixture.js"
|
|
21
|
-
|
|
22
|
-
afterEach(() => installContext(undefined))
|
|
23
|
-
|
|
24
|
-
const change = (path: string, status: Change["status"]): Change => ({
|
|
25
|
-
path,
|
|
26
|
-
status,
|
|
27
|
-
before: status === "added" ? undefined : "a",
|
|
28
|
-
after: status === "deleted" ? undefined : "b",
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const tip = (...changes: Change[]): KeptTip =>
|
|
32
|
-
({
|
|
33
|
-
hash: "abc",
|
|
34
|
-
changes: Object.assign([...changes], { paths: [], get: () => undefined }),
|
|
35
|
-
}) as KeptTip
|
|
36
|
-
|
|
37
|
-
describe("judgeSeam", () => {
|
|
38
|
-
const judgeRequests = (kept: KeptTip, vars: Record<string, string> = {}) => {
|
|
39
|
-
const requests: StepRequest[] = []
|
|
40
|
-
installContext({
|
|
41
|
-
...fixtureContext({ vars }),
|
|
42
|
-
matches: globMatches,
|
|
43
|
-
step: async (request: StepRequest) => {
|
|
44
|
-
requests.push(request)
|
|
45
|
-
return { answers: { shape: { answer: "land", p: 0.9 } }, truncated: [] }
|
|
46
|
-
},
|
|
47
|
-
} as unknown as FlowContext)
|
|
48
|
-
return judgeSeam(kept).then(() => requests)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
it.each([
|
|
52
|
-
["src/foo*bar.ts", "src/foobar.ts"],
|
|
53
|
-
["src/**", "src/deep/foobar.ts"],
|
|
54
|
-
])("added %s does not cover modified %s", async (added, sibling) => {
|
|
55
|
-
const requests = await judgeRequests(tip(change(added, "added"), change(sibling, "modified")))
|
|
56
|
-
const request = requests[0]
|
|
57
|
-
if (request?.kind !== "judge") throw new Error("expected a judge step")
|
|
58
|
-
expect(JSON.parse(request.evidence.strays as string)).toEqual([sibling])
|
|
59
|
-
expect(JSON.parse(request.evidence.inAllowlist as string)).toContain(added)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it("splits seamAllow on commas, trims entries and drops blanks", async () => {
|
|
63
|
-
const requests = await judgeRequests(tip(change("src/x.ts", "modified")), {
|
|
64
|
-
seamAllow: " a/** , ,b.ts,, ",
|
|
65
|
-
})
|
|
66
|
-
const request = requests[0]
|
|
67
|
-
if (request?.kind !== "judge") throw new Error("expected a judge step")
|
|
68
|
-
expect(JSON.parse(request.evidence.allow as string)).toEqual(["a/**", "b.ts"])
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it("exempts only added paths: a deleted path stays a stray", async () => {
|
|
72
|
-
const requests = await judgeRequests(tip(change("src/gone.ts", "deleted")))
|
|
73
|
-
const request = requests[0]
|
|
74
|
-
if (request?.kind !== "judge") throw new Error("expected a judge step")
|
|
75
|
-
expect(JSON.parse(request.evidence.strays as string)).toEqual(["src/gone.ts"])
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
describe("naivePass", () => {
|
|
80
|
-
const capture = (rejection?: { strays: string[]; p: number }) =>
|
|
81
|
-
captureStep(() => naivePass(rejection), { scope: "seam" })
|
|
82
|
-
|
|
83
|
-
it("keeps its turn under the step name seam.naive", async () => {
|
|
84
|
-
const request = await capture()
|
|
85
|
-
if (request.kind !== "agent") throw new Error("unreachable")
|
|
86
|
-
expect(request.name).toBe("naive")
|
|
87
|
-
expect(request.options).toMatchObject({ keep: true })
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it("runs on the coder model, base by default, the coderModel setting otherwise", async () => {
|
|
91
|
-
const plain = await capture()
|
|
92
|
-
const set = await captureStep(() => naivePass(), {
|
|
93
|
-
scope: "seam",
|
|
94
|
-
env: { coderModel: "sonnet" },
|
|
95
|
-
})
|
|
96
|
-
if (plain.kind !== "agent" || set.kind !== "agent") throw new Error("unreachable")
|
|
97
|
-
expect(plain.options.model).toBe("base")
|
|
98
|
-
expect(set.options.model).toBe("sonnet")
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it("carries the rejection into the retry prompt", async () => {
|
|
102
|
-
const request = await capture({ strays: ["src/core.ts"], p: 0.8 })
|
|
103
|
-
if (request.kind !== "agent") throw new Error("unreachable")
|
|
104
|
-
expect(request.prompt).toContain("src/core.ts")
|
|
105
|
-
expect(request.prompt).toContain("0.8")
|
|
106
|
-
})
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
describe("naiveAcceptance", () => {
|
|
110
|
-
it("carries every test file the pass added or changed, named or not", () => {
|
|
111
|
-
installContext({ ...fixtureContext({}), matches: globMatches } as unknown as FlowContext)
|
|
112
|
-
expect(
|
|
113
|
-
naiveAcceptance(
|
|
114
|
-
tip(
|
|
115
|
-
change("src/a.test.ts", "modified"),
|
|
116
|
-
change("tests/b.feature", "added"),
|
|
117
|
-
change("src/gone.test.ts", "deleted"),
|
|
118
|
-
change("src/calc.ts", "modified"),
|
|
119
|
-
change(".gtd/x.test.ts", "added"),
|
|
120
|
-
),
|
|
121
|
-
),
|
|
122
|
-
).toEqual({ hash: "abc", tests: ["src/a.test.ts", "tests/b.feature"] })
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
describe("acceptanceFault", () => {
|
|
127
|
-
const file = (path: string, status: Change["status"], after: string): Change => ({
|
|
128
|
-
path,
|
|
129
|
-
status,
|
|
130
|
-
before: status === "added" ? undefined : "",
|
|
131
|
-
after,
|
|
132
|
-
})
|
|
133
|
-
const refusal = (...changes: Change[]): string | undefined => {
|
|
134
|
-
installContext({ ...fixtureContext({}), matches: globMatches } as unknown as FlowContext)
|
|
135
|
-
return acceptanceFault(tip(...changes))
|
|
136
|
-
}
|
|
137
|
-
const tagged = 'import { add } from "../src/calc.js"\nit("adds")'
|
|
138
|
-
|
|
139
|
-
it("refuses a test importing a file the pass added, resolved from the test's directory", () => {
|
|
140
|
-
const message = refusal(
|
|
141
|
-
file("src/naive/helper.ts", "added", "export const h = 1"),
|
|
142
|
-
file("src/naive/helper.test.ts", "added", `import { h } from "./helper.js"\n${tagged}`),
|
|
143
|
-
)
|
|
144
|
-
expect(message).toMatch(/^acceptance-internals: /)
|
|
145
|
-
expect(message).toContain("src/naive/helper.test.ts → src/naive/helper.ts")
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it("refuses an aliased or directory import of an added module", () => {
|
|
149
|
-
const added = file("src/naive/index.ts", "added", "")
|
|
150
|
-
expect(
|
|
151
|
-
refusal(added, file("tests/a.test.ts", "added", `import "@/naive"\n${tagged}`)),
|
|
152
|
-
).toContain("acceptance-internals")
|
|
153
|
-
expect(
|
|
154
|
-
refusal(added, file("tests/a.test.ts", "added", `import "../src/naive/index.js"\n${tagged}`)),
|
|
155
|
-
).toContain("acceptance-internals")
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it("refuses a bare . or .. import of an added index", () => {
|
|
159
|
-
expect(
|
|
160
|
-
refusal(
|
|
161
|
-
file("src/naive/index.ts", "added", ""),
|
|
162
|
-
file("src/naive/__tests__/a.test.ts", "added", `import { x } from ".."\n${tagged}`),
|
|
163
|
-
),
|
|
164
|
-
).toContain("acceptance-internals")
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it("reads a URL route or path-like data as no import", () => {
|
|
168
|
-
expect(
|
|
169
|
-
refusal(
|
|
170
|
-
file("src/routes/users.ts", "added", ""),
|
|
171
|
-
file(
|
|
172
|
-
"tests/a.test.ts",
|
|
173
|
-
"added",
|
|
174
|
-
`request(app).get("/users")\nrun("src/routes/users.ts")\n${tagged}`,
|
|
175
|
-
),
|
|
176
|
-
),
|
|
177
|
-
).toBe(undefined)
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it("reads no import out of a .feature file", () => {
|
|
181
|
-
expect(
|
|
182
|
-
refusal(
|
|
183
|
-
file("src/subtract.ts", "added", ""),
|
|
184
|
-
file("features/calc.feature", "added", `Given a file "src/subtract.ts"\n${tagged}`),
|
|
185
|
-
),
|
|
186
|
-
).toBe(undefined)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it("skips commented-out imports and takes a package subpath as no repository path", () => {
|
|
190
|
-
expect(
|
|
191
|
-
refusal(
|
|
192
|
-
file("src/naive/helper.ts", "added", ""),
|
|
193
|
-
file("src/date-fns/format.ts", "added", ""),
|
|
194
|
-
file(
|
|
195
|
-
"src/naive/a.test.ts",
|
|
196
|
-
"added",
|
|
197
|
-
`// import { h } from "./helper.js"\nimport { format } from "date-fns/format"\n${tagged}`,
|
|
198
|
-
),
|
|
199
|
-
),
|
|
200
|
-
).toBe(undefined)
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it("lets a test reach an existing module, a same-named file elsewhere, or a URL", () => {
|
|
204
|
-
expect(
|
|
205
|
-
refusal(
|
|
206
|
-
file("src/naive/api.ts", "added", ""),
|
|
207
|
-
file("src/calc.ts", "modified", ""),
|
|
208
|
-
file(
|
|
209
|
-
"tests/a.test.ts",
|
|
210
|
-
"added",
|
|
211
|
-
`import "../src/calc.js"\nimport "./api.js"\nfetch("https://x/api")\n${tagged}`,
|
|
212
|
-
),
|
|
213
|
-
),
|
|
214
|
-
).toBe(undefined)
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
const REVIEW = ".gtd/REVIEW.md"
|
|
219
|
-
const REFACTOR = ".gtd/REFACTOR.md"
|
|
220
|
-
const NOTE = ".gtd/REFACTOR-NOTE.md"
|
|
221
|
-
const reviewDoc = [
|
|
222
|
-
"# Review: abc1234",
|
|
223
|
-
"",
|
|
224
|
-
"<!-- base: 0000000000000000000000000000000000000000 -->",
|
|
225
|
-
"",
|
|
226
|
-
"## calc",
|
|
227
|
-
"- [ ] ./src/a.ts#1-1",
|
|
228
|
-
"add",
|
|
229
|
-
"",
|
|
230
|
-
].join("\n")
|
|
231
|
-
|
|
232
|
-
interface Seamed {
|
|
233
|
-
readonly names: string[]
|
|
234
|
-
readonly outcome: SeamOutcome | Error
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const FLAGGED = "# Requirements\n\n## Release Flag\n\nnew\n"
|
|
238
|
-
|
|
239
|
-
/** Runs `seamBuild` against a scripted context: the judge answers `answer`, every review signs off. */
|
|
240
|
-
const driveSeam = async (
|
|
241
|
-
answer: string,
|
|
242
|
-
stray: Change = change("src/a.ts", "modified"),
|
|
243
|
-
vars: Record<string, string> = {},
|
|
244
|
-
reds = 0,
|
|
245
|
-
requirements = FLAGGED,
|
|
246
|
-
): Promise<Seamed> => {
|
|
247
|
-
let red = false
|
|
248
|
-
let redsLeft = reds
|
|
249
|
-
const names: string[] = []
|
|
250
|
-
const stack: string[] = []
|
|
251
|
-
const files = new Map<string, string>([
|
|
252
|
-
["src/a.ts", "a"],
|
|
253
|
-
[".gtd/REQUIREMENTS.md", requirements],
|
|
254
|
-
])
|
|
255
|
-
let current = "c0"
|
|
256
|
-
const effects: Record<string, () => void> = {
|
|
257
|
-
note: () => void files.set(NOTE, "Seam: a shared op"),
|
|
258
|
-
deriving: () => void files.set(REFACTOR, "package"),
|
|
259
|
-
closing: () => void files.delete(REFACTOR),
|
|
260
|
-
"review.reviewing": () => void files.set(REVIEW, reviewDoc),
|
|
261
|
-
"review.closing": () => {
|
|
262
|
-
files.delete(REVIEW)
|
|
263
|
-
current = "after-refactor"
|
|
264
|
-
},
|
|
265
|
-
}
|
|
266
|
-
installContext({
|
|
267
|
-
...fixtureContext({ vars: { refactorQualityReviews: "", ...vars } }),
|
|
268
|
-
matches: globMatches,
|
|
269
|
-
refuse: (message: string) => {
|
|
270
|
-
throw new Error(message)
|
|
271
|
-
},
|
|
272
|
-
pushScope: (scope: { name?: string }) => void stack.push(scope.name ?? ""),
|
|
273
|
-
popScope: () => void stack.pop(),
|
|
274
|
-
read: (path: string) => files.get(path),
|
|
275
|
-
head: () => current,
|
|
276
|
-
kept: () => tip(stray),
|
|
277
|
-
changes: () => (red ? [change(".gtd/FEEDBACK.md", "added")] : []),
|
|
278
|
-
changesSince: () => [],
|
|
279
|
-
step: (request: StepRequest) => {
|
|
280
|
-
if (request.kind === "restart") return Promise.resolve()
|
|
281
|
-
const full = [...stack, request.name].join(".")
|
|
282
|
-
red = full === "seam.check" && redsLeft-- > 0
|
|
283
|
-
if (full === "seam.check") {
|
|
284
|
-
if (red) files.set(".gtd/FEEDBACK.md", "FAIL")
|
|
285
|
-
else files.delete(".gtd/FEEDBACK.md")
|
|
286
|
-
}
|
|
287
|
-
if (full === "seam.clear") files.delete(".gtd/FEEDBACK.md")
|
|
288
|
-
names.push(full)
|
|
289
|
-
effects[request.name]?.()
|
|
290
|
-
if (request.kind === "judge")
|
|
291
|
-
return Promise.resolve({ answers: { shape: { answer, p: 0.9 } }, truncated: [] })
|
|
292
|
-
return Promise.resolve(undefined)
|
|
293
|
-
},
|
|
294
|
-
} as unknown as FlowContext)
|
|
295
|
-
let outcome: SeamOutcome | Error
|
|
296
|
-
try {
|
|
297
|
-
outcome = await seamBuild("c-base")
|
|
298
|
-
} catch (error) {
|
|
299
|
-
outcome = error as Error
|
|
300
|
-
}
|
|
301
|
-
return { names, outcome }
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
describe("seamBuild", () => {
|
|
305
|
-
it("land resolves the kept tip to land, with the caller's base", async () => {
|
|
306
|
-
const h = await driveSeam("land")
|
|
307
|
-
expect(h.outcome).toEqual({ reference: "abc", base: "c-base", land: true })
|
|
308
|
-
expect(h.names).toEqual(["seam.naive", "seam.check", "seam.judge"])
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
it("with no release flag asked for resolves a vetted pass to land with no judge", async () => {
|
|
312
|
-
const h = await driveSeam("reject", undefined, {}, 0, "# Requirements\n")
|
|
313
|
-
expect(h.outcome).toEqual({ reference: "abc", base: "c-base", land: true })
|
|
314
|
-
expect(h.names).toEqual(["seam.naive", "seam.check"])
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
it("a red check retries the pass, and the judge sees it only once green", async () => {
|
|
318
|
-
const h = await driveSeam("land", undefined, {}, 1)
|
|
319
|
-
expect(h.names).toEqual(["seam.naive", "seam.check", "seam.naive", "seam.check", "seam.judge"])
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
it("an overruled red pass gets a fresh budget for the judge's rejects", async () => {
|
|
323
|
-
const h = await driveSeam("reject", undefined, {}, 3)
|
|
324
|
-
const steers = h.names.flatMap((name, i) => (name === "seam.steer" ? [i] : []))
|
|
325
|
-
expect(steers).toHaveLength(2)
|
|
326
|
-
expect(h.names.slice(steers[0]! + 1, steers[1]).filter((n) => n === "seam.judge")).toHaveLength(
|
|
327
|
-
3,
|
|
328
|
-
)
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
it("a third red rests at seam.steer, and a clean landing there takes the red pass to the judge", async () => {
|
|
332
|
-
const h = await driveSeam("land", undefined, {}, 3)
|
|
333
|
-
expect(h.names).toEqual([
|
|
334
|
-
"seam.naive",
|
|
335
|
-
"seam.check",
|
|
336
|
-
"seam.naive",
|
|
337
|
-
"seam.check",
|
|
338
|
-
"seam.naive",
|
|
339
|
-
"seam.check",
|
|
340
|
-
"seam.steer",
|
|
341
|
-
"seam.clear",
|
|
342
|
-
"seam.judge",
|
|
343
|
-
])
|
|
344
|
-
expect(h.outcome).toEqual({ reference: "abc", base: "c-base", land: true })
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
it("refactor-first runs the reviewed refactor right after the note and resolves HEAD after it", async () => {
|
|
348
|
-
const h = await driveSeam("refactor-first")
|
|
349
|
-
expect(h.outcome).toEqual({
|
|
350
|
-
reference: "abc",
|
|
351
|
-
base: "after-refactor",
|
|
352
|
-
refactored: true,
|
|
353
|
-
rebuild: { note: "Seam: a shared op", acceptance: { hash: "abc", tests: [] } },
|
|
354
|
-
})
|
|
355
|
-
expect(h.names.indexOf("seam.note")).toBe(h.names.indexOf("seam.judge") + 1)
|
|
356
|
-
expect(h.names.indexOf("refactor.deriving")).toBe(h.names.indexOf("seam.note") + 1)
|
|
357
|
-
expect(h.names).toContain("refactor.build.review.closing")
|
|
358
|
-
expect(h.names.some((n) => n.includes("approve"))).toBe(false)
|
|
359
|
-
expect(h.names.some((n) => n.startsWith("implement"))).toBe(false)
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it("refactor-first with only test-file strays resolves the caller's base with no refactor and no by-hand", async () => {
|
|
363
|
-
const h = await driveSeam("refactor-first", change("src/a.test.ts", "modified"), {
|
|
364
|
-
seamAllow: ".gtd/**",
|
|
365
|
-
})
|
|
366
|
-
expect(h.outcome).toEqual({
|
|
367
|
-
reference: "abc",
|
|
368
|
-
base: "c-base",
|
|
369
|
-
rebuild: { acceptance: { hash: "abc", tests: ["src/a.test.ts"] } },
|
|
370
|
-
})
|
|
371
|
-
expect(h.names).not.toContain("seam.by-hand")
|
|
372
|
-
expect(h.names.some((n) => n.startsWith("refactor"))).toBe(false)
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
it("rests an unreadable test-file stray at seam.by-hand", async () => {
|
|
376
|
-
const h = await driveSeam(
|
|
377
|
-
"refactor-first",
|
|
378
|
-
{ path: "src/a.test.ts", status: "modified", before: undefined, after: "b" },
|
|
379
|
-
{ seamAllow: ".gtd/**" },
|
|
380
|
-
)
|
|
381
|
-
expect(h.names).toContain("seam.by-hand")
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
it("rests unreadable strays at seam.by-hand and runs no refactor", async () => {
|
|
385
|
-
const h = await driveSeam("refactor-first", {
|
|
386
|
-
path: "src/a.ts",
|
|
387
|
-
status: "modified",
|
|
388
|
-
before: undefined,
|
|
389
|
-
after: "b",
|
|
390
|
-
})
|
|
391
|
-
expect(h.names).toContain("seam.by-hand")
|
|
392
|
-
expect(h.outcome).toEqual({
|
|
393
|
-
reference: "abc",
|
|
394
|
-
base: "c-base",
|
|
395
|
-
rebuild: { acceptance: { hash: "abc", tests: [] } },
|
|
396
|
-
})
|
|
397
|
-
})
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
describe("implement", () => {
|
|
401
|
-
let lastOptions: Extract<StepRequest, { kind: "agent" }>["options"] | undefined
|
|
402
|
-
let requirements = FLAGGED
|
|
403
|
-
const building = async (
|
|
404
|
-
reference: string | undefined,
|
|
405
|
-
tier?: "low" | "high",
|
|
406
|
-
amend?: boolean,
|
|
407
|
-
call: () => Promise<void> = () => implement({ reference, tier, amend }),
|
|
408
|
-
) => {
|
|
409
|
-
const requests: StepRequest[] = []
|
|
410
|
-
lastOptions = undefined
|
|
411
|
-
installContext(
|
|
412
|
-
fixtureContext(
|
|
413
|
-
{},
|
|
414
|
-
{
|
|
415
|
-
step: (request: StepRequest) => {
|
|
416
|
-
requests.push(request)
|
|
417
|
-
return Promise.resolve(
|
|
418
|
-
request.kind === "judge" ? { answers: {}, truncated: [] } : undefined,
|
|
419
|
-
)
|
|
420
|
-
},
|
|
421
|
-
pushScope: () => undefined,
|
|
422
|
-
popScope: () => undefined,
|
|
423
|
-
changesSince: () => [],
|
|
424
|
-
read: (path: string) => (path === ".gtd/REQUIREMENTS.md" ? requirements : undefined),
|
|
425
|
-
},
|
|
426
|
-
),
|
|
427
|
-
)
|
|
428
|
-
await call()
|
|
429
|
-
const request = requests.find((r) => r.kind === "agent" && r.name === "building")
|
|
430
|
-
if (request?.kind !== "agent") throw new Error("no building turn")
|
|
431
|
-
lastOptions = request.options
|
|
432
|
-
return request.prompt
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
it("builds on the coder model", async () => {
|
|
436
|
-
await building("abc")
|
|
437
|
-
expect(lastOptions?.model).toBe("base")
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
it("after a refactor, lets an empty turn complete the step and says so", async () => {
|
|
441
|
-
const prompt = await building("abc", undefined, undefined, () =>
|
|
442
|
-
implement({ reference: "abc", afterRefactor: true }),
|
|
443
|
-
)
|
|
444
|
-
expect(lastOptions?.allowEmpty).toBe(true)
|
|
445
|
-
expect(prompt).toMatch(/change\s+nothing and finish your turn/)
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
it("without a refactor, an empty turn stays an attempt", async () => {
|
|
449
|
-
const prompt = await building("abc")
|
|
450
|
-
expect(lastOptions?.allowEmpty).toBeUndefined()
|
|
451
|
-
expect(prompt).not.toMatch(/change\s+nothing/)
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
it("the deprecated positional form renders the same prompt as the options form", async () => {
|
|
455
|
-
const modern = await building("abc", "low")
|
|
456
|
-
const legacy = await building("abc", "low", undefined, () => implement("abc", "low"))
|
|
457
|
-
expect(legacy).toBe(modern)
|
|
458
|
-
expect(legacy).toContain("git show abc")
|
|
459
|
-
const amended = await building(undefined, undefined, true)
|
|
460
|
-
expect(
|
|
461
|
-
await building(undefined, undefined, undefined, () => implement(undefined, undefined, true)),
|
|
462
|
-
).toBe(amended)
|
|
463
|
-
const bare = await building(undefined, undefined, undefined, () => implement())
|
|
464
|
-
expect(bare).toBe(await building(undefined))
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
it("a rebuild quotes the refactor note and names the naive acceptance tests to carry over", async () => {
|
|
468
|
-
const prompt = await building("abc", undefined, undefined, () =>
|
|
469
|
-
implement({
|
|
470
|
-
reference: "abc",
|
|
471
|
-
rebuild: {
|
|
472
|
-
note: "# Note\n\nSeam: a shared op",
|
|
473
|
-
acceptance: { hash: "abc", tests: ["src/a.test.ts"] },
|
|
474
|
-
},
|
|
475
|
-
}),
|
|
476
|
-
)
|
|
477
|
-
expect(prompt).toContain(" > Seam: a shared op")
|
|
478
|
-
expect(prompt).toContain("git show abc -- 'src/a.test.ts'")
|
|
479
|
-
})
|
|
480
|
-
|
|
481
|
-
it("a rebuild whose naive pass wrote no test asks to carry nothing over", async () => {
|
|
482
|
-
const prompt = await building("abc", undefined, undefined, () =>
|
|
483
|
-
implement({ reference: "abc", rebuild: { acceptance: { hash: "abc", tests: [] } } }),
|
|
484
|
-
)
|
|
485
|
-
expect(prompt).not.toContain("Carry over")
|
|
486
|
-
expect(prompt).not.toContain("refactor note")
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
it("names the reference with a git show line", async () => {
|
|
490
|
-
expect(await building("abc")).toContain("git show abc")
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
it("carries no git show line without a reference", async () => {
|
|
494
|
-
expect(await building(undefined)).not.toContain("git show")
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
it("first lap renders the reference line and the fresh-write wording", async () => {
|
|
498
|
-
const prompt = await building("abc")
|
|
499
|
-
expect(prompt).toContain("git show abc")
|
|
500
|
-
expect(prompt).toContain("Implement them properly")
|
|
501
|
-
expect(prompt).toContain("write\n the change fresh")
|
|
502
|
-
expect(prompt).toContain("new release flag")
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
it("amend says the change exists, keeps the earlier flag and asks for no new one", async () => {
|
|
506
|
-
const prompt = await building(undefined, undefined, true)
|
|
507
|
-
expect(prompt).toContain("already exists")
|
|
508
|
-
expect(prompt).toContain("amend")
|
|
509
|
-
expect(prompt).toContain("Keep the release flag")
|
|
510
|
-
expect(prompt).toContain("if it added one")
|
|
511
|
-
expect(prompt).not.toContain("implement them properly")
|
|
512
|
-
expect(prompt).not.toContain("Implement them properly")
|
|
513
|
-
expect(prompt).not.toContain("write the change fresh")
|
|
514
|
-
expect(prompt).toContain("otherwise put")
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
it("asks for no release flag when the requirements ask for none, whatever the tier", async () => {
|
|
518
|
-
requirements = "# Requirements\n"
|
|
519
|
-
try {
|
|
520
|
-
const prompt = await building(undefined, "high", true)
|
|
521
|
-
expect(prompt).toContain("Add no release flag")
|
|
522
|
-
expect(prompt).toContain("amend")
|
|
523
|
-
} finally {
|
|
524
|
-
requirements = FLAGGED
|
|
525
|
-
}
|
|
526
|
-
})
|
|
527
|
-
})
|