@pmelab/gtd 38.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.
@@ -0,0 +1,88 @@
1
+ @inmem
2
+ Feature: a rebuild stopped at its context limit is continued on the branch
3
+
4
+ The rebuild's snapshot is its own commit: the continuation finds the work
5
+ in the tree, and the health loop greens the whole build afterwards.
6
+ `seamAllow` is empty here, so the naive pass's change to an existing test
7
+ file is the judge's only stray and refactor-first rebuilds with no refactor.
8
+
9
+ Background:
10
+ Given the shipped workflows, as gtd install scaffolds them
11
+ And an environment variable "GTD_SEAMALLOW" set to ""
12
+ And an environment variable "GTD_QUALITYREVIEWS" set to ""
13
+ And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
14
+ And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
15
+ And a file "src/calc.ts" with:
16
+ """
17
+ export const add = (a: number, b: number) => a + b
18
+ """
19
+ And a file "tests/calc.test.ts" with:
20
+ """
21
+ it("adds", () => {})
22
+ """
23
+ And the working tree is committed as "feat: add calculator"
24
+ And a file "TODO.md" with:
25
+ """
26
+ # Add subtract and multiply
27
+ """
28
+ And step "unwind" changes nothing
29
+ And step "start-gate.check" changes nothing
30
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
31
+ """
32
+ # Requirements
33
+
34
+ ## Acceptance Criteria
35
+
36
+ - AC-1: `subtract(5, 3)` returns 2.
37
+ - AC-2: `multiply(2, 3)` returns 6.
38
+ """
39
+ And step "design.gate.answer" changes nothing
40
+ And step "seam.check" changes nothing
41
+
42
+ Scenario: a rebuild stopped at its context limit lands its snapshot and is continued on the branch
43
+ Given step "seam.naive" writes "src/ops.ts" with:
44
+ """
45
+ export const subtract = (a: number, b: number) => a - b
46
+ export const multiply = (a: number, b: number) => a * b
47
+ """
48
+ And step "seam.naive" writes "tests/calc.test.ts" with:
49
+ """
50
+ it("adds", () => {})
51
+ it("[AC-1] subtracts", () => {})
52
+ it("[AC-2] multiplies", () => {})
53
+ """
54
+ And judge step "seam.judge" answers:
55
+ """
56
+ [{"id":"shape","answer":"refactor-first","p":0.9}]
57
+ """
58
+ And step "implement.building" writes "tests/calc.test.ts" with:
59
+ """
60
+ it("adds", () => {})
61
+ it("[AC-1] subtracts", () => {})
62
+ """
63
+ And step "implement.building" writes ".gtd/HANDOFF.md" with:
64
+ """
65
+ Rebuilt subtract behind the flag; multiply was next.
66
+ """
67
+ When the process is driven
68
+ Then the process rests at "implement.continue.building"
69
+ And the resting step's content contains "Still open: AC-2."
70
+ And the resting step's content contains "Its work is already in the tree."
71
+ And the resting step's content contains "Delete `.gtd/HANDOFF.md` once you are done"
72
+ And "tests/calc.test.ts" contains "[AC-1] subtracts"
73
+
74
+ Given step "implement.continue.building" writes "tests/calc.test.ts" with:
75
+ """
76
+ it("adds", () => {})
77
+ it("[AC-1] subtracts", () => {})
78
+ it("[AC-2] multiplies", () => {})
79
+ """
80
+ And step "implement.continue.building" deletes ".gtd/HANDOFF.md"
81
+ When the process is driven
82
+ Then the process rests at "implement.health.check"
83
+ And the commit subjects end with:
84
+ """
85
+ gtd(agent): implement.building → implement.continue.building
86
+ gtd(agent): implement.continue.building → implement.health.check
87
+ """
88
+
@@ -0,0 +1,64 @@
1
+ @inmem
2
+ Feature: a continuation stays inside its coarse slice
3
+
4
+ A naive pass of one coarse slice that stops at its context limit is
5
+ continued with that slice's criteria still open, never another slice's.
6
+
7
+ Background:
8
+ Given the shipped workflows, as gtd install scaffolds them
9
+ And an environment variable "GTD_QUALITYREVIEWS" set to ""
10
+ And an environment variable "GTD_FLAGMODE" set to "false"
11
+ And an environment variable "GTD_SLICEMAXCRITERIA" set to "2"
12
+ And a file "src/calc.ts" with:
13
+ """
14
+ export const add = (a: number, b: number) => a + b
15
+ """
16
+ And the working tree is committed as "feat: add calculator"
17
+ And a file "TODO.md" with:
18
+ """
19
+ # More arithmetic
20
+
21
+ subtract, multiply and divide.
22
+ """
23
+ And step "unwind" changes nothing
24
+ And step "start-gate.check" changes nothing
25
+ And step "design.gate.answer" changes nothing
26
+ And step "seam.check" changes nothing
27
+
28
+ Scenario: the first slice's continuation is told only that slice's open criterion
29
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
30
+ """
31
+ # Requirements
32
+
33
+ ## Acceptance Criteria
34
+
35
+ - AC-1: subtract(a, b) returns a - b.
36
+ - AC-2: multiply(a, b) returns a * b.
37
+ - AC-3: divide(a, b) returns a / b.
38
+ """
39
+ And step "design.slicing" writes ".gtd/SLICES.md" with:
40
+ """
41
+ # Slices
42
+
43
+ ## Subtract and divide
44
+
45
+ - AC-1
46
+ - AC-3
47
+
48
+ ## Multiply
49
+
50
+ - AC-2
51
+ """
52
+ And step "seam.naive" writes "src/sub.test.ts" with:
53
+ """
54
+ it("[AC-1] subtracts", () => {})
55
+ """
56
+ And step "seam.naive" writes ".gtd/HANDOFF.md" with:
57
+ """
58
+ Subtract is tested; divide was next.
59
+ """
60
+ When the process is driven
61
+ Then the process rests at "seam.continue.naive"
62
+ And the resting step's content contains "This slice builds AC-1, AC-3 only"
63
+ And the resting step's content contains "Still open: AC-3."
64
+ And the resting step's content does not contain "AC-2"
@@ -0,0 +1,205 @@
1
+ @inmem
2
+ Feature: a turn stopped at its context limit is snapshotted and continued
3
+
4
+ A naive pass, refactor or build turn told to stop at its context limit
5
+ writes its handoff note to `.gtd/HANDOFF.md`. That turn is the snapshot —
6
+ the naive pass's kept tip, or the refactor's or build's own commit — and a
7
+ continuation in a fresh conversation (`<phase>.continue.<step>`) picks up
8
+ from it with the note and what is still open. After `continuations` (3) of
9
+ them, or `continuationStall` (2) in a row that close nothing still open, the
10
+ phase rests at `<phase>.oversized`.
11
+
12
+ Background:
13
+ Given the shipped workflows, as gtd install scaffolds them
14
+ And an environment variable "GTD_QUALITYREVIEWS" set to ""
15
+ And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
16
+ And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
17
+ And a file "src/calc.ts" with:
18
+ """
19
+ export const add = (a: number, b: number) => a + b
20
+ """
21
+ And the working tree is committed as "feat: add calculator"
22
+ And a file "TODO.md" with:
23
+ """
24
+ # Add subtract and multiply
25
+ """
26
+ And step "unwind" changes nothing
27
+ And step "start-gate.check" changes nothing
28
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
29
+ """
30
+ # Requirements
31
+
32
+ ## Acceptance Criteria
33
+
34
+ - AC-1: `subtract(5, 3)` returns 2.
35
+ - AC-2: `multiply(2, 3)` returns 6.
36
+ """
37
+ And step "design.gate.answer" changes nothing
38
+ And step "seam.check" changes nothing
39
+
40
+ Scenario: a naive pass stopped at its context limit is continued from its kept tip with the open IDs
41
+ Given step "seam.naive" writes "tests/calc.test.ts" with:
42
+ """
43
+ it("[AC-1] subtracts", () => {})
44
+ """
45
+ And step "seam.naive" writes ".gtd/HANDOFF.md" with:
46
+ """
47
+ Wrote subtract and its test; multiply was next.
48
+ """
49
+ When the process is driven
50
+ Then the process rests at "seam.continue.naive"
51
+ And the resting step's content contains "Your previous turn stopped at its context limit"
52
+ And the resting step's content contains "Still open: AC-2."
53
+ And the resting step's content contains "Wrote subtract and its test; multiply was next."
54
+ And the resting step's content contains "':(top)' ':(top,exclude).gtd/HANDOFF.md' | git apply"
55
+ And ".gtd/HANDOFF.md" does not exist
56
+ And the output of gtd "next --json" contains "\"resume\":false"
57
+ And the output of gtd "next --json" contains "\"memory\":\"seam.continue#"
58
+
59
+ Given step "seam.continue.naive" writes "tests/calc.test.ts" with:
60
+ """
61
+ it("[AC-1] subtracts", () => {})
62
+ it("[AC-2] multiplies", () => {})
63
+ """
64
+ When the process is driven
65
+ Then the process rests at "implement.applying"
66
+ And the commit subjects end with:
67
+ """
68
+ gtd(human): design.gate.answer → seam.naive
69
+ gtd(agent): seam.naive → seam.continue.naive
70
+ gtd(agent): seam.continue.naive → seam.check
71
+ gtd(check): seam.check → implement.applying
72
+ """
73
+
74
+ Scenario: two continuations in a row that close nothing rest for a person
75
+ Given step "seam.naive" writes "tests/calc.test.ts" with:
76
+ """
77
+ it("[AC-1] subtracts", () => {})
78
+ """
79
+ And step "seam.naive" writes ".gtd/HANDOFF.md" with:
80
+ """
81
+ Subtract is tested; multiply was next.
82
+ """
83
+ And step "seam.continue.naive" writes ".gtd/HANDOFF.md" with:
84
+ """
85
+ Still on multiply.
86
+ """
87
+ When the process is driven
88
+ Then the process rests at "seam.oversized"
89
+ And the resting step's content contains "the last 2 continuations closed nothing still open"
90
+ And the commit subjects end with:
91
+ """
92
+ gtd(human): design.gate.answer → seam.naive
93
+ gtd(agent): seam.naive → seam.continue.naive
94
+ gtd(agent): seam.continue.naive
95
+ gtd(agent): seam.continue.naive → seam.oversized
96
+ """
97
+
98
+ Scenario: a refactor stopped at its context limit is run green before its continuation
99
+ Given step "seam.naive" writes "tests/calc.test.ts" with:
100
+ """
101
+ it("[AC-1] subtracts", () => {})
102
+ it("[AC-2] multiplies", () => {})
103
+ """
104
+ And 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
+ export const multiply = (a: number, b: number) => a * b
109
+ """
110
+ And judge step "seam.judge" answers:
111
+ """
112
+ [{"id":"shape","answer":"refactor-first","p":0.9}]
113
+ """
114
+ And step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
115
+ """
116
+ Seam: a shared `op` in src/calc.ts.
117
+ """
118
+ And step "refactor.deriving" runs its script
119
+ And step "refactor.building" writes ".gtd/HANDOFF.md" with:
120
+ """
121
+ Started on op; nothing changed yet.
122
+ """
123
+ And step "refactor.health.check" changes nothing
124
+ When the process is driven
125
+ Then the process rests at "refactor.continue.building"
126
+ And the resting step's content contains "Still open: src/calc.ts."
127
+ And the resting step's content contains "Started on op; nothing changed yet."
128
+ And the commit subjects end with:
129
+ """
130
+ gtd(agent): refactor.building → refactor.health.check
131
+ gtd(check): refactor.health.check → refactor.continue.building
132
+ """
133
+
134
+ Scenario: a refactor continuation that leaves the note in place stopped again
135
+ Given step "seam.naive" writes "tests/calc.test.ts" with:
136
+ """
137
+ it("[AC-1] subtracts", () => {})
138
+ it("[AC-2] multiplies", () => {})
139
+ """
140
+ And step "seam.naive" writes "src/calc.ts" with:
141
+ """
142
+ export const add = (a: number, b: number) => a + b
143
+ export const subtract = (a: number, b: number) => a - b
144
+ """
145
+ And judge step "seam.judge" answers:
146
+ """
147
+ [{"id":"shape","answer":"refactor-first","p":0.9}]
148
+ """
149
+ And step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
150
+ """
151
+ Seam: a shared `op` in src/calc.ts.
152
+ """
153
+ And step "refactor.deriving" runs its script
154
+ And step "refactor.building" writes "src/calc.ts" with:
155
+ """
156
+ export const op = (a: number, b: number, sign: number) => a + sign * b
157
+ export const add = (a: number, b: number) => a + b
158
+ """
159
+ And step "refactor.building" writes ".gtd/HANDOFF.md" with:
160
+ """
161
+ op is in; routing add through it was next.
162
+ """
163
+ And step "refactor.health.check" changes nothing
164
+ And step "refactor.continue.building" writes "src/calc.ts" with:
165
+ """
166
+ export const op = (a: number, b: number, sign: number) => a + sign * b
167
+ export const add = (a: number, b: number) => op(a, b, 1)
168
+ """
169
+ When the process is driven
170
+ Then the process rests at "refactor.continue.building"
171
+ And the commit subjects end with:
172
+ """
173
+ gtd(agent): refactor.building → refactor.health.check
174
+ gtd(check): refactor.health.check → refactor.continue.building
175
+ gtd(agent): refactor.continue.building → refactor.health.check
176
+ gtd(check): refactor.health.check → refactor.continue.building
177
+ gtd(agent): refactor.continue.building
178
+ """
179
+
180
+ Scenario: once a naive pass has continued, its retry runs in a fresh conversation, not as a continuation
181
+ Given step "seam.naive" writes "tests/calc.test.ts" with:
182
+ """
183
+ it("[AC-1] subtracts", () => {})
184
+ """
185
+ And step "seam.naive" writes ".gtd/HANDOFF.md" with:
186
+ """
187
+ Multiply was next.
188
+ """
189
+ And step "seam.continue.naive" writes "tests/calc.test.ts" with:
190
+ """
191
+ it("[AC-1] subtracts", () => {})
192
+ it("[AC-2] multiplies", () => {})
193
+ """
194
+ And step "seam.continue.naive" writes "src/calc.ts" with:
195
+ """
196
+ export const add = (a: number, b: number) => a - b
197
+ """
198
+ And judge step "seam.judge" answers:
199
+ """
200
+ [{"id":"shape","answer":"reject","p":0.9}]
201
+ """
202
+ When the process is driven
203
+ Then the process rests at "seam.fresh.naive"
204
+ And the resting step's content contains "rejected"
205
+ And the output of gtd "next --json" contains "\"resume\":false"
@@ -189,3 +189,14 @@ export const skipDesign = (stated: StatedCriteria | undefined): Promise<boolean>
189
189
  })
190
190
  return true
191
191
  })
192
+
193
+ /** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
194
+ export const requireCriteriaIfEdited = (): void => {
195
+ const change = changes().get(REQUIREMENTS)
196
+ if (change === undefined) return
197
+ if (change.status === "deleted")
198
+ refuse(
199
+ `gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`,
200
+ )
201
+ requireCriteria(REQUIREMENTS)
202
+ }
@@ -15,8 +15,9 @@ import {
15
15
  } from "../../flows/index.js"
16
16
  import { lensEntriesOf } from "../../workflow-kit/index.js"
17
17
  import { markFenced } from "../shared/fences.js"
18
+ import { continuing, phase, type Phase } from "../shared/continuation.js"
18
19
  import { healthy, type Guard } from "../shared/health.js"
19
- import { coder, FEEDBACK, REFACTOR } from "../shared/steps.js"
20
+ import { coder, FEEDBACK, HANDOFF, REFACTOR } from "../shared/steps.js"
20
21
  import * as t from "../shared/text.js"
21
22
  import { defaults, testFileGlobs } from "../shared/vars.js"
22
23
  import { buildTail, reUnwind, type FeedbackTarget } from "./review.js"
@@ -263,7 +264,12 @@ const guardOf =
263
264
  }
264
265
 
265
266
  /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
266
- const refactorLap = async (p: Plan, guard: Guard, feedback?: string): Promise<void> => {
267
+ const refactorLap = async (
268
+ p: Plan,
269
+ guard: Guard,
270
+ refactor: Phase,
271
+ feedback?: string,
272
+ ): Promise<void> => {
267
273
  const { pattern, strays, reference, note } = p
268
274
  await run(
269
275
  "deriving",
@@ -276,32 +282,59 @@ const refactorLap = async (p: Plan, guard: Guard, feedback?: string): Promise<vo
276
282
  ]),
277
283
  { label: "Deriving the refactor package", file: REFACTOR_PACKAGE },
278
284
  )
279
- await t.agentWithSkills(
280
- "building",
281
- t.refactorBuildingPrompt({
282
- pkg: REFACTOR_PACKAGE,
283
- strays,
284
- pattern,
285
- reference,
286
- feedback: feedback !== undefined,
287
- note: note !== undefined,
288
- }),
289
- { label: "Refactoring", model: coder(), system: t.builderSystem() },
290
- )
291
- guard()
292
- await healthy(
293
- async () => {
294
- await t.agentWithSkills("fix.suite.fixing", t.refactorFixPrompt(strays, pattern), {
295
- label: "Fixing the check",
296
- file: FEEDBACK,
297
- model: coder(),
298
- system: t.builderSystem(),
299
- })
300
- guard()
301
- },
302
- { describeGuard: guard, rules: t.refactorRules(strays, pattern) },
303
- )
304
- await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" })
285
+ const reached = new Set<string>()
286
+ // Each snapshot is greened before its continuation: every sub-step keeps the old suite green.
287
+ await continuing(refactor, async (from, fresh) => {
288
+ await fresh(() =>
289
+ t.agentWithSkills(
290
+ "building",
291
+ t.refactorBuildingPrompt({
292
+ pkg: REFACTOR_PACKAGE,
293
+ strays,
294
+ pattern,
295
+ reference,
296
+ feedback: feedback !== undefined,
297
+ note: note !== undefined,
298
+ from,
299
+ }),
300
+ {
301
+ label: from === undefined ? "Refactoring" : "Continuing the refactor",
302
+ model: coder(),
303
+ system: t.builderSystem(),
304
+ },
305
+ ),
306
+ )
307
+ // Committed by the snapshot, so a turn stopped again may leave it as it was: present means stopped.
308
+ const left = read(HANDOFF)
309
+ guard()
310
+ const snapshot = head()
311
+ // The building turns' own changes only: a fix turn reaching a path is not the refactor reaching it.
312
+ for (const c of changes()) reached.add(c.path)
313
+ await healthy(
314
+ async () => {
315
+ await t.agentWithSkills("fix.suite.fixing", t.refactorFixPrompt(strays, pattern), {
316
+ label: "Fixing the check",
317
+ file: FEEDBACK,
318
+ model: coder(),
319
+ system: t.builderSystem(),
320
+ })
321
+ guard()
322
+ },
323
+ { describeGuard: guard, rules: t.refactorRules(strays, pattern) },
324
+ )
325
+ if (left === undefined) return { result: undefined }
326
+ return {
327
+ result: undefined,
328
+ handoff: {
329
+ snapshot,
330
+ note: left.trim(),
331
+ open: strays.filter((path) => !reached.has(path)),
332
+ },
333
+ }
334
+ })
335
+ await run("closing", removeScript([REFACTOR_PACKAGE, HANDOFF]), {
336
+ label: "Closing out the refactor",
337
+ })
305
338
  }
306
339
 
307
340
  const strayAccess = (strays: readonly string[]) => ({
@@ -315,7 +348,7 @@ export const refactorFirst = async (
315
348
  const p = plan(shape, options)
316
349
  if (p === undefined) return false
317
350
  return scope({ name: "refactor", access: strayAccess(p.strays) }, async () => {
318
- await refactorLap(p, guardOf(p, head()))
351
+ await refactorLap(p, guardOf(p, head()), phase("refactor"))
319
352
  return true
320
353
  })
321
354
  }
@@ -342,9 +375,10 @@ export const reviewedRefactor = async (
342
375
  "gtd land: refactor-feedback: the review feedback changed files but wrote no .gtd/REFACTOR.md — a refactor cannot act on it; revert the change, or `gtd abandon` and restart",
343
376
  dropped: t.refactorDroppedRefusal,
344
377
  }
378
+ const refactor = phase("refactor")
345
379
  let collected: string | undefined
346
380
  for (;;) {
347
- await refactorLap(p, guard, collected)
381
+ await refactorLap(p, guard, refactor, collected)
348
382
  // A person's edit at an escalation rest is never refused, as in the lap.
349
383
  const outcome = await buildTail(entry, {
350
384
  green: true,