@pmelab/gtd 58.2.0 → 59.0.1

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