@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.
Files changed (142) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +115 -390
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
  4. package/dist/gtd.bundle.mjs +76 -59
  5. package/dist/testkit/gherkin.mjs +30 -2
  6. package/dist/testkit.mjs +1 -1
  7. package/package.json +3 -42
  8. package/skills/authoring/SKILL.md +31 -25
  9. package/src/cli/Cli.test.ts +15 -19
  10. package/src/cli/Cli.ts +2 -4
  11. package/src/cli/Program.test.ts +60 -29
  12. package/src/cli/Program.ts +5 -7
  13. package/src/edge/Edge.ts +3 -3
  14. package/src/flags/index.test.ts +0 -13
  15. package/src/flags/index.ts +1 -3
  16. package/src/gherkin/git-control.feature +78 -0
  17. package/src/gherkin/index.ts +35 -1
  18. package/src/install/Install.test.ts +14 -11
  19. package/src/install/Install.ts +38 -15
  20. package/src/install/index.ts +1 -2
  21. package/src/replay/Replay.test.ts +1 -1
  22. package/src/replay/index.ts +1 -1
  23. package/src/step/planStart.ts +6 -1
  24. package/src/testkit/TestRepo.test.ts +118 -2
  25. package/src/testkit/TestRepo.ts +57 -6
  26. package/src/testkit/index.ts +1 -0
  27. package/src/twins/EmittedScriptRecognizer.ts +12 -14
  28. package/src/twins/FlowScripts.ts +10 -9
  29. package/src/twins/InMemRepo.ts +26 -1
  30. package/src/twins/Layers.ts +12 -1
  31. package/src/twins/index.ts +6 -1
  32. package/src/workflow/index.ts +1 -0
  33. package/src/workflow/load.test.ts +138 -113
  34. package/src/workflow/load.ts +1 -1
  35. package/src/workflow/vars.test.ts +30 -3
  36. package/src/workflow/vars.ts +34 -2
  37. package/src/workflow-kit/index.ts +1 -1
  38. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  39. package/dist/chunks/review-Bfaror38.mjs +0 -427
  40. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  41. package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
  42. package/dist/workflows/cleanup.mjs +0 -36
  43. package/dist/workflows/feature.mjs +0 -1018
  44. package/dist/workflows/fix.mjs +0 -41
  45. package/dist/workflows/review.mjs +0 -45
  46. package/dist/workflows/rollout.mjs +0 -48
  47. package/dist/workflows.mjs +0 -40651
  48. package/src/install/ConfigScaffold.ts +0 -22
  49. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  50. package/src/workflows/cleanup/cleanup.feature +0 -315
  51. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  52. package/src/workflows/cleanup/cleanup.ts +0 -52
  53. package/src/workflows/cleanup/index.ts +0 -6
  54. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  55. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  56. package/src/workflows/feature/criteria-coverage.feature +0 -102
  57. package/src/workflows/feature/criteria.test.ts +0 -147
  58. package/src/workflows/feature/criteria.ts +0 -267
  59. package/src/workflows/feature/fast-path.feature +0 -93
  60. package/src/workflows/feature/feature.test.ts +0 -338
  61. package/src/workflows/feature/feature.ts +0 -156
  62. package/src/workflows/feature/green.ts +0 -19
  63. package/src/workflows/feature/index.ts +0 -18
  64. package/src/workflows/feature/lens-models.feature +0 -69
  65. package/src/workflows/feature/lens-review.feature +0 -566
  66. package/src/workflows/feature/naive-acceptance.feature +0 -147
  67. package/src/workflows/feature/naive-test.feature +0 -263
  68. package/src/workflows/feature/one-step-land.feature +0 -93
  69. package/src/workflows/feature/planning.ts +0 -41
  70. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  71. package/src/workflows/feature/refactor-first.feature +0 -158
  72. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  73. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  74. package/src/workflows/feature/refactor-review.feature +0 -1528
  75. package/src/workflows/feature/refactor.test.ts +0 -1220
  76. package/src/workflows/feature/refactor.ts +0 -368
  77. package/src/workflows/feature/release-flag.feature +0 -167
  78. package/src/workflows/feature/release-flag.test.ts +0 -49
  79. package/src/workflows/feature/release-flag.ts +0 -46
  80. package/src/workflows/feature/review-gate.feature +0 -241
  81. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  82. package/src/workflows/feature/risk-tier.feature +0 -351
  83. package/src/workflows/feature/seam-rejection.feature +0 -146
  84. package/src/workflows/feature/seam.feature +0 -297
  85. package/src/workflows/feature/seam.test.ts +0 -527
  86. package/src/workflows/feature/seam.ts +0 -426
  87. package/src/workflows/feature/standards.feature +0 -354
  88. package/src/workflows/feature/steps.test.ts +0 -65
  89. package/src/workflows/feature/steps.ts +0 -45
  90. package/src/workflows/feature/tier-pin.feature +0 -66
  91. package/src/workflows/feature/tier.test.ts +0 -166
  92. package/src/workflows/feature/tier.ts +0 -56
  93. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  94. package/src/workflows/fix/fix-quality.feature +0 -212
  95. package/src/workflows/fix/fix.feature +0 -79
  96. package/src/workflows/fix/fix.test.ts +0 -74
  97. package/src/workflows/fix/fix.ts +0 -52
  98. package/src/workflows/fix/index.ts +0 -6
  99. package/src/workflows/index.ts +0 -47
  100. package/src/workflows/review/index.ts +0 -6
  101. package/src/workflows/review/review-door.feature +0 -199
  102. package/src/workflows/review/review.test.ts +0 -27
  103. package/src/workflows/review/review.ts +0 -60
  104. package/src/workflows/rollout/index.ts +0 -6
  105. package/src/workflows/rollout/rollout.feature +0 -510
  106. package/src/workflows/rollout/rollout.test.ts +0 -263
  107. package/src/workflows/rollout/rollout.ts +0 -82
  108. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  109. package/src/workflows/shared/access.test.ts +0 -62
  110. package/src/workflows/shared/access.ts +0 -24
  111. package/src/workflows/shared/config.fixture.ts +0 -12
  112. package/src/workflows/shared/fences.ts +0 -24
  113. package/src/workflows/shared/health.test.ts +0 -148
  114. package/src/workflows/shared/health.ts +0 -191
  115. package/src/workflows/shared/learn.test.ts +0 -140
  116. package/src/workflows/shared/learn.ts +0 -201
  117. package/src/workflows/shared/lenses.test.ts +0 -39
  118. package/src/workflows/shared/lenses.ts +0 -294
  119. package/src/workflows/shared/options.test.ts +0 -157
  120. package/src/workflows/shared/options.ts +0 -92
  121. package/src/workflows/shared/progress.feature +0 -124
  122. package/src/workflows/shared/prose.ts +0 -263
  123. package/src/workflows/shared/recheck.feature +0 -60
  124. package/src/workflows/shared/registry.test.ts +0 -345
  125. package/src/workflows/shared/registry.ts +0 -183
  126. package/src/workflows/shared/reserved.ts +0 -11
  127. package/src/workflows/shared/review.test.ts +0 -1062
  128. package/src/workflows/shared/review.ts +0 -446
  129. package/src/workflows/shared/skills.test.ts +0 -121
  130. package/src/workflows/shared/skills.ts +0 -49
  131. package/src/workflows/shared/standards.test.ts +0 -90
  132. package/src/workflows/shared/standards.ts +0 -156
  133. package/src/workflows/shared/steps.test.ts +0 -219
  134. package/src/workflows/shared/steps.ts +0 -201
  135. package/src/workflows/shared/tail.test.ts +0 -454
  136. package/src/workflows/shared/tail.ts +0 -264
  137. package/src/workflows/shared/text.fixture.ts +0 -128
  138. package/src/workflows/shared/text.test.ts +0 -1430
  139. package/src/workflows/shared/text.ts +0 -1700
  140. package/src/workflows/shared/threads.test.ts +0 -129
  141. package/src/workflows/shared/vars.ts +0 -47
  142. 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
- })