@pmelab/gtd 27.3.0 → 27.5.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 (79) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +22 -1
  3. package/dist/chunks/{Schema-CyImoHTj.mjs → Schema-DO5ttJaI.mjs} +1 -1
  4. package/dist/chunks/{reserved-BPdVZkgv.mjs → reserved-DM5W_LAt.mjs} +64 -3
  5. package/dist/chunks/{tail-iLAMoigl.mjs → tail-6gvLXXcg.mjs} +1 -1
  6. package/dist/chunks/{wire-BY1S-obW.mjs → wire-C12dahrH.mjs} +6 -4
  7. package/dist/flows.mjs +27 -2
  8. package/dist/gtd.bundle.mjs +218 -32
  9. package/dist/testkit.mjs +358 -37
  10. package/dist/wire.mjs +1 -1
  11. package/dist/workflows/cleanup.mjs +2 -2
  12. package/dist/workflows/feature.mjs +140 -64
  13. package/dist/workflows/fix.mjs +2 -2
  14. package/dist/workflows/review.mjs +2 -2
  15. package/dist/workflows/rollout.mjs +1 -1
  16. package/dist/workflows.mjs +346 -107
  17. package/package.json +1 -1
  18. package/src/cli/Cli.test.ts +49 -1
  19. package/src/cli/Cli.ts +25 -8
  20. package/src/cli/History.ts +5 -0
  21. package/src/cli/Program.ts +1 -0
  22. package/src/edge/Edge.test.ts +205 -1
  23. package/src/edge/Edge.ts +105 -12
  24. package/src/emit/GitScript.test.ts +39 -3
  25. package/src/emit/GitScript.ts +25 -1
  26. package/src/emit/index.ts +2 -0
  27. package/src/flags/index.test.ts +14 -1
  28. package/src/flags/index.ts +14 -3
  29. package/src/flows/helpers.ts +34 -2
  30. package/src/flows/runtime.ts +7 -0
  31. package/src/install/Install.ts +2 -1
  32. package/src/platform/Git.test.ts +25 -1
  33. package/src/platform/Git.ts +30 -0
  34. package/src/platform/index.ts +1 -0
  35. package/src/replay/Format.test.ts +16 -1
  36. package/src/replay/Replay.test.ts +91 -0
  37. package/src/replay/Replay.ts +22 -2
  38. package/src/replay/Trailers.test.ts +110 -1
  39. package/src/replay/Trailers.ts +37 -1
  40. package/src/replay/index.ts +2 -0
  41. package/src/replay/judgeDiffShape.test.ts +43 -0
  42. package/src/replay/seamGate.test.ts +62 -0
  43. package/src/step/LandStep.ts +1 -0
  44. package/src/step/planStart.ts +3 -2
  45. package/src/step/planStep.test.ts +32 -0
  46. package/src/step/planStep.ts +3 -1
  47. package/src/testkit/EmittedScriptRecognizer.test.ts +27 -0
  48. package/src/testkit/EmittedScriptRecognizer.ts +12 -0
  49. package/src/testkit/FakeGitOperations.ts +5 -0
  50. package/src/testkit/GitTiers.ts +158 -0
  51. package/src/testkit/InMemRepo.ts +38 -3
  52. package/src/testkit/TestRepo.ts +3 -0
  53. package/src/testkit/steps.ts +18 -0
  54. package/src/wire/BeatDocument.test.ts +25 -0
  55. package/src/wire/BeatDocument.ts +4 -2
  56. package/src/wire/BeatPlain.test.ts +1 -0
  57. package/src/wire/BeatStatus.test.ts +2 -0
  58. package/src/wire/BeatStatus.ts +13 -2
  59. package/src/wire/WireSchema.test.ts +10 -0
  60. package/src/wire/types.ts +8 -0
  61. package/src/workflow/Workflow.ts +1 -0
  62. package/src/workflow/index.ts +8 -1
  63. package/src/workflow/vars.test.ts +23 -1
  64. package/src/workflow/vars.ts +24 -0
  65. package/src/workflows/feature/feature.ts +9 -2
  66. package/src/workflows/feature/index.ts +1 -0
  67. package/src/workflows/feature/seam-pin.feature +52 -0
  68. package/src/workflows/feature/seam.feature +387 -0
  69. package/src/workflows/feature/seam.test.ts +88 -0
  70. package/src/workflows/feature/seam.ts +110 -0
  71. package/src/workflows/index.ts +1 -0
  72. package/src/workflows/shared/options.test.ts +12 -1
  73. package/src/workflows/shared/options.ts +1 -0
  74. package/src/workflows/shared/skills.test.ts +20 -2
  75. package/src/workflows/shared/skills.ts +6 -0
  76. package/src/workflows/shared/text.test.ts +59 -0
  77. package/src/workflows/shared/text.ts +86 -0
  78. package/src/workflows/shared/vars.ts +4 -0
  79. package/src/workflows/shared/workflows.steps.ts +39 -1
@@ -0,0 +1,387 @@
1
+ @inmem
2
+ Feature: the naive pass and seam judge replace architecture behind GTD_FLAG_NAIVE_PASS
3
+
4
+ With the flag on, after design a naive pass is set aside under a kept ref, the
5
+ judge reads its diff, and the process either implements straight away, refactors
6
+ the seam first behind a human approval, or retries the naive pass. With the flag
7
+ off nothing changes: architecture and packages run as before.
8
+
9
+ Background:
10
+ Given the shipped workflows, as gtd install scaffolds them
11
+ And the gtd config ends with:
12
+ """
13
+ export const config = {
14
+ skills: {
15
+ implement: ["my-tdd", "my-runbook"],
16
+ },
17
+ }
18
+ """
19
+ And an environment variable "GTD_QUALITYREVIEWS" set to ""
20
+ And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
21
+ And a file "src/calc.ts" with:
22
+ """
23
+ export const add = (a: number, b: number) => a + b
24
+ """
25
+ And a file "src/calc.test.ts" with:
26
+ """
27
+ import { expect, it } from "vitest"
28
+ it("adds", () => expect(1 + 1).toBe(2))
29
+ """
30
+ And the working tree is committed as "feat: add calculator"
31
+ And a file "TODO.md" with:
32
+ """
33
+ # Add subtract
34
+
35
+ subtract(a, b) returns the difference.
36
+ """
37
+ And step "unwind" changes nothing
38
+ And step "start-gate.check" changes nothing
39
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
40
+ """
41
+ # Requirements
42
+
43
+ ## Concerns
44
+
45
+ ### Subtract
46
+
47
+ - `subtract(a, b)` returns `a - b`.
48
+ """
49
+ And step "design.gate.answer" changes nothing
50
+
51
+ Scenario: a land judgment goes straight to the implementation with no seam gate or refactor
52
+ Given step "seam.naive" writes "src/calc.ts" with:
53
+ """
54
+ export const add = (a: number, b: number) => a + b
55
+ export const subtract = (a: number, b: number) => a - b
56
+ """
57
+ And judge step "seam.judge" answers:
58
+ """
59
+ [{"id":"shape","answer":"land","p":0.9}]
60
+ """
61
+ When the process is driven
62
+ Then the process rests at "implement.building"
63
+ And the last commit body contains "Gtd-Judge:"
64
+ And the commit subjects end with:
65
+ """
66
+ chore: initial commit
67
+ feat: add calculator
68
+ gtd(human): idle → unwind
69
+ gtd(check): unwind → start-gate.check
70
+ gtd(check): start-gate.check → design.triage
71
+ gtd(agent): design.triage → design.gate.answer
72
+ gtd(human): design.gate.answer → seam.naive
73
+ gtd(agent): seam.naive → seam.judge
74
+ gtd(judge): seam.judge → implement.building
75
+ """
76
+
77
+ Scenario: config skills for the implement scope reach the implement step
78
+ Given step "seam.naive" writes "src/calc.ts" with:
79
+ """
80
+ export const add = (a: number, b: number) => a + b
81
+ export const subtract = (a: number, b: number) => a - b
82
+ """
83
+ And judge step "seam.judge" answers:
84
+ """
85
+ [{"id":"shape","answer":"land","p":0.9}]
86
+ """
87
+ When the process is driven
88
+ Then the process rests at "implement.building"
89
+ And the resting step's content contains "my-tdd, my-runbook"
90
+
91
+ Scenario: the naive pass lands as a marker commit and its turn stays off the branch
92
+ Given step "seam.naive" writes "src/calc.ts" with:
93
+ """
94
+ export const add = (a: number, b: number) => a + b
95
+ export const subtract = (a: number, b: number) => a - b
96
+ """
97
+ When the process is driven
98
+ Then the process rests at "seam.judge"
99
+ And the last commit subject is "gtd(agent): seam.naive → seam.judge"
100
+ And the last commit body contains "Gtd-Kept:"
101
+ And "src/calc.ts" does not contain "subtract"
102
+
103
+ Scenario: the implementation is one build turn, a green check, then the requirements are closed out
104
+ Given step "seam.naive" writes "src/calc.ts" with:
105
+ """
106
+ export const add = (a: number, b: number) => a + b
107
+ export const subtract = (a: number, b: number) => a - b
108
+ """
109
+ And judge step "seam.judge" answers:
110
+ """
111
+ [{"id":"shape","answer":"land","p":0.9}]
112
+ """
113
+ And step "implement.building" writes "src/calc.ts" with:
114
+ """
115
+ export const add = (a: number, b: number) => a + b
116
+ export const subtract = (a: number, b: number) => a - b
117
+ export const SUBTRACT_ENABLED = process.env.GTD_FLAG_SUBTRACT === "1"
118
+ """
119
+ And step "implement.health.check" changes nothing
120
+ And step "implement.closing" runs its script
121
+ When the process is driven
122
+ Then the process rests at "build.review.reviewing"
123
+ And the commit subjects end with:
124
+ """
125
+ gtd(judge): seam.judge → implement.building
126
+ gtd(agent): implement.building → implement.health.check
127
+ gtd(check): implement.health.check → implement.closing
128
+ gtd(check): implement.closing → build.review.reviewing
129
+ """
130
+ And ".gtd/REQUIREMENTS.md" does not exist
131
+
132
+ Scenario: a naive pass modifying a test file and adding a new one skips the judge
133
+ Given step "seam.naive" writes "src/subtract.ts" with:
134
+ """
135
+ export const subtract = (a: number, b: number) => a - b
136
+ """
137
+ And step "seam.naive" writes "src/calc.test.ts" with:
138
+ """
139
+ import { expect, it } from "vitest"
140
+ it("adds", () => expect(2 + 2).toBe(4))
141
+ """
142
+ When the process is driven
143
+ Then the process rests at "implement.building"
144
+ And the commit subjects end with:
145
+ """
146
+ gtd(human): design.gate.answer → seam.naive
147
+ gtd(agent): seam.naive → implement.building
148
+ """
149
+
150
+ Scenario: a naive pass adding a glob-named file does not hide a modified sibling
151
+ Given step "seam.naive" writes "src/calc*.ts" with:
152
+ """
153
+ export const subtract = (a: number, b: number) => a - b
154
+ """
155
+ And step "seam.naive" writes "src/calc.ts" with:
156
+ """
157
+ export const add = (a: number, b: number) => a + b
158
+ export const subtract = (a: number, b: number) => a - b
159
+ """
160
+ And judge step "seam.judge" answers:
161
+ """
162
+ [{"id":"shape","answer":"reject","p":0.9}]
163
+ """
164
+ When the process is driven
165
+ Then the process rests at "seam.steer"
166
+ And the resting step's content contains "src/calc.ts"
167
+ And the resting step's content does not contain "src/calc*.ts"
168
+
169
+ Scenario: a refactor-first judgment rests at the approval gate, then refactors, then implements
170
+ Given step "seam.naive" writes "src/calc.ts" with:
171
+ """
172
+ export const add = (a: number, b: number) => a + b
173
+ export const subtract = (a: number, b: number) => a - b
174
+ """
175
+ And judge step "seam.judge" answers:
176
+ """
177
+ [{"id":"shape","answer":"refactor-first","p":0.9}]
178
+ """
179
+ And step "refactor.deriving" writes ".gtd/REFACTOR.md" with:
180
+ """
181
+ # Behaviour-neutral refactor
182
+
183
+ ## Scope (read-only)
184
+
185
+ - `src/calc.ts` # gtd-path-exempt: scenario fixture, not a repo file
186
+ """
187
+ And step "refactor.building" writes "src/calc.ts" with:
188
+ """
189
+ export const add = (a: number, b: number) => a + b
190
+ export const op = (a: number, b: number, sign: number) => a + sign * b
191
+ """
192
+ And step "refactor.health.check" changes nothing
193
+ And step "refactor.closing" runs its script
194
+ When the process is driven
195
+ Then the process rests at "seam.approve"
196
+ And the resting step's content contains "src/calc.ts"
197
+ When gtd lands
198
+ Then the last commit subject is "gtd(human): seam.approve → refactor.deriving"
199
+ And the last commit body contains "Gtd-Approved:"
200
+ When the process is driven
201
+ Then the process rests at "implement.building"
202
+ And the commit subjects end with:
203
+ """
204
+ gtd(judge): seam.judge → seam.approve
205
+ gtd(human): seam.approve → refactor.deriving
206
+ gtd(check): refactor.deriving → refactor.building
207
+ gtd(agent): refactor.building → refactor.health.check
208
+ gtd(check): refactor.health.check → refactor.closing
209
+ gtd(check): refactor.closing → implement.building
210
+ """
211
+
212
+ Scenario: three rejects retry the naive pass twice, then rest at the steer gate
213
+ Given step "seam.naive" writes "src/calc.ts" with:
214
+ """
215
+ export const add = (a: number, b: number) => b + a
216
+ """
217
+ And judge step "seam.judge" answers:
218
+ """
219
+ [{"id":"shape","answer":"reject","p":0.9}]
220
+ """
221
+ When the process is driven
222
+ Then the process rests at "seam.steer"
223
+ And the resting step's content contains "Overrule"
224
+ And the resting step's content contains "src/calc.ts"
225
+ And the commit subjects end with:
226
+ """
227
+ gtd(human): design.gate.answer → seam.naive
228
+ gtd(agent): seam.naive → seam.judge
229
+ gtd(judge): seam.judge → seam.naive
230
+ gtd(agent): seam.naive → seam.judge
231
+ gtd(judge): seam.judge → seam.naive
232
+ gtd(agent): seam.naive → seam.judge
233
+ gtd(judge): seam.judge → seam.steer
234
+ """
235
+
236
+ Scenario: an edit at the steer gate runs the naive pass again with a fresh retry budget
237
+ Given step "seam.naive" writes "src/calc.ts" with:
238
+ """
239
+ export const add = (a: number, b: number) => b + a
240
+ """
241
+ And judge step "seam.judge" answers:
242
+ """
243
+ [{"id":"shape","answer":"reject","p":0.9}]
244
+ """
245
+ When the process is driven
246
+ Then the process rests at "seam.steer"
247
+ Given a file ".gtd/REQUIREMENTS.md" with:
248
+ """
249
+ # Requirements
250
+
251
+ ## Concerns
252
+
253
+ ### Subtract
254
+
255
+ - `subtract(a, b)` returns `a - b`, in a new module.
256
+ """
257
+ When gtd lands
258
+ Then the process rests at "seam.naive"
259
+ And the resting step's content does not contain "was rejected"
260
+ When the process is driven
261
+ Then the process rests at "seam.steer"
262
+ And the commit subjects end with:
263
+ """
264
+ gtd(human): seam.steer → seam.naive
265
+ gtd(agent): seam.naive → seam.judge
266
+ gtd(judge): seam.judge → seam.naive
267
+ gtd(agent): seam.naive → seam.judge
268
+ gtd(judge): seam.judge → seam.naive
269
+ gtd(agent): seam.naive → seam.judge
270
+ gtd(judge): seam.judge → seam.steer
271
+ """
272
+
273
+ Scenario: a clean landing at the steer gate overrules, with no seam gate and no refactor
274
+ Given step "seam.naive" writes "src/calc.ts" with:
275
+ """
276
+ export const add = (a: number, b: number) => b + a
277
+ """
278
+ And judge step "seam.judge" answers:
279
+ """
280
+ [{"id":"shape","answer":"reject","p":0.9}]
281
+ """
282
+ When the process is driven
283
+ Then the process rests at "seam.steer"
284
+ When gtd lands
285
+ Then the process rests at "implement.building"
286
+ And the commit subjects end with:
287
+ """
288
+ gtd(judge): seam.judge → seam.steer
289
+ gtd(human): seam.steer → implement.building
290
+ """
291
+
292
+ Scenario: review feedback re-runs design and the whole seam phase
293
+ Given step "seam.naive" writes "src/calc.ts" with:
294
+ """
295
+ export const add = (a: number, b: number) => b + a
296
+ """
297
+ And judge step "seam.judge" answers:
298
+ """
299
+ [{"id":"shape","answer":"land","p":0.9}]
300
+ """
301
+ And step "implement.building" writes "src/calc.ts" with:
302
+ """
303
+ export const add = (a: number, b: number) => a + b
304
+ export const SUBTRACT = process.env.GTD_FLAG_SUBTRACT === "1"
305
+ """
306
+ And step "implement.health.check" changes nothing
307
+ And step "implement.closing" runs its script
308
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
309
+ """
310
+ # Review: abc1234
311
+
312
+ <!-- base: 0000000 -->
313
+
314
+ ## calc
315
+
316
+ - [ ] ./src/calc.ts#1-1
317
+ new add function
318
+ """
319
+ When the process is driven
320
+ Then the process rests at "build.review.await-review"
321
+ Given a file ".gtd/REVIEW.md" with:
322
+ """
323
+ # Review: abc1234
324
+
325
+ <!-- base: 0000000 -->
326
+
327
+ ## calc
328
+
329
+ - [ ] ./src/calc.ts#1-1
330
+ new add function[^why]
331
+
332
+ [^why]:
333
+ - H: also export multiply
334
+ """
335
+ And step "build.review.collecting" writes ".gtd/REQUIREMENTS.md" with:
336
+ """
337
+ # Requirements
338
+
339
+ ## Concerns
340
+
341
+ ### Multiply
342
+
343
+ - Export `multiply()`.
344
+ """
345
+ And step "build.review.collecting" writes ".gtd/REVIEW.md" with:
346
+ """
347
+ # Review: abc1234
348
+
349
+ <!-- base: 0000000 -->
350
+
351
+ ## calc
352
+
353
+ - [ ] ./src/calc.ts#1-1
354
+ new add function[^why]
355
+
356
+ [^why]:
357
+ - H: also export multiply
358
+ - A: noted in the requirements.
359
+ """
360
+ When gtd lands
361
+ And the process is driven
362
+ Then the process rests at "build.review.await-review"
363
+ Given a file ".gtd/REVIEW.md" with:
364
+ """
365
+ # Review: abc1234
366
+
367
+ <!-- base: 0000000 -->
368
+
369
+ ## calc
370
+
371
+ - [ ] ./src/calc.ts#1-1
372
+ new add function
373
+ """
374
+ And step "build.review.closing" runs its script
375
+ And step "re-unwind" runs its script
376
+ When gtd lands
377
+ And the process is driven
378
+ Then the process rests at "implement.building"
379
+ And the commit subjects end with:
380
+ """
381
+ gtd(check): re-unwind → design.triage
382
+ gtd(agent): design.triage → design.gate.answer
383
+ gtd(human): design.gate.answer → seam.naive
384
+ gtd(agent): seam.naive → seam.judge
385
+ gtd(judge): seam.judge → implement.building
386
+ gtd(agent): implement.building
387
+ """
@@ -0,0 +1,88 @@
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 { judgeSeam, naivePass } from "./seam.js"
12
+ import { captureStep } from "../shared/text.fixture.js"
13
+
14
+ afterEach(() => installContext(undefined))
15
+
16
+ const change = (path: string, status: Change["status"]): Change => ({
17
+ path,
18
+ status,
19
+ before: status === "added" ? undefined : "a",
20
+ after: status === "deleted" ? undefined : "b",
21
+ })
22
+
23
+ const tip = (...changes: Change[]): KeptTip =>
24
+ ({
25
+ hash: "abc",
26
+ changes: Object.assign([...changes], { paths: [], get: () => undefined }),
27
+ }) as KeptTip
28
+
29
+ describe("judgeSeam", () => {
30
+ const judgeRequests = (kept: KeptTip, vars: Record<string, string> = {}) => {
31
+ const requests: StepRequest[] = []
32
+ installContext({
33
+ ...fixtureContext({ vars }),
34
+ matches: globMatches,
35
+ step: async (request: StepRequest) => {
36
+ requests.push(request)
37
+ return { answers: { shape: { answer: "land", p: 0.9 } }, truncated: [] }
38
+ },
39
+ } as unknown as FlowContext)
40
+ return judgeSeam(kept).then(() => requests)
41
+ }
42
+
43
+ it.each([
44
+ ["src/foo*bar.ts", "src/foobar.ts"],
45
+ ["src/**", "src/deep/foobar.ts"],
46
+ ])("added %s does not cover modified %s", async (added, sibling) => {
47
+ const requests = await judgeRequests(tip(change(added, "added"), change(sibling, "modified")))
48
+ const request = requests[0]
49
+ if (request?.kind !== "judge") throw new Error("expected a judge step")
50
+ expect(JSON.parse(request.evidence.strays as string)).toEqual([sibling])
51
+ expect(JSON.parse(request.evidence.inAllowlist as string)).toContain(added)
52
+ })
53
+
54
+ it("splits seamAllow on commas, trims entries and drops blanks", async () => {
55
+ const requests = await judgeRequests(tip(change("src/x.ts", "modified")), {
56
+ seamAllow: " a/** , ,b.ts,, ",
57
+ })
58
+ const request = requests[0]
59
+ if (request?.kind !== "judge") throw new Error("expected a judge step")
60
+ expect(JSON.parse(request.evidence.allow as string)).toEqual(["a/**", "b.ts"])
61
+ })
62
+
63
+ it("exempts only added paths: a deleted path stays a stray", async () => {
64
+ const requests = await judgeRequests(tip(change("src/gone.ts", "deleted")))
65
+ const request = requests[0]
66
+ if (request?.kind !== "judge") throw new Error("expected a judge step")
67
+ expect(JSON.parse(request.evidence.strays as string)).toEqual(["src/gone.ts"])
68
+ })
69
+ })
70
+
71
+ describe("naivePass", () => {
72
+ const capture = (rejection?: { strays: string[]; p: number }) =>
73
+ captureStep(() => naivePass(rejection), { scope: "seam" })
74
+
75
+ it("keeps its turn under the step name seam.naive", async () => {
76
+ const request = await capture()
77
+ if (request.kind !== "agent") throw new Error("unreachable")
78
+ expect(request.name).toBe("naive")
79
+ expect(request.options).toMatchObject({ keep: true })
80
+ })
81
+
82
+ it("carries the rejection into the retry prompt", async () => {
83
+ const request = await capture({ strays: ["src/core.ts"], p: 0.8 })
84
+ if (request.kind !== "agent") throw new Error("unreachable")
85
+ expect(request.prompt).toContain("src/core.ts")
86
+ expect(request.prompt).toContain("0.8")
87
+ })
88
+ })
@@ -0,0 +1,110 @@
1
+ import {
2
+ changes,
3
+ human,
4
+ judgeDiffShape,
5
+ kept,
6
+ removeScript,
7
+ run,
8
+ scope,
9
+ seamGate,
10
+ vars,
11
+ type DiffShapeResult,
12
+ type KeptTip,
13
+ } from "../../flows/index.js"
14
+ import { healthy } from "../shared/health.js"
15
+ import { coder, FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
16
+ import * as t from "../shared/text.js"
17
+ import { defaults } from "../shared/vars.js"
18
+ import { refactorFirst } from "./refactor.js"
19
+
20
+ /** Naive-pass retries after a `reject`; the next `reject` rests at `seam.steer`. */
21
+ const NAIVE_RETRIES = 2
22
+
23
+ /** A change to an existing file outside the allowlist is a stray even when the task needs it: the judge decides. */
24
+ export const judgeSeam = (tip: KeptTip): Promise<DiffShapeResult> =>
25
+ judgeDiffShape("judge", {
26
+ changes: tip.changes,
27
+ allow: (vars.seamAllow ?? defaults.seamAllow!)
28
+ .split(",")
29
+ .map((glob) => glob.trim())
30
+ .filter((glob) => glob !== ""),
31
+ exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path),
32
+ })
33
+
34
+ export const naivePass = (rejection?: t.SeamRejection): Promise<void> =>
35
+ t.agentWithSkills("naive", t.naivePassPrompt(rejection), {
36
+ label: rejection === undefined ? "Writing the naive pass" : "Rewriting the naive pass",
37
+ keep: true,
38
+ model: coder(),
39
+ system: t.builderSystem(),
40
+ })
41
+
42
+ export const implement = (reference: string): Promise<void> =>
43
+ scope("implement", async () => {
44
+ await t.agentWithSkills("building", t.implementBuildingPrompt(reference), {
45
+ label: "Implementing",
46
+ model: coder(),
47
+ system: t.builderSystem(),
48
+ })
49
+ await healthy(() =>
50
+ t.agentWithSkills("fix.suite.fixing", t.implementFixPrompt(), {
51
+ label: "Fixing the check",
52
+ file: FEEDBACK,
53
+ model: coder(),
54
+ system: t.builderSystem(),
55
+ }),
56
+ )
57
+ await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" })
58
+ })
59
+
60
+ interface Decision {
61
+ readonly reference: string
62
+ /** Present only on `refactor-first`: the refactor runs before the implementation. */
63
+ readonly shape?: DiffShapeResult
64
+ }
65
+
66
+ const decide = (): Promise<Decision> =>
67
+ scope("seam", async () => {
68
+ let rejection: t.SeamRejection | undefined
69
+ let retries = 0
70
+ for (;;) {
71
+ await naivePass(rejection)
72
+ const tip = kept("naive")!
73
+ const shape = await judgeSeam(tip)
74
+ if (shape.choice === "land") return { reference: tip.hash }
75
+ if (shape.choice === "refactor-first") {
76
+ await seamGate("approve", shape)
77
+ return { reference: tip.hash, shape }
78
+ }
79
+ rejection = { strays: shape.strays, p: shape.judgment?.answers.shape?.p ?? 0 }
80
+ if (retries < NAIVE_RETRIES) {
81
+ retries++
82
+ continue
83
+ }
84
+ await human("steer", {
85
+ message: t.seamSteerMessage(rejection),
86
+ label: "Steer or overrule the rejected naive pass",
87
+ file: REQUIREMENTS,
88
+ acceptClean: true,
89
+ })
90
+ // Any edit steers, with a fresh budget and no stale rejection; clean overrules.
91
+ if (changes().length === 0) return { reference: tip.hash }
92
+ rejection = undefined
93
+ retries = 0
94
+ }
95
+ })
96
+
97
+ export const seamBuild = async (): Promise<void> => {
98
+ const { reference, shape } = await decide()
99
+ if (shape !== undefined && !(await refactorFirst(shape, { reference }))) {
100
+ // Unreadable strays were never judged, and no refactor runs: a person reads the diff.
101
+ await scope("seam", () =>
102
+ human("by-hand", {
103
+ message: t.seamByHandMessage(shape.strays, shape.unreadable, reference),
104
+ label: "Reviewing unreadable files by hand",
105
+ acceptClean: true,
106
+ }),
107
+ )
108
+ }
109
+ await implement(reference)
110
+ }
@@ -23,6 +23,7 @@ export * from "./feature/steps.js"
23
23
  export * from "./feature/planning.js"
24
24
  export * from "./feature/packages.js"
25
25
  export * from "./feature/refactor.js"
26
+ export * from "./feature/seam.js"
26
27
  export { buildTail, type ReviewOutcome } from "./feature/review.js"
27
28
  export * from "./feature/feature.js"
28
29
  export { base, review, reviewFlow } from "./review/review.js"
@@ -9,10 +9,21 @@ const { flagShipped } = options
9
9
  describe("the shipped workflow factories", () => {
10
10
  it("take exactly the settings the shipped workflows declare as options", () => {
11
11
  expect(Object.keys(ShippedOptions.fields).sort()).toEqual(
12
- [...Object.keys(defaults), ...Object.keys(envDefaults)].sort(),
12
+ [...Object.keys(defaults), ...Object.keys(envDefaults)]
13
+ .filter((name) => name !== "flag_naive_pass")
14
+ .sort(),
13
15
  )
14
16
  })
15
17
 
18
+ it("declare seamAllow and flag_naive_pass defaults; flag_naive_pass is no option", () => {
19
+ expect(defaults.flag_naive_pass).toBe("0")
20
+ expect(Object.keys(ShippedOptions.fields)).not.toContain("flag_naive_pass")
21
+ expect(configure(feature({ seamAllow: "docs/**" }))).toMatchObject({
22
+ ok: true,
23
+ build: { defaults: { seamAllow: "docs/**", flag_naive_pass: "0" } },
24
+ })
25
+ })
26
+
16
27
  it("lay each option over the shipped setting of its own kind", () => {
17
28
  const result = configure(feature({ qualityReviews: "correctness", testCommand: "npm run ci" }))
18
29
  expect(result).toMatchObject({
@@ -17,6 +17,7 @@ export const ShippedOptions = Schema.Struct({
17
17
  reviewNoteActionable: setting,
18
18
  reviewSeverityMinP: setting,
19
19
  refactorFlagPattern: setting,
20
+ seamAllow: setting,
20
21
  qualityReviews: setting,
21
22
  testCommand: setting,
22
23
  plannerModel: setting,
@@ -29,6 +29,10 @@ describe("the bundled workflow's skills export", () => {
29
29
  "refactor",
30
30
  "refactor.fix.suite",
31
31
  "refactor.health",
32
+ "seam",
33
+ "implement",
34
+ "implement.fix.suite",
35
+ "implement.health",
32
36
  "build.quality.correctness",
33
37
  "build.quality.owasp-security",
34
38
  "build.quality.ponytail-review",
@@ -39,8 +43,18 @@ describe("the bundled workflow's skills export", () => {
39
43
  )
40
44
  })
41
45
 
42
- it("keys no group scope", () => {
43
- for (const group of ["packages.item.fix", "build.fix", "build.review.fix", "build.quality"]) {
46
+ it("keys no group scope or non-agent step", () => {
47
+ for (const group of [
48
+ "packages.item.fix",
49
+ "build.fix",
50
+ "build.review.fix",
51
+ "build.quality",
52
+ "implement.fix",
53
+ "seam.judge",
54
+ "seam.approve",
55
+ "seam.steer",
56
+ "seam.by-hand",
57
+ ]) {
44
58
  expect(bundled).not.toHaveProperty(group)
45
59
  }
46
60
  })
@@ -63,6 +77,10 @@ describe("the bundled workflow's skills export", () => {
63
77
  refactor: ["ponytail"],
64
78
  "refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
65
79
  "refactor.health": ["ponytail", "debugging-and-error-recovery"],
80
+ seam: ["incremental-implementation"],
81
+ implement: ["test-driven-development", "incremental-implementation"],
82
+ "implement.fix.suite": ["debugging-and-error-recovery"],
83
+ "implement.health": ["debugging-and-error-recovery"],
66
84
  })
67
85
  })
68
86
 
@@ -29,6 +29,12 @@ export const skills = (
29
29
  refactor: ["ponytail"],
30
30
  "refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
31
31
  "refactor.health": ["ponytail", "debugging-and-error-recovery"],
32
+ // Declared whether or not `naive_pass` is on, so a config keyed on them keeps
33
+ // loading when the flag flips off.
34
+ seam: ["incremental-implementation"],
35
+ implement: ["test-driven-development", "incremental-implementation"],
36
+ "implement.fix.suite": ["debugging-and-error-recovery"],
37
+ "implement.health": ["debugging-and-error-recovery"],
32
38
  ...lensScopes(
33
39
  "build.quality",
34
40
  lensesOf(vars.qualityReviews ?? ""),