@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
@@ -11,21 +11,29 @@ import {
11
11
  } from "../../flows/index.js"
12
12
  import { gate, healthy } from "../shared/health.js"
13
13
  import { flagModeOn, shipped } from "../shared/options.js"
14
- import { FEEDBACK, fixCheck, QUALITY } from "../shared/steps.js"
14
+ import { FEEDBACK, fixCheck, QUALITY, REQUIREMENTS } from "../shared/steps.js"
15
15
  import { refusePendingHandOff, tail } from "../shared/tail.js"
16
16
  import * as t from "../shared/text.js"
17
- import { skipDesign, statedCriteria, type StatedCriteria } from "./criteria.js"
17
+ import {
18
+ requireCriteriaFrozen,
19
+ skipDesign,
20
+ sliceIds,
21
+ statedCriteria,
22
+ type CriteriaSlice,
23
+ type StatedCriteria,
24
+ } from "./criteria.js"
18
25
  import { design } from "./planning.js"
19
26
  import { buildTail, isCode, reUnwind, type ReviewOutcome } from "./review.js"
20
- import { implement, seamBuild } from "./seam.js"
21
- import { recordRisk, riskTierOn } from "./tier.js"
27
+ import { closeOut, implement, seamBuild } from "./seam.js"
28
+ import { sliceCap, sliceCriteria, slicing } from "./slicing.js"
29
+ import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
22
30
 
23
31
  // The ordinary start. Any change to the tree starts a process: `idle` →
24
32
  // `unwind` reverts the sketch (its intent survives in history) → a
25
- // green-baseline gate → design → a naive pass, judged with `flagMode` on: it
26
- // lands as the implementation, or (refactoring first when the judge asks) is
27
- // built fresh from → the health loop → the quality lap, and the episode ends
28
- // back at `idle`. Human review is the `review` door's.
33
+ // green-baseline gate → design → per slice of the criteria, a naive pass,
34
+ // judged with `flagMode` on: it lands as the implementation, or (refactoring
35
+ // first when the judge asks) is built fresh from → the health loop → the
36
+ // quality lap, and the episode ends back at `idle`. Human review is the `review` door's.
29
37
 
30
38
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
31
39
  export const unwind = (): Promise<void> => {
@@ -37,27 +45,67 @@ export const unwind = (): Promise<void> => {
37
45
 
38
46
  export { reUnwind }
39
47
 
40
- /** A naive pass (judged only with `flagMode` on), then implement and the quality lap; design must have run since `firstBase`. */
48
+ /**
49
+ * The slices `design.slicing` grouped the criteria into, each built in turn
50
+ * from where the previous one landed; design must have run since `firstBase`.
51
+ */
41
52
  export const buildFeature = async (firstBase: string): Promise<void> => {
42
53
  const tier = riskTierOn() ? recordRisk() : undefined
43
- const seam = await seamBuild(firstBase)
44
- await slice("feature", async () => {
54
+ const slices = await slicing()
55
+ // A single slice keeps the unnumbered names and builds against every criterion.
56
+ const one = slices.length === 1
57
+ for (const i of slices.keys()) {
58
+ const more = i < slices.length - 1
59
+ const criteria = sliceCriteria(slices, i)
60
+ // A steer may have deleted every criterion of a later slice: nothing to build.
61
+ if (sliceIds(criteria)?.length === 0) {
62
+ if (!more) await closeOut()
63
+ continue
64
+ }
65
+ const n = one ? "" : `-${i + 1}`
66
+ await buildSlice(i === 0 ? firstBase : head(), {
67
+ tier,
68
+ criteria,
69
+ refactor: `refactor${n}`,
70
+ feature: `feature${n}`,
71
+ more,
72
+ })
73
+ }
74
+ }
75
+
76
+ interface SliceBuild {
77
+ readonly tier: RiskTier | undefined
78
+ readonly criteria: CriteriaSlice | undefined
79
+ readonly refactor: string
80
+ readonly feature: string
81
+ readonly more: boolean
82
+ }
83
+
84
+ /** A naive pass (judged only with `flagMode` on), then implement and the quality lap. */
85
+ const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> => {
86
+ const seam = await seamBuild(sliceBase, { criteria: build.criteria, refactor: build.refactor })
87
+ await slice(build.feature, async () => {
45
88
  await implement({
46
89
  reference: seam.reference,
47
- tier,
90
+ tier: build.tier,
48
91
  afterRefactor: seam.refactored,
49
92
  naive: seam.land === true ? seam.reference : undefined,
50
93
  rebuild: seam.rebuild,
94
+ slice: build.criteria,
95
+ base: build.criteria === undefined ? undefined : sliceBase,
96
+ more: build.more,
51
97
  })
52
98
  // A feature slice with no code beyond the carried-over tests: the refactor carried the
53
- // feature, so the lap covers the whole change.
99
+ // feature, so the lap covers the whole slice.
54
100
  const carried = seam.rebuild?.acceptance?.tests ?? []
55
101
  const base =
56
102
  seam.refactored === true &&
57
103
  !changesSince(seam.base).some((c) => isCode(c.path) && !carried.includes(c.path))
58
- ? firstBase
104
+ ? sliceBase
59
105
  : seam.base
60
- await tail(settleQuality, { green: true, lensBase: base })
106
+ // The criteria stay in the tree for the next slice; no quality fix may move them.
107
+ const guard = build.more ? () => requireCriteriaFrozen(REQUIREMENTS) : undefined
108
+ await tail(settleQuality, { green: true, lensBase: base, guard, guardRests: false })
61
109
  })
62
110
  }
63
111
 
@@ -103,6 +151,7 @@ export const ordinaryStart = async (): Promise<void> => {
103
151
  refusePendingHandOff()
104
152
  // Read right after idle, so a bad value refuses the landing that would pin it.
105
153
  flagModeOn()
154
+ sliceCap()
106
155
  const stated = statedCriteria(changes(), head())
107
156
  await unwind()
108
157
  if (changes(FEEDBACK).some((c) => c.status !== "deleted")) {
@@ -5,6 +5,7 @@ export * from "./criteria.js"
5
5
  export * from "./planning.js"
6
6
  export * from "./refactor.js"
7
7
  export * from "./seam.js"
8
+ export * from "./slicing.js"
8
9
  export {
9
10
  buildTail,
10
11
  type BuildTailOptions,
@@ -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,
@@ -17,18 +17,27 @@ import {
17
17
  removeScript,
18
18
  run,
19
19
  scope,
20
- slice,
20
+ slice as sliced,
21
+ start,
21
22
  vars,
22
23
  wrote,
23
24
  type Change,
24
25
  type DiffShapeResult,
25
26
  type KeptTip,
26
27
  } from "../../flows/index.js"
27
- import { coder, FEEDBACK, REFACTOR_NOTE, REQUIREMENTS } from "../shared/steps.js"
28
+ import { continuing, phase, type Handoff, type Phase } from "../shared/continuation.js"
29
+ import { coder, FEEDBACK, HANDOFF, REFACTOR_NOTE, REQUIREMENTS, SLICES } from "../shared/steps.js"
28
30
  import * as t from "../shared/text.js"
29
31
  import { defaults } from "../shared/vars.js"
30
32
  import { green, requireCoverage } from "./coverage.js"
31
- import { criteriaOf, requireCriteria, requireCriteriaFrozen } from "./criteria.js"
33
+ import {
34
+ criterionIds,
35
+ requireCriteria,
36
+ requireCriteriaFrozen,
37
+ requireCriteriaIfEdited,
38
+ sliceIds,
39
+ type CriteriaSlice,
40
+ } from "./criteria.js"
32
41
  import {
33
42
  COMMENT,
34
43
  holdsCallSites,
@@ -54,14 +63,47 @@ export const judgeSeam = (tip: KeptTip): Promise<DiffShapeResult> =>
54
63
  exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path),
55
64
  })
56
65
 
57
- export const naivePass = (retry?: t.NaiveRetry): Promise<void> =>
58
- t.agentWithSkills("naive", t.naivePassPrompt(retry), {
59
- label: retry === undefined ? "Writing the naive pass" : "Rewriting the naive pass",
66
+ export const naivePass = (
67
+ retry?: t.NaiveRetry,
68
+ slice?: CriteriaSlice,
69
+ from?: Handoff,
70
+ ): Promise<void> =>
71
+ t.agentWithSkills("naive", t.naivePassPrompt(retry, sliceIds(slice), from), {
72
+ label:
73
+ from !== undefined
74
+ ? "Continuing the naive pass"
75
+ : retry === undefined
76
+ ? "Writing the naive pass"
77
+ : "Rewriting the naive pass",
60
78
  keep: true,
61
79
  model: coder(),
62
80
  system: t.builderSystem(),
63
81
  })
64
82
 
83
+ const handoffOf = (tip: KeptTip, slice: CriteriaSlice | undefined): Handoff | undefined => {
84
+ const note = tip.changes.find((c) => c.path === HANDOFF && c.after !== undefined)
85
+ if (note === undefined) return undefined
86
+ return {
87
+ snapshot: tip.hash,
88
+ note: note.after!.trim(),
89
+ open: unnamed(idsOf(REQUIREMENTS, slice), acceptanceTests(tip.changes)),
90
+ }
91
+ }
92
+
93
+ /** A naive pass, continued until a pass ends without a handoff note: the snapshot is its kept tip. */
94
+ const naiveContinued = (
95
+ retry: t.NaiveRetry | undefined,
96
+ slice: CriteriaSlice | undefined,
97
+ naive: Phase,
98
+ ) =>
99
+ continuing(naive, async (from, fresh) => {
100
+ const tip = await fresh(async () => {
101
+ await naivePass(retry, slice, from)
102
+ return kept("naive")!
103
+ })
104
+ return { result: tip, handoff: handoffOf(tip, slice) }
105
+ })
106
+
65
107
  // Only a string an import, `require` or `mock` call takes: a URL route or path-like data is not one.
66
108
  const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g
67
109
  const EXTENSION = /\.[\w-]+$/
@@ -107,8 +149,8 @@ const addedLines = (change: Change): string => {
107
149
  const acceptanceTests = (changes: readonly Change[]): readonly Change[] =>
108
150
  changes.filter((c) => c.after !== undefined && isCode(c.path) && isTestFile(c.path))
109
151
 
110
- const idsOf = (file: string): string[] =>
111
- (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "")
152
+ const idsOf = (file: string, slice?: CriteriaSlice): string[] =>
153
+ criterionIds(read(file) ?? "", slice)
112
154
 
113
155
  // One rule for both gates, so a naive test carried over verbatim always satisfies the rebuild's.
114
156
  const unnamed = (ids: readonly string[], tests: readonly Change[]): string[] => {
@@ -121,9 +163,13 @@ const unnamed = (ids: readonly string[], tests: readonly Change[]): string[] =>
121
163
  * test line of the pass names, or a test importing a file the pass added —
122
164
  * naive structure a reset and refactor replaces, which the tests must survive.
123
165
  */
124
- export const acceptanceFault = (file: string, tip: KeptTip): string | undefined => {
166
+ export const acceptanceFault = (
167
+ file: string,
168
+ tip: KeptTip,
169
+ slice?: CriteriaSlice,
170
+ ): string | undefined => {
125
171
  const tests = acceptanceTests(tip.changes)
126
- const untested = unnamed(idsOf(file), tests)
172
+ const untested = unnamed(idsOf(file, slice), tests)
127
173
  if (untested.length > 0)
128
174
  return `acceptance-tests: no test of the naive pass names ${untested.join(", ")} — write a behaviour-level test for each criterion with its ID in the test or scenario name, e.g. "[${untested[0]}] …"`
129
175
  const added = tip.changes
@@ -143,8 +189,8 @@ export const acceptanceFault = (file: string, tip: KeptTip): string | undefined
143
189
  }
144
190
 
145
191
  /** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
146
- export const naiveAcceptance = (tip: KeptTip): t.Acceptance => {
147
- const ids = idsOf(REQUIREMENTS)
192
+ export const naiveAcceptance = (tip: KeptTip, slice?: CriteriaSlice): t.Acceptance => {
193
+ const ids = idsOf(REQUIREMENTS, slice)
148
194
  const perTest = acceptanceTests(tip.changes).map((c) => {
149
195
  const lines = addedLines(c)
150
196
  return { path: c.path, ids: ids.filter((id) => named(id, lines)) }
@@ -232,6 +278,12 @@ export interface ImplementOptions {
232
278
  readonly naive?: string | undefined
233
279
  /** A rebuild after a refactor-first judgment: the note it plugs into, the acceptance tests it must carry. */
234
280
  readonly rebuild?: t.Rebuild | undefined
281
+ /** The criteria this slice builds, when the change has more than one slice. */
282
+ readonly slice?: CriteriaSlice | undefined
283
+ /** Where the slice began: the coverage judge reads only the tests changed since. */
284
+ readonly base?: string | undefined
285
+ /** Not the last slice: `.gtd/REQUIREMENTS.md` and `.gtd/SLICES.md` stay for the next one. */
286
+ readonly more?: boolean | undefined
235
287
  }
236
288
 
237
289
  /** Resolves `false` when the tip no longer applies (the failure is in `.gtd/FEEDBACK.md`) or has nothing outside `.gtd/` to apply. */
@@ -242,6 +294,56 @@ const applied = async (tip: string): Promise<boolean> => {
242
294
  return !wrote(FEEDBACK) && codeChanges().length > 0
243
295
  }
244
296
 
297
+ /** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
298
+ const settled = (
299
+ carried: readonly string[] | undefined,
300
+ slice: CriteriaSlice | undefined,
301
+ ): readonly string[] => {
302
+ const listed = idsOf(REQUIREMENTS, slice)
303
+ if (carried === undefined) return listed
304
+ return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id))
305
+ }
306
+
307
+ // A snapshot lands on the branch as it is; the health loop greens the whole build after.
308
+ // Open IDs count every test line since the process start, so an amend lap's earlier tests close theirs.
309
+ const build = (options: ImplementOptions): Promise<void> =>
310
+ continuing(phase("build", requireCriteriaIfEdited), async (from, fresh) => {
311
+ await fresh(() =>
312
+ t.agentWithSkills(
313
+ "building",
314
+ t.implementBuildingPrompt(
315
+ options.reference,
316
+ options.tier,
317
+ options.amend,
318
+ options.afterRefactor,
319
+ flagModeOn(),
320
+ options.rebuild,
321
+ sliceIds(options.slice),
322
+ from,
323
+ ),
324
+ {
325
+ label: from === undefined ? "Implementing" : "Continuing the implementation",
326
+ model: coder(),
327
+ system: t.builderSystem(),
328
+ ...(options.afterRefactor === true ? { allowEmpty: true } : {}),
329
+ },
330
+ ),
331
+ )
332
+ requireCriteriaFrozen(REQUIREMENTS)
333
+ // Committed by the snapshot, so a turn stopped again may leave it as it was: present means stopped.
334
+ const note = read(HANDOFF)
335
+ if (note === undefined) return { result: undefined }
336
+ const ids = settled(options.rebuild?.acceptance?.ids, options.slice)
337
+ return {
338
+ result: undefined,
339
+ handoff: {
340
+ snapshot: head(),
341
+ note: note.trim(),
342
+ open: unnamed(ids, acceptanceTests(changesSince(start()))),
343
+ },
344
+ }
345
+ })
346
+
245
347
  export function implement(options?: ImplementOptions): Promise<void>
246
348
  /** @deprecated Pass `implement({ reference, tier, amend })`. */
247
349
  export function implement(
@@ -260,37 +362,31 @@ export function implement(
260
362
  const since = head()
261
363
  const acceptance = options.rebuild?.acceptance
262
364
  const built = options.naive === undefined || !(await applied(options.naive))
263
- if (built)
264
- await t.agentWithSkills(
265
- "building",
266
- t.implementBuildingPrompt(
267
- options.reference,
268
- options.tier,
269
- options.amend,
270
- options.afterRefactor,
271
- flagModeOn(),
272
- options.rebuild,
273
- ),
274
- {
275
- label: "Implementing",
276
- model: coder(),
277
- system: t.builderSystem(),
278
- ...(options.afterRefactor === true ? { allowEmpty: true } : {}),
279
- },
280
- )
281
- requireCriteriaFrozen(REQUIREMENTS)
282
- if (built && acceptance !== undefined) requireAcceptance(acceptance, since)
365
+ if (built) await build(options)
366
+ else requireCriteriaFrozen(REQUIREMENTS)
367
+ // Only the criteria still listed: a person may narrow them at `implement.oversized`.
368
+ if (built && acceptance !== undefined)
369
+ requireAcceptance({ ...acceptance, ids: settled(acceptance.ids, options.slice) }, since)
283
370
  // Fix turns are held to the carried tests too: deleting a red one must not buy a green suite.
284
371
  await green(
285
372
  built && acceptance !== undefined ? () => requireAcceptanceKept(acceptance) : undefined,
286
373
  )
287
- await requireCoverage()
288
- await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), {
289
- label: "Closing out the implementation",
290
- })
374
+ await requireCoverage(options.base, options.slice)
375
+ await closing(options.more === true)
291
376
  })
292
377
  }
293
378
 
379
+ /** The last slice's closing, for a last slice a steer left with no criterion to build. */
380
+ export const closeOut = (): Promise<void> => scope("implement", () => closing(false))
381
+
382
+ /** A slice with `more` after it keeps the criteria and the slicing for the next one. */
383
+ const closing = (more: boolean): Promise<void> =>
384
+ run(
385
+ "closing",
386
+ removeScript(more ? [REFACTOR_NOTE, HANDOFF] : [REQUIREMENTS, REFACTOR_NOTE, SLICES, HANDOFF]),
387
+ { label: "Closing out the implementation" },
388
+ )
389
+
294
390
  interface Decision {
295
391
  readonly reference: string
296
392
  readonly tip: KeptTip
@@ -340,8 +436,12 @@ interface Vetting {
340
436
  }
341
437
 
342
438
  /** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
343
- const vetted = async (tip: KeptTip, vetting: Vetting): Promise<Verdict | undefined> => {
344
- const fault = vetting.waived ? undefined : acceptanceFault(REQUIREMENTS, tip)
439
+ const vetted = async (
440
+ tip: KeptTip,
441
+ vetting: Vetting,
442
+ slice: CriteriaSlice | undefined,
443
+ ): Promise<Verdict | undefined> => {
444
+ const fault = vetting.waived ? undefined : acceptanceFault(REQUIREMENTS, tip, slice)
345
445
  if (fault !== undefined) {
346
446
  const verdict = await vetting.again({ fault, hash: tip.hash })
347
447
  if (verdict.kind !== "overruled") return verdict
@@ -361,15 +461,15 @@ const ruled = async (tip: KeptTip, again: Vetting["again"]): Promise<Decision |
361
461
  return next.kind === "overruled" ? { reference: tip.hash, tip } : next
362
462
  }
363
463
 
364
- const decide = (): Promise<Decision> =>
464
+ const decide = (slice: CriteriaSlice | undefined): Promise<Decision> =>
365
465
  scope("seam", async () => {
366
466
  const vetting: Vetting = { again: budget(), waived: false }
467
+ const naive = phase("naive pass", requireCriteriaIfEdited)
367
468
  let retry: t.NaiveRetry | undefined
368
469
  for (;;) {
369
- await naivePass(retry)
370
- const tip = kept("naive")!
470
+ const tip = await naiveContinued(retry, slice, naive)
371
471
  const outcome =
372
- (await vetted(tip, vetting)) ??
472
+ (await vetted(tip, vetting, slice)) ??
373
473
  (flagModeOn() ? await ruled(tip, vetting.again) : { reference: tip.hash, tip })
374
474
  if (!("kind" in outcome)) return outcome
375
475
  retry = outcome.kind === "retry" ? outcome.retry : undefined
@@ -410,8 +510,14 @@ export const refactorNote = async (
410
510
  return note
411
511
  }
412
512
 
413
- export const seamBuild = async (base: string): Promise<SeamOutcome> => {
414
- const { reference, tip, shape } = await decide()
513
+ export interface SeamSlice {
514
+ /** Absent for a change built in one slice. */
515
+ readonly criteria?: CriteriaSlice | undefined
516
+ readonly refactor?: string | undefined
517
+ }
518
+
519
+ export const seamBuild = async (base: string, slice: SeamSlice = {}): Promise<SeamOutcome> => {
520
+ const { reference, tip, shape } = await decide(slice.criteria)
415
521
  if (shape === undefined) return { reference, base, land: true }
416
522
  if (shape.unreadable.length > 0) {
417
523
  await scope("seam", () =>
@@ -421,17 +527,20 @@ export const seamBuild = async (base: string): Promise<SeamOutcome> => {
421
527
  acceptClean: true,
422
528
  }),
423
529
  )
424
- return { reference, base, rebuild: { acceptance: naiveAcceptance(tip) } }
530
+ return { reference, base, rebuild: { acceptance: naiveAcceptance(tip, slice.criteria) } }
425
531
  }
426
532
  const strays = refactorStrays(shape)
427
- if (strays.length === 0) return { reference, base, rebuild: { acceptance: naiveAcceptance(tip) } }
533
+ if (strays.length === 0)
534
+ return { reference, base, rebuild: { acceptance: naiveAcceptance(tip, slice.criteria) } }
428
535
  const note = await scope("seam", () => refactorNote(reference, strays))
429
- const refactored = await slice("refactor", () => reviewedRefactor(shape, { reference, note }))
536
+ const refactored = await sliced(slice.refactor ?? "refactor", () =>
537
+ reviewedRefactor(shape, { reference, note }),
538
+ )
430
539
  return {
431
540
  reference,
432
541
  base: head(),
433
542
  refactored,
434
543
  // Fixed at `seam.note`, like the package's scope: a later edit of the file reaches neither turn.
435
- rebuild: { note, acceptance: naiveAcceptance(tip) },
544
+ rebuild: { note, acceptance: naiveAcceptance(tip, slice.criteria) },
436
545
  }
437
546
  }
@@ -0,0 +1,14 @@
1
+ @inmem
2
+ Feature: a bad sliceMaxCriteria refuses the landing that would pin it
3
+
4
+ The cap is read right after idle, so a value that is not a positive integer
5
+ never gets pinned into a process that would refuse every later landing.
6
+
7
+ Scenario: a zero cap refuses the idle landing
8
+ Given the shipped workflows, as gtd install scaffolds them
9
+ And an environment variable "GTD_SLICEMAXCRITERIA" set to "0"
10
+ And a file "TODO.md" with:
11
+ """
12
+ # Something
13
+ """
14
+ Then driving the process is refused with "sliceMaxCriteria must be a positive integer, got \"0\""