@pmelab/gtd 37.0.0 → 38.1.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 (41) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -0
  3. package/dist/chunks/{reserved-zAfoJtR7.mjs → reserved-CfQ8eurM.mjs} +85 -17
  4. package/dist/chunks/{tail-BRC4DGdV.mjs → tail-C7yaWvaS.mjs} +1 -1
  5. package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
  6. package/dist/gtd.bundle.mjs +1 -1
  7. package/dist/testkit.mjs +1 -1
  8. package/dist/wire.mjs +1 -1
  9. package/dist/workflows/cleanup.mjs +2 -2
  10. package/dist/workflows/feature.mjs +318 -76
  11. package/dist/workflows/fix.mjs +2 -2
  12. package/dist/workflows/review.mjs +2 -2
  13. package/dist/workflows/rollout.mjs +1 -1
  14. package/dist/workflows.mjs +419 -109
  15. package/package.json +1 -1
  16. package/src/testkit/steps.ts +39 -0
  17. package/src/wire/ContextLimit.ts +1 -1
  18. package/src/workflows/feature/continuation-cap.feature +65 -0
  19. package/src/workflows/feature/continuation-rebuild.feature +88 -0
  20. package/src/workflows/feature/continuation-slices.feature +64 -0
  21. package/src/workflows/feature/continuation.feature +205 -0
  22. package/src/workflows/feature/coverage.ts +14 -6
  23. package/src/workflows/feature/criteria.test.ts +17 -1
  24. package/src/workflows/feature/criteria.ts +37 -3
  25. package/src/workflows/feature/feature.ts +64 -15
  26. package/src/workflows/feature/index.ts +1 -0
  27. package/src/workflows/feature/refactor.ts +64 -30
  28. package/src/workflows/feature/seam.ts +157 -48
  29. package/src/workflows/feature/slicing-cap.feature +14 -0
  30. package/src/workflows/feature/slicing.feature +225 -0
  31. package/src/workflows/feature/slicing.test.ts +49 -0
  32. package/src/workflows/feature/slicing.ts +105 -0
  33. package/src/workflows/index.ts +1 -0
  34. package/src/workflows/shared/continuation.ts +92 -0
  35. package/src/workflows/shared/fences.ts +5 -0
  36. package/src/workflows/shared/options.ts +3 -0
  37. package/src/workflows/shared/steps.ts +2 -0
  38. package/src/workflows/shared/text.test.ts +6 -2
  39. package/src/workflows/shared/text.ts +153 -22
  40. package/src/workflows/shared/vars.ts +6 -0
  41. package/src/workflows/shared/workflows.steps.ts +13 -0
@@ -0,0 +1,225 @@
1
+ @inmem
2
+ Feature: a change with more criteria than the cap is built in coarse slices
3
+
4
+ After design, criteria within sliceMaxCriteria are one slice, built as
5
+ before. Above it design.slicing groups them in .gtd/SLICES.md, at most the
6
+ cap per slice, and each slice runs in order — naive pass, implement, quality
7
+ lap — from where the previous one landed, as slices feature-1, feature-2, …
8
+
9
+ Background:
10
+ Given the shipped workflows, as gtd install scaffolds them
11
+ And an environment variable "GTD_QUALITYREVIEWS" set to ""
12
+ And an environment variable "GTD_FLAGMODE" set to "false"
13
+ And an environment variable "GTD_SLICEMAXCRITERIA" set to "2"
14
+ And a file "src/calc.ts" with:
15
+ """
16
+ export const add = (a: number, b: number) => a + b
17
+ """
18
+ And the working tree is committed as "feat: add calculator"
19
+ And a file "TODO.md" with:
20
+ """
21
+ # More arithmetic
22
+
23
+ subtract, multiply and divide.
24
+ """
25
+ And step "unwind" changes nothing
26
+ And step "start-gate.check" changes nothing
27
+ And step "design.gate.answer" changes nothing
28
+ And step "seam.check" changes nothing
29
+ And step "implement.applying" runs its script
30
+ And step "implement.health.check" changes nothing
31
+ And step "implement.closing" runs its script
32
+
33
+ Scenario: criteria within the cap are one unnumbered slice with no slicing turn
34
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
35
+ """
36
+ # Requirements
37
+
38
+ ## Acceptance Criteria
39
+
40
+ - AC-1: subtract(a, b) returns a - b.
41
+ - AC-2: multiply(a, b) returns a * b.
42
+ """
43
+ And step "seam.naive" writes "src/calc.test.ts" with:
44
+ """
45
+ it("[AC-1] subtracts", () => {})
46
+ it("[AC-2] multiplies", () => {})
47
+ """
48
+ And step "seam.naive" is prompted with "Read `.gtd/REQUIREMENTS.md`"
49
+ And judge step "implement.coverage" answers:
50
+ """
51
+ [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
52
+ """
53
+ When the process is driven
54
+ Then the process rests at "idle"
55
+ And the commit subjects end with:
56
+ """
57
+ gtd(human): design.gate.answer → seam.naive
58
+ gtd(agent): seam.naive → seam.check
59
+ gtd(check): seam.check → implement.applying
60
+ gtd(check): implement.applying → implement.health.check
61
+ gtd(check): implement.health.check → implement.coverage
62
+ gtd(judge): implement.coverage → implement.closing
63
+ gtd(check): implement.closing → idle
64
+ """
65
+ And the output lines of gtd "slices" start with:
66
+ """
67
+ feature
68
+ """
69
+ And the output of gtd "slices" does not contain "feature-"
70
+ And no commit body contains "This slice builds"
71
+
72
+ Scenario: criteria above the cap are sliced and built slice by slice
73
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
74
+ """
75
+ # Requirements
76
+
77
+ ## Acceptance Criteria
78
+
79
+ - AC-1: subtract(a, b) returns a - b.
80
+ - AC-2: multiply(a, b) returns a * b.
81
+ - AC-3: divide(a, b) returns a / b.
82
+ """
83
+ And step "design.slicing" is prompted with "at most 2 per slice"
84
+ And step "design.slicing" writes ".gtd/SLICES.md" with:
85
+ """
86
+ # Slices
87
+
88
+ ## Subtract and divide
89
+
90
+ - AC-1
91
+ - AC-3
92
+
93
+ ## Multiply
94
+
95
+ - AC-2
96
+ """
97
+ And step "seam.naive", when prompted with "This slice builds AC-1, AC-3 only", writes "src/sub.test.ts" with:
98
+ """
99
+ it("[AC-1] subtracts", () => {})
100
+ it("[AC-3] divides", () => {})
101
+ """
102
+ And step "seam.naive", when prompted with "This slice builds AC-2 only", writes "src/mul.test.ts" with:
103
+ """
104
+ it("[AC-2] multiplies", () => {})
105
+ """
106
+ And judge step "implement.coverage", when its questions contain "AC-3", answers:
107
+ """
108
+ [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-3","answer":"yes","p":0.9}]
109
+ """
110
+ And judge step "implement.coverage", when its questions contain "AC-2", answers:
111
+ """
112
+ [{"id":"AC-2","answer":"yes","p":0.9}]
113
+ """
114
+ When the process is driven
115
+ Then the process rests at "idle"
116
+ And the commit subjects end with:
117
+ """
118
+ gtd(agent): design.triage → design.gate.answer
119
+ gtd(human): design.gate.answer → design.slicing
120
+ gtd(agent): design.slicing → seam.naive
121
+ gtd(agent): seam.naive → seam.check
122
+ gtd(check): seam.check → implement.applying
123
+ gtd(check): implement.applying → implement.health.check
124
+ gtd(check): implement.health.check → implement.coverage
125
+ gtd(judge): implement.coverage → implement.closing
126
+ gtd(check): implement.closing → seam.naive
127
+ gtd(agent): seam.naive → seam.check
128
+ gtd(check): seam.check → implement.applying
129
+ gtd(check): implement.applying → implement.health.check
130
+ gtd(check): implement.health.check → implement.coverage
131
+ gtd(judge): implement.coverage → implement.closing
132
+ gtd(check): implement.closing → idle
133
+ """
134
+ And the output lines of gtd "slices" start with:
135
+ """
136
+ feature-1
137
+ feature-2
138
+ """
139
+ And slice "feature-2" begins where slice "feature-1" ends
140
+ And "src/sub.test.ts" contains "[AC-3] divides"
141
+ And "src/mul.test.ts" contains "[AC-2] multiplies"
142
+ And ".gtd/REQUIREMENTS.md" does not exist
143
+ And ".gtd/SLICES.md" does not exist
144
+
145
+ Scenario: a slice over the cap is refused
146
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
147
+ """
148
+ # Requirements
149
+
150
+ ## Acceptance Criteria
151
+
152
+ - AC-1: subtract(a, b) returns a - b.
153
+ - AC-2: multiply(a, b) returns a * b.
154
+ - AC-3: divide(a, b) returns a / b.
155
+ """
156
+ And step "design.slicing" writes ".gtd/SLICES.md" with:
157
+ """
158
+ # Slices
159
+
160
+ ## Everything
161
+
162
+ - AC-1
163
+ - AC-2
164
+ - AC-3
165
+ """
166
+ Then driving the process is refused with "more than 2 criteria in: Everything"
167
+
168
+ Scenario: a criterion in no slice is refused
169
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
170
+ """
171
+ # Requirements
172
+
173
+ ## Acceptance Criteria
174
+
175
+ - AC-1: subtract(a, b) returns a - b.
176
+ - AC-2: multiply(a, b) returns a * b.
177
+ - AC-3: divide(a, b) returns a / b.
178
+ """
179
+ And step "design.slicing" writes ".gtd/SLICES.md" with:
180
+ """
181
+ # Slices
182
+
183
+ ## Subtract
184
+
185
+ - AC-1
186
+
187
+ ## Multiply
188
+
189
+ - AC-2
190
+ """
191
+ Then driving the process is refused with "no slice for: AC-3"
192
+
193
+ Scenario: a slicing turn may not change the acceptance criteria
194
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
195
+ """
196
+ # Requirements
197
+
198
+ ## Acceptance Criteria
199
+
200
+ - AC-1: subtract(a, b) returns a - b.
201
+ - AC-2: multiply(a, b) returns a * b.
202
+ - AC-3: divide(a, b) returns a / b.
203
+ """
204
+ And step "design.slicing" writes ".gtd/REQUIREMENTS.md" with:
205
+ """
206
+ # Requirements
207
+
208
+ ## Acceptance Criteria
209
+
210
+ - AC-1: subtract(a, b) returns a - b.
211
+ - AC-2: multiply(a, b) returns a * b.
212
+ """
213
+ And step "design.slicing" writes ".gtd/SLICES.md" with:
214
+ """
215
+ # Slices
216
+
217
+ ## Subtract
218
+
219
+ - AC-1
220
+
221
+ ## Multiply
222
+
223
+ - AC-2
224
+ """
225
+ Then driving the process is refused with "wrote outside its write access"
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { sliceCriteria, slicesOf } from "./slicing.js"
3
+
4
+ describe("slicesOf", () => {
5
+ it("reads each level-2 heading's ID'd items, in order", () => {
6
+ expect(
7
+ slicesOf("# Slices\n\n## Parse\n\n- AC-1: parses\n- AC-3\n\n## Print\n\n- AC-2 prints\n"),
8
+ ).toEqual([
9
+ { title: "Parse", ids: ["AC-1", "AC-3"] },
10
+ { title: "Print", ids: ["AC-2"] },
11
+ ])
12
+ })
13
+
14
+ it("keeps an item not led by an ID as an empty one, and skips fenced lines", () => {
15
+ expect(slicesOf("## One\n\n```md\n## Two\n- AC-9\n```\n- AC-10\n- see AC-1\n")).toEqual([
16
+ { title: "One", ids: ["AC-10", ""] },
17
+ ])
18
+ })
19
+
20
+ it("ends a slice at any other heading", () => {
21
+ expect(
22
+ slicesOf("## One\n\n- AC-1\n\n### Why\n\n- shares the lexer\n\n# Later\n\n- AC-5\n"),
23
+ ).toEqual([{ title: "One", ids: ["AC-1"] }])
24
+ })
25
+
26
+ it("reads no slice from a file without level-2 headings", () => {
27
+ expect(slicesOf("# Slices\n\n- AC-1\n")).toEqual([])
28
+ })
29
+ })
30
+
31
+ describe("sliceCriteria", () => {
32
+ const slices = [
33
+ { title: "A", ids: ["AC-1", "AC-3"] },
34
+ { title: "B", ids: ["AC-2"] },
35
+ { title: "C", ids: ["AC-4"] },
36
+ ]
37
+
38
+ it("keeps an earlier slice to its own IDs", () => {
39
+ expect(sliceCriteria(slices, 1)).toEqual({ only: ["AC-2"] })
40
+ })
41
+
42
+ it("gives the last slice every ID no earlier slice builds", () => {
43
+ expect(sliceCriteria(slices, 2)).toEqual({ except: ["AC-1", "AC-3", "AC-2"] })
44
+ })
45
+
46
+ it("leaves a single slice unsliced", () => {
47
+ expect(sliceCriteria([slices[0]!], 0)).toBeUndefined()
48
+ })
49
+ })
@@ -0,0 +1,105 @@
1
+ import { read, refuse, scope, vars } from "../../flows/index.js"
2
+ import { HEADING, ITEM, markFenced } from "../shared/fences.js"
3
+ import { planner, REQUIREMENTS, SLICES } from "../shared/steps.js"
4
+ import * as t from "../shared/text.js"
5
+ import { defaults } from "../shared/vars.js"
6
+ import { criterionIds, type CriteriaSlice } from "./criteria.js"
7
+
8
+ /** One slice of the change: the criterion IDs it builds, in the order `.gtd/SLICES.md` lists them. */
9
+ export interface Slice {
10
+ readonly title: string
11
+ readonly ids: readonly string[]
12
+ }
13
+
14
+ const ID = /^(AC-\d+)(?![\w-])/
15
+
16
+ /** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
17
+ export const sliceCap = (): number => {
18
+ const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria!
19
+ if (!/^[1-9]\d*$/.test(value))
20
+ refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`)
21
+ return Number(value)
22
+ }
23
+
24
+ /** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
25
+ export const slicesOf = (text: string): readonly Slice[] => {
26
+ const slices: { title: string; ids: string[] }[] = []
27
+ // Only items right under a `##` heading: another heading ends the slice.
28
+ let open = false
29
+ for (const { line, fenced } of markFenced(text)) {
30
+ if (fenced) continue
31
+ const heading = HEADING.exec(line)
32
+ if (heading !== null) {
33
+ open = heading[1] === "##"
34
+ if (open) slices.push({ title: heading[2]!, ids: [] })
35
+ continue
36
+ }
37
+ const item = ITEM.exec(line)
38
+ if (item !== null && open) slices.at(-1)!.ids.push(ID.exec(item[1]!)?.[1] ?? "")
39
+ }
40
+ return slices
41
+ }
42
+
43
+ const twice = (list: readonly string[]): string[] => [
44
+ ...new Set(list.filter((id, i) => list.indexOf(id) !== i)),
45
+ ]
46
+
47
+ /** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
48
+ const sliceFault = (
49
+ slices: readonly Slice[],
50
+ ids: readonly string[],
51
+ cap: number,
52
+ ): string | undefined => {
53
+ const listed = slices.flatMap((s) => s.ids)
54
+ if (slices.length === 0) return `no "## " slice`
55
+ const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title)
56
+ if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`
57
+ if (listed.includes("")) return "a list item not led by a criterion ID"
58
+ const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title)
59
+ if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`
60
+ const repeated = twice(listed)
61
+ if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`
62
+ const unknown = listed.filter((id) => !ids.includes(id))
63
+ if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`
64
+ const missing = ids.filter((id) => !listed.includes(id))
65
+ if (missing.length > 0) return `no slice for: ${missing.join(", ")}`
66
+ return undefined
67
+ }
68
+
69
+ /** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
70
+ export const requireSlices = (ids: readonly string[], cap: number): readonly Slice[] => {
71
+ const slices = slicesOf(read(SLICES) ?? "")
72
+ const why = sliceFault(slices, ids, cap)
73
+ if (why !== undefined)
74
+ refuse(
75
+ `gtd land: slices: ${SLICES} has ${why} — list each slice as a "## <title>" heading over "- AC-<n>" items, every criterion of ${REQUIREMENTS} in exactly one slice, at most ${cap} per slice`,
76
+ )
77
+ return slices
78
+ }
79
+
80
+ /**
81
+ * The slices to build, in order. Criteria within `sliceMaxCriteria` are one
82
+ * slice and no turn; above it `design.slicing` groups them.
83
+ */
84
+ export const slicing = async (): Promise<readonly Slice[]> => {
85
+ const all = criterionIds(read(REQUIREMENTS) ?? "")
86
+ const cap = sliceCap()
87
+ if (all.length <= cap) return [{ title: "", ids: all }]
88
+ return scope("design", async () => {
89
+ await t.agentWithSkills("slicing", t.slicingPrompt(cap), {
90
+ label: "Slicing the change",
91
+ file: SLICES,
92
+ model: planner(),
93
+ system: t.designSystem(),
94
+ })
95
+ return requireSlices(all, cap)
96
+ })
97
+ }
98
+
99
+ /** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
100
+ export const sliceCriteria = (slices: readonly Slice[], i: number): CriteriaSlice | undefined =>
101
+ slices.length === 1
102
+ ? undefined
103
+ : i < slices.length - 1
104
+ ? { only: slices[i]!.ids }
105
+ : { except: slices.slice(0, -1).flatMap((s) => s.ids) }
@@ -25,6 +25,7 @@ export * from "./feature/criteria.js"
25
25
  export * from "./feature/planning.js"
26
26
  export * from "./feature/refactor.js"
27
27
  export * from "./feature/seam.js"
28
+ export * from "./feature/slicing.js"
28
29
  export {
29
30
  buildTail,
30
31
  type BuildTailOptions,
@@ -0,0 +1,92 @@
1
+ import { human, refuse, scope, vars } from "../../flows/index.js"
2
+ import * as t from "./text.js"
3
+ import type { Handoff } from "./text.js"
4
+ import { defaults } from "./vars.js"
5
+
6
+ export type { Handoff }
7
+
8
+ export interface Turned<T> {
9
+ readonly result: T
10
+ /** Set when the turn wrote a handoff note, i.e. stopped at its context limit. */
11
+ readonly handoff?: Handoff | undefined
12
+ }
13
+
14
+ /** Runs a turn's agent step in a conversation of its own, or in the caller's. */
15
+ export type Fresh = <T>(fn: () => Promise<T>) => Promise<T>
16
+
17
+ /**
18
+ * One phase's continuations, shared by every `continuing()` call in it (a naive
19
+ * retry, a refactor lap) so the caps hold per phase. `spent`: once a turn hit
20
+ * its context limit, resuming that conversation would only hit it again.
21
+ */
22
+ export interface Phase {
23
+ readonly name: string
24
+ spent: boolean
25
+ count: number
26
+ stalled: number
27
+ /** Run after a landing at `oversized`, where a person may narrow the requirements; absent, the gate does not suggest it. */
28
+ readonly steered?: (() => void) | undefined
29
+ }
30
+
31
+ export const phase = (name: string, steered?: () => void): Phase => ({
32
+ name,
33
+ spent: false,
34
+ count: 0,
35
+ stalled: 0,
36
+ steered,
37
+ })
38
+
39
+ const own: Fresh = (fn) => fn()
40
+ // Every `scope()` call is a new conversation; the names keep continuations
41
+ // apart in history from later turns of a phase that merely start afresh.
42
+ const continued: Fresh = (fn) => scope("continue", fn)
43
+ const afresh: Fresh = (fn) => scope("fresh", fn)
44
+
45
+ const setting = (name: "continuations" | "continuationStall", least: number): number => {
46
+ const raw = vars[name] ?? defaults[name]!
47
+ const n = Number(raw)
48
+ if (raw.trim() === "" || !Number.isInteger(n) || n < least)
49
+ refuse(`gtd: ${name} must be a whole number of at least ${least}, got ${JSON.stringify(raw)}`)
50
+ return n
51
+ }
52
+
53
+ /**
54
+ * Run `turn` until one ends without a handoff note: each stopped turn is picked
55
+ * up by a continuation in a fresh conversation. Past `continuations` of them in
56
+ * the phase, or after `continuationStall` in a row that close nothing still
57
+ * open, the phase rests at `oversized` for a person; landing there grants a new round.
58
+ */
59
+ export const continuing = async <T>(
60
+ phase: Phase,
61
+ turn: (from: Handoff | undefined, fresh: Fresh) => Promise<Turned<T>>,
62
+ ): Promise<T> => {
63
+ const cap = setting("continuations", 0)
64
+ const stall = setting("continuationStall", 1)
65
+ let last = await turn(undefined, phase.spent ? afresh : own)
66
+ while (last.handoff !== undefined) {
67
+ phase.spent = true
68
+ const from = last.handoff
69
+ if (phase.count >= cap || phase.stalled >= stall) {
70
+ await human("oversized", {
71
+ message: t.oversizedMessage(
72
+ phase.name,
73
+ from,
74
+ phase.count >= cap ? { cap } : { stall },
75
+ phase.steered !== undefined,
76
+ ),
77
+ label: "Too big for its context",
78
+ acceptClean: true,
79
+ })
80
+ phase.steered?.()
81
+ phase.count = 0
82
+ phase.stalled = 0
83
+ }
84
+ last = await turn(from, continued)
85
+ phase.count++
86
+ // Nothing countable left open is no stall: only the cap stops such a phase.
87
+ const closed =
88
+ from.open.length === 0 || from.open.some((item) => !(last.handoff?.open ?? []).includes(item))
89
+ phase.stalled = closed ? 0 : phase.stalled + 1
90
+ }
91
+ return last.result
92
+ }
@@ -17,3 +17,8 @@ export const markFenced = (text: string): readonly { line: string; fenced: boole
17
17
  return { line, fenced }
18
18
  })
19
19
  }
20
+
21
+ /** A markdown heading: its `#` run, then its text. */
22
+ export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/
23
+ /** A list item, its checkbox dropped: its text. */
24
+ export const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/
@@ -21,8 +21,11 @@ export const ShippedOptions = Schema.Struct({
21
21
  reviewSeverityMinP: setting,
22
22
  refactorFlagPattern: setting,
23
23
  seamAllow: setting,
24
+ sliceMaxCriteria: setting,
24
25
  qualityReviews: setting,
25
26
  refactorQualityReviews: setting,
27
+ continuations: setting,
28
+ continuationStall: setting,
26
29
  testCommand: setting,
27
30
  plannerModel: setting,
28
31
  coderModel: setting,
@@ -14,6 +14,8 @@ export const REVIEW = ".gtd/REVIEW.md"
14
14
  export const REFACTOR = ".gtd/REFACTOR.md"
15
15
  export const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md"
16
16
  export const QUALITY = ".gtd/QUALITY.md"
17
+ export const SLICES = ".gtd/SLICES.md"
18
+ export const HANDOFF = ".gtd/HANDOFF.md"
17
19
 
18
20
  export const planner = (): string => env.plannerModel ?? ""
19
21
  export const coder = (): string => env.coderModel ?? ""
@@ -976,7 +976,9 @@ describe("the seam phase's texts", () => {
976
976
  it("has a red retry pick up the kept attempt instead of starting over", () => {
977
977
  const prompt = renderText(() => naivePassPrompt({ red: "abc123" }))
978
978
  expect(prompt).toContain("left the suite red")
979
- expect(prompt).toContain("git diff --binary abc123^ abc123 | git apply")
979
+ expect(prompt).toContain(
980
+ "git diff --binary --no-color --no-ext-diff --no-textconv abc123^ abc123 | git apply",
981
+ )
980
982
  expect(prompt).toContain(".gtd/FEEDBACK.md")
981
983
  expect(prompt).not.toContain("rejected")
982
984
  })
@@ -1042,7 +1044,9 @@ describe("the seam phase's texts", () => {
1042
1044
  }
1043
1045
  const prompt = renderText(() => naivePassPrompt(fault))
1044
1046
  expect(prompt).toContain("names AC-2")
1045
- expect(prompt).toContain("git diff --binary abc123^ abc123 | git apply")
1047
+ expect(prompt).toContain(
1048
+ "git diff --binary --no-color --no-ext-diff --no-textconv abc123^ abc123 | git apply",
1049
+ )
1046
1050
  const message = renderText(() => seamSteerMessage(fault))
1047
1051
  expect(message).toContain("names AC-2")
1048
1052
  expect(message).toContain("`seam.check` runs its suite")