@pmelab/gtd 36.0.0 → 38.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 (62) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +30 -9
  3. package/claude/hooks/context.ts +66 -0
  4. package/claude/hooks/drive.ts +3 -0
  5. package/claude/hooks/register.tsx +69 -6
  6. package/dist/chunks/{reserved-BAv71I4i.mjs → reserved-sfMdy7v7.mjs} +83 -22
  7. package/dist/chunks/{tail-kar1zhOb.mjs → tail-Cgca5Gp-.mjs} +1 -1
  8. package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
  9. package/dist/gtd.bundle.mjs +498 -56
  10. package/dist/testkit.mjs +115 -3
  11. package/dist/wire.mjs +2 -2
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +277 -58
  14. package/dist/workflows/fix.mjs +2 -2
  15. package/dist/workflows/review.mjs +2 -2
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +355 -75
  18. package/package.json +1 -1
  19. package/src/cli/Cli.test.ts +10 -3
  20. package/src/cli/Cli.ts +76 -1
  21. package/src/cli/Program.test.ts +1 -0
  22. package/src/cli/Program.ts +28 -0
  23. package/src/cli/ToolCommands.ts +3 -0
  24. package/src/edge/Edge.ts +11 -0
  25. package/src/flows/runtime.ts +11 -0
  26. package/src/replay/Replay.ts +51 -2
  27. package/src/testkit/Layers.ts +1 -0
  28. package/src/testkit/steps.ts +35 -0
  29. package/src/wire/BeatDocument.ts +12 -2
  30. package/src/wire/BeatStatus.ts +3 -0
  31. package/src/wire/ContextLimit.fixture.ts +30 -0
  32. package/src/wire/ContextLimit.test.ts +96 -0
  33. package/src/wire/ContextLimit.ts +169 -0
  34. package/src/wire/index.ts +1 -0
  35. package/src/wire/types.ts +7 -0
  36. package/src/workflow/Workflow.ts +9 -1
  37. package/src/workflows/feature/context-limit.feature +37 -0
  38. package/src/workflows/feature/coverage.ts +22 -8
  39. package/src/workflows/feature/criteria.test.ts +17 -1
  40. package/src/workflows/feature/criteria.ts +26 -3
  41. package/src/workflows/feature/feature.ts +69 -16
  42. package/src/workflows/feature/index.ts +1 -0
  43. package/src/workflows/feature/refactor-absorbs.feature +12 -5
  44. package/src/workflows/feature/refactor-first.feature +11 -2
  45. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  46. package/src/workflows/feature/refactor-rebuild.feature +269 -0
  47. package/src/workflows/feature/refactor-review.feature +45 -1
  48. package/src/workflows/feature/refactor.ts +9 -3
  49. package/src/workflows/feature/seam.test.ts +108 -3
  50. package/src/workflows/feature/seam.ts +169 -31
  51. package/src/workflows/feature/slicing-cap.feature +14 -0
  52. package/src/workflows/feature/slicing.feature +225 -0
  53. package/src/workflows/feature/slicing.test.ts +49 -0
  54. package/src/workflows/feature/slicing.ts +105 -0
  55. package/src/workflows/index.ts +1 -0
  56. package/src/workflows/shared/fences.ts +5 -0
  57. package/src/workflows/shared/options.test.ts +11 -2
  58. package/src/workflows/shared/options.ts +19 -4
  59. package/src/workflows/shared/steps.ts +2 -0
  60. package/src/workflows/shared/text.ts +129 -28
  61. package/src/workflows/shared/vars.ts +1 -0
  62. 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,
@@ -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+)?(.*)$/
@@ -8,8 +8,9 @@ const { defaults, envDefaults, feature, fix, review, ShippedOptions } = workflow
8
8
  const { flagShipped } = options
9
9
 
10
10
  describe("the shipped workflow factories", () => {
11
- it("take exactly the settings the shipped workflows declare as options", () => {
12
- expect(Object.keys(ShippedOptions.fields).sort()).toEqual(
11
+ it("take exactly the settings the shipped workflows declare as options, and contextLimit", () => {
12
+ const settings = Object.keys(ShippedOptions.fields).filter((name) => name !== "contextLimit")
13
+ expect(settings.sort()).toEqual(
13
14
  [...Object.keys(defaults), ...Object.keys(envDefaults)]
14
15
  .filter((name) => !name.startsWith("flag_"))
15
16
  .sort(),
@@ -23,6 +24,14 @@ describe("the shipped workflow factories", () => {
23
24
  expect(defaults).toHaveProperty(`flag_${name}`, defaultOn ? "1" : "0")
24
25
  })
25
26
 
27
+ it("keep contextLimit out of the settings", () => {
28
+ const built = configure(feature({ contextLimit: { hard: 0.8 } }))
29
+ expect(built).toMatchObject({ ok: true })
30
+ const build = built?.ok === true ? built.build : undefined
31
+ expect(build?.defaults).not.toHaveProperty("contextLimit")
32
+ expect(build?.envDefaults).not.toHaveProperty("contextLimit")
33
+ })
34
+
26
35
  it("declare a seamAllow default a factory option overrides", () => {
27
36
  expect(configure(feature({ seamAllow: "docs/**" }))).toMatchObject({
28
37
  ok: true,
@@ -6,6 +6,7 @@ import {
6
6
  type WorkflowDoors,
7
7
  type WorkflowFactory,
8
8
  refuse,
9
+ scope,
9
10
  vars,
10
11
  } from "../../flows/index.js"
11
12
  import { defaultFlagRegistry, defaults, envDefaults } from "./vars.js"
@@ -20,12 +21,20 @@ export const ShippedOptions = Schema.Struct({
20
21
  reviewSeverityMinP: setting,
21
22
  refactorFlagPattern: setting,
22
23
  seamAllow: setting,
24
+ sliceMaxCriteria: setting,
23
25
  qualityReviews: setting,
24
26
  refactorQualityReviews: setting,
25
27
  testCommand: setting,
26
28
  plannerModel: setting,
27
29
  coderModel: setting,
28
30
  flagMode: Schema.optionalWith(Schema.Boolean, { exact: true }),
31
+ contextLimit: Schema.optionalWith(
32
+ Schema.Struct({
33
+ soft: Schema.optionalWith(Schema.Number, { exact: true }),
34
+ hard: Schema.optionalWith(Schema.Number, { exact: true }),
35
+ }),
36
+ { exact: true },
37
+ ),
29
38
  })
30
39
 
31
40
  export type ShippedOptions = Schema.Schema.Encoded<typeof ShippedOptions>
@@ -50,6 +59,12 @@ export const flagModeOn = (): boolean => {
50
59
  return value === "true"
51
60
  }
52
61
 
62
+ // Not a setting: a factory option is known at load, so the scope wraps the
63
+ // whole flow without reading a setting before its first step. The engine
64
+ // refuses limits it cannot use when it reaches an agent step.
65
+ const limited = (flow: Flow, context: ShippedOptions["contextLimit"]): Flow =>
66
+ context === undefined ? flow : () => scope({ context }, flow)
67
+
53
68
  /** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
54
69
  export const shipped = (
55
70
  flow: Flow,
@@ -58,8 +73,8 @@ export const shipped = (
58
73
  ): WorkflowFactory<ShippedOptions> =>
59
74
  defineWorkflow(
60
75
  Schema.standardSchemaV1(ShippedOptions, { onExcessProperty: "error", errors: "all" }),
61
- (options) => ({
62
- flow,
76
+ ({ contextLimit, ...options }) => ({
77
+ flow: limited(flow, contextLimit),
63
78
  defaults: over(defaults, options),
64
79
  envDefaults: over(envDefaults, options),
65
80
  doors,
@@ -77,10 +92,10 @@ export const flagShipped = (
77
92
  ): WorkflowFactory<FlagOptions> =>
78
93
  defineWorkflow(
79
94
  Schema.standardSchemaV1(FlagOptions, { onExcessProperty: "error", errors: "all" }),
80
- (options) => {
95
+ ({ contextLimit, ...options }) => {
81
96
  const flagDefaults = { ...defaults, flag: "", flagRegistry: defaultFlagRegistry }
82
97
  return {
83
- flow,
98
+ flow: limited(flow, contextLimit),
84
99
  defaults: over(flagDefaults, options),
85
100
  envDefaults: over(envDefaults, options),
86
101
  doors: { [name]: { args: [{ name: "flag" }], vars: ({ flag }) => ({ flag: flag ?? "" }) } },
@@ -12,7 +12,9 @@ export const ESCALATION = ".gtd/ESCALATION.md"
12
12
  export const REQUIREMENTS = ".gtd/REQUIREMENTS.md"
13
13
  export const REVIEW = ".gtd/REVIEW.md"
14
14
  export const REFACTOR = ".gtd/REFACTOR.md"
15
+ export const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md"
15
16
  export const QUALITY = ".gtd/QUALITY.md"
17
+ export const SLICES = ".gtd/SLICES.md"
16
18
 
17
19
  export const planner = (): string => env.plannerModel ?? ""
18
20
  export const coder = (): string => env.coderModel ?? ""