@pmelab/gtd 52.1.0 → 53.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 (90) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +91 -129
  3. package/claude/hooks/drive.ts +0 -3
  4. package/claude/hooks/register.tsx +3 -68
  5. package/claude/hooks/ship.ts +85 -1595
  6. package/dist/chunks/{reserved-DxB5CROj.mjs → reserved-CQ7amcKG.mjs} +45 -132
  7. package/dist/chunks/{review-CKMSdvgW.mjs → review-CNgN_V8T.mjs} +2 -2
  8. package/dist/chunks/{tail-QZc7Uz2U.mjs → tail-JZ9PAOwp.mjs} +2 -14
  9. package/dist/chunks/{wire-BJuZWR3C.mjs → wire-C12dahrH.mjs} +2 -130
  10. package/dist/flows.mjs +1 -16
  11. package/dist/gtd.bundle.mjs +476 -982
  12. package/dist/testkit.mjs +58 -417
  13. package/dist/wire.mjs +2 -2
  14. package/dist/workflows/cleanup.mjs +2 -2
  15. package/dist/workflows/feature.mjs +112 -363
  16. package/dist/workflows/fix.mjs +2 -2
  17. package/dist/workflows/review.mjs +3 -3
  18. package/dist/workflows/rollout.mjs +1 -1
  19. package/dist/workflows.mjs +311 -674
  20. package/package.json +1 -1
  21. package/src/cli/Cli.test.ts +1 -86
  22. package/src/cli/Cli.ts +13 -133
  23. package/src/cli/Metrics.test.ts +21 -31
  24. package/src/cli/Metrics.ts +25 -36
  25. package/src/cli/Program.test.ts +1 -35
  26. package/src/cli/Program.ts +5 -64
  27. package/src/cli/ToolCommands.ts +0 -3
  28. package/src/edge/Edge.test.ts +20 -322
  29. package/src/edge/Edge.ts +10 -181
  30. package/src/edge/index.ts +0 -1
  31. package/src/flows/runtime.test.ts +0 -27
  32. package/src/flows/runtime.ts +1 -49
  33. package/src/install/Install.ts +15 -24
  34. package/src/replay/Replay.test.ts +0 -169
  35. package/src/replay/Replay.ts +4 -93
  36. package/src/replay/index.ts +0 -1
  37. package/src/testkit/Layers.ts +0 -1
  38. package/src/wire/BeatDocument.ts +2 -12
  39. package/src/wire/BeatStatus.ts +0 -3
  40. package/src/wire/WireSchema.test.ts +0 -18
  41. package/src/wire/index.ts +0 -2
  42. package/src/wire/types.ts +0 -7
  43. package/src/workflow/Workflow.ts +1 -9
  44. package/src/workflows/feature/criteria.test.ts +1 -14
  45. package/src/workflows/feature/criteria.ts +6 -38
  46. package/src/workflows/feature/feature.ts +34 -118
  47. package/src/workflows/feature/hands-on-skip.feature +2 -70
  48. package/src/workflows/feature/index.ts +0 -1
  49. package/src/workflows/feature/one-step-land.feature +0 -4
  50. package/src/workflows/feature/prototype-rounds.feature +1 -1
  51. package/src/workflows/feature/prototype.feature +3 -3
  52. package/src/workflows/feature/prototype.ts +18 -29
  53. package/src/workflows/feature/refactor-absorbs.feature +3 -8
  54. package/src/workflows/feature/refactor-first.feature +0 -5
  55. package/src/workflows/feature/refactor-rebuild.feature +0 -6
  56. package/src/workflows/feature/refactor-review.feature +11 -11
  57. package/src/workflows/feature/refactor.ts +30 -64
  58. package/src/workflows/feature/review-gate.feature +1 -1
  59. package/src/workflows/feature/seam-rejection.feature +0 -4
  60. package/src/workflows/feature/seam.test.ts +7 -26
  61. package/src/workflows/feature/seam.ts +52 -165
  62. package/src/workflows/index.ts +0 -1
  63. package/src/workflows/shared/lenses.ts +0 -13
  64. package/src/workflows/shared/options.test.ts +2 -11
  65. package/src/workflows/shared/options.ts +4 -21
  66. package/src/workflows/shared/review.test.ts +3 -3
  67. package/src/workflows/shared/steps.ts +0 -2
  68. package/src/workflows/shared/text.test.ts +10 -80
  69. package/src/workflows/shared/text.ts +35 -211
  70. package/src/workflows/shared/vars.ts +2 -8
  71. package/src/workflows/shared/workflows.steps.ts +0 -13
  72. package/claude/hooks/context.ts +0 -66
  73. package/src/replay/Slices.test.ts +0 -73
  74. package/src/replay/Slices.ts +0 -45
  75. package/src/wire/ContextLimit.fixture.ts +0 -30
  76. package/src/wire/ContextLimit.test.ts +0 -96
  77. package/src/wire/ContextLimit.ts +0 -169
  78. package/src/wire/Slices.ts +0 -25
  79. package/src/wire/golden/1/slices.json +0 -11
  80. package/src/workflows/feature/context-limit.feature +0 -37
  81. package/src/workflows/feature/continuation-cap.feature +0 -69
  82. package/src/workflows/feature/continuation-rebuild.feature +0 -92
  83. package/src/workflows/feature/continuation-slices.feature +0 -68
  84. package/src/workflows/feature/continuation.feature +0 -209
  85. package/src/workflows/feature/slice-findings.feature +0 -256
  86. package/src/workflows/feature/slicing-cap.feature +0 -23
  87. package/src/workflows/feature/slicing.feature +0 -297
  88. package/src/workflows/feature/slicing.test.ts +0 -49
  89. package/src/workflows/feature/slicing.ts +0 -105
  90. package/src/workflows/shared/continuation.ts +0 -92
@@ -1,297 +0,0 @@
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_PROTOTYPEROUNDS" set to "0"
12
- And an environment variable "GTD_QUALITYREVIEWS" set to ""
13
- And an environment variable "GTD_FLAGMODE" set to "false"
14
- And an environment variable "GTD_SLICEMAXCRITERIA" set to "2"
15
- And a file "src/calc.ts" with:
16
- """
17
- export const add = (a: number, b: number) => a + b
18
- """
19
- And the working tree is committed as "feat: add calculator"
20
- And a file "TODO.md" with:
21
- """
22
- # More arithmetic
23
-
24
- subtract, multiply and divide.
25
- """
26
- And step "unwind" changes nothing
27
- And step "start-gate.check" changes nothing
28
- And step "design.gate.answer" changes nothing
29
- And step "seam.check" changes nothing
30
- And step "implement.applying" runs its script
31
- And step "implement.health.check" changes nothing
32
- And step "implement.closing" runs its script
33
-
34
- Scenario: criteria within the cap are one unnumbered slice with no slicing turn
35
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
36
- """
37
- # Requirements
38
-
39
- ## Impact
40
-
41
- - the greeting module
42
-
43
- ## Acceptance Criteria
44
-
45
- - AC-1: subtract(a, b) returns a - b.
46
- - AC-2: multiply(a, b) returns a * b.
47
- """
48
- And step "seam.naive" writes "src/calc.test.ts" with:
49
- """
50
- it("[AC-1] subtracts", () => {})
51
- it("[AC-2] multiplies", () => {})
52
- """
53
- And step "seam.naive" is prompted with "Read `.gtd/REQUIREMENTS.md`"
54
- When the process is driven
55
- Then the process rests at "build.review.reviewing"
56
- And the commit subjects end with:
57
- """
58
- gtd(human): design.gate.answer → seam.naive
59
- gtd(agent): seam.naive → seam.check
60
- gtd(check): seam.check → implement.applying
61
- gtd(check): implement.applying → implement.health.check
62
- gtd(check): implement.health.check → implement.closing
63
- gtd(check): implement.closing → build.review.reviewing
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
- ## Impact
78
-
79
- - the greeting module
80
-
81
- ## Acceptance Criteria
82
-
83
- - AC-1: subtract(a, b) returns a - b.
84
- - AC-2: multiply(a, b) returns a * b.
85
- - AC-3: divide(a, b) returns a / b.
86
- """
87
- And step "design.slicing" is prompted with "at most 2 per slice"
88
- And step "design.slicing" writes ".gtd/SLICES.md" with:
89
- """
90
- # Slices
91
-
92
- ## Subtract and divide
93
-
94
- - AC-1
95
- - AC-3
96
-
97
- ## Multiply
98
-
99
- - AC-2
100
- """
101
- And step "seam.naive", when prompted with "This slice builds AC-1, AC-3 only", writes "src/sub.test.ts" with:
102
- """
103
- it("[AC-1] subtracts", () => {})
104
- it("[AC-3] divides", () => {})
105
- """
106
- And step "seam.naive", when prompted with "This slice builds AC-2 only", writes "src/mul.test.ts" with:
107
- """
108
- it("[AC-2] multiplies", () => {})
109
- """
110
- When the process is driven
111
- Then the process rests at "build.review.reviewing"
112
- And the commit subjects end with:
113
- """
114
- gtd(agent): design.triage → design.gate.answer
115
- gtd(human): design.gate.answer → design.slicing
116
- gtd(agent): design.slicing → seam.naive
117
- gtd(agent): seam.naive → seam.check
118
- gtd(check): seam.check → implement.applying
119
- gtd(check): implement.applying → implement.health.check
120
- gtd(check): implement.health.check → implement.closing
121
- gtd(check): implement.closing → seam.naive
122
- gtd(agent): seam.naive → seam.check
123
- gtd(check): seam.check → implement.applying
124
- gtd(check): implement.applying → implement.health.check
125
- gtd(check): implement.health.check → implement.closing
126
- gtd(check): implement.closing → build.review.reviewing
127
- """
128
- And the output lines of gtd "slices" start with:
129
- """
130
- feature-1
131
- feature-2
132
- """
133
- And slice "feature-2" begins where slice "feature-1" ends
134
- And "src/sub.test.ts" contains "[AC-3] divides"
135
- And "src/mul.test.ts" contains "[AC-2] multiplies"
136
- And ".gtd/REQUIREMENTS.md" does not exist
137
- And ".gtd/SLICES.md" does not exist
138
-
139
- Scenario: a slice over the cap is refused
140
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
141
- """
142
- # Requirements
143
-
144
- ## Impact
145
-
146
- - the greeting module
147
-
148
- ## Acceptance Criteria
149
-
150
- - AC-1: subtract(a, b) returns a - b.
151
- - AC-2: multiply(a, b) returns a * b.
152
- - AC-3: divide(a, b) returns a / b.
153
- """
154
- And step "design.slicing" writes ".gtd/SLICES.md" with:
155
- """
156
- # Slices
157
-
158
- ## Everything
159
-
160
- - AC-1
161
- - AC-2
162
- - AC-3
163
- """
164
- Then driving the process is refused with "more than 2 criteria in: Everything"
165
-
166
- Scenario: a criterion in no slice is refused
167
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
168
- """
169
- # Requirements
170
-
171
- ## Impact
172
-
173
- - the greeting module
174
-
175
- ## Acceptance Criteria
176
-
177
- - AC-1: subtract(a, b) returns a - b.
178
- - AC-2: multiply(a, b) returns a * b.
179
- - AC-3: divide(a, b) returns a / b.
180
- """
181
- And step "design.slicing" writes ".gtd/SLICES.md" with:
182
- """
183
- # Slices
184
-
185
- ## Subtract
186
-
187
- - AC-1
188
-
189
- ## Multiply
190
-
191
- - AC-2
192
- """
193
- Then driving the process is refused with "no slice for: AC-3"
194
-
195
- Scenario: a slicing turn may not change the acceptance criteria
196
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
197
- """
198
- # Requirements
199
-
200
- ## Impact
201
-
202
- - the greeting module
203
-
204
- ## Acceptance Criteria
205
-
206
- - AC-1: subtract(a, b) returns a - b.
207
- - AC-2: multiply(a, b) returns a * b.
208
- - AC-3: divide(a, b) returns a / b.
209
- """
210
- And step "design.slicing" writes ".gtd/REQUIREMENTS.md" with:
211
- """
212
- # Requirements
213
-
214
- ## Impact
215
-
216
- - the greeting module
217
-
218
- ## Acceptance Criteria
219
-
220
- - AC-1: subtract(a, b) returns a - b.
221
- - AC-2: multiply(a, b) returns a * b.
222
- """
223
- And step "design.slicing" writes ".gtd/SLICES.md" with:
224
- """
225
- # Slices
226
-
227
- ## Subtract
228
-
229
- - AC-1
230
-
231
- ## Multiply
232
-
233
- - AC-2
234
- """
235
- Then driving the process is refused with "wrote outside its write access"
236
-
237
- Scenario: a steer that empties the last slice still ends at the review of the whole change
238
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
239
- """
240
- # Requirements
241
-
242
- ## Impact
243
-
244
- - the greeting module
245
-
246
- ## Acceptance Criteria
247
-
248
- - AC-1: subtract(a, b) returns a - b.
249
- - AC-2: multiply(a, b) returns a * b.
250
- - AC-3: divide(a, b) returns a / b.
251
- """
252
- And step "design.slicing" writes ".gtd/SLICES.md" with:
253
- """
254
- # Slices
255
-
256
- ## Subtract and divide
257
-
258
- - AC-1
259
- - AC-3
260
-
261
- ## Multiply
262
-
263
- - AC-2
264
- """
265
- And step "seam.naive" writes "src/sub.test.ts" with:
266
- """
267
- it("[AC-1] subtracts", () => {})
268
- """
269
- When the process is driven
270
- Then the process rests at "seam.steer"
271
- Given a file ".gtd/REQUIREMENTS.md" with:
272
- """
273
- # Requirements
274
-
275
- ## Impact
276
-
277
- - the greeting module
278
-
279
- ## Acceptance Criteria
280
-
281
- - AC-1: subtract(a, b) returns a - b.
282
- """
283
- When gtd lands
284
- And the process is driven
285
- Then the process rests at "build.review.reviewing"
286
- And the commit subjects end with:
287
- """
288
- gtd(human): seam.steer → seam.naive
289
- gtd(agent): seam.naive → seam.check
290
- gtd(check): seam.check → implement.applying
291
- gtd(check): implement.applying → implement.health.check
292
- gtd(check): implement.health.check → implement.closing
293
- gtd(check): implement.closing → build.review.reviewing
294
- """
295
- And ".gtd/REQUIREMENTS.md" does not exist
296
- And ".gtd/SLICES.md" does not exist
297
- And the output of gtd "slices" does not contain "feature-2"
@@ -1,49 +0,0 @@
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
- })
@@ -1,105 +0,0 @@
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) }
@@ -1,92 +0,0 @@
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
- }