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