@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -0
- package/dist/chunks/{reserved-zAfoJtR7.mjs → reserved-CfQ8eurM.mjs} +85 -17
- package/dist/chunks/{tail-BRC4DGdV.mjs → tail-C7yaWvaS.mjs} +1 -1
- package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
- package/dist/gtd.bundle.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +318 -76
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +419 -109
- package/package.json +1 -1
- package/src/testkit/steps.ts +39 -0
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/feature/continuation-cap.feature +65 -0
- package/src/workflows/feature/continuation-rebuild.feature +88 -0
- package/src/workflows/feature/continuation-slices.feature +64 -0
- package/src/workflows/feature/continuation.feature +205 -0
- package/src/workflows/feature/coverage.ts +14 -6
- package/src/workflows/feature/criteria.test.ts +17 -1
- package/src/workflows/feature/criteria.ts +37 -3
- package/src/workflows/feature/feature.ts +64 -15
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor.ts +64 -30
- package/src/workflows/feature/seam.ts +157 -48
- package/src/workflows/feature/slicing-cap.feature +14 -0
- package/src/workflows/feature/slicing.feature +225 -0
- package/src/workflows/feature/slicing.test.ts +49 -0
- package/src/workflows/feature/slicing.ts +105 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/continuation.ts +92 -0
- package/src/workflows/shared/fences.ts +5 -0
- package/src/workflows/shared/options.ts +3 -0
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/text.test.ts +6 -2
- package/src/workflows/shared/text.ts +153 -22
- package/src/workflows/shared/vars.ts +6 -0
- 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 {
|
|
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 {
|
|
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 →
|
|
26
|
-
// lands as the implementation, or (refactoring
|
|
27
|
-
// built fresh from → the health loop → the
|
|
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
|
-
/**
|
|
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
|
|
44
|
-
|
|
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
|
|
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
|
-
?
|
|
104
|
+
? sliceBase
|
|
59
105
|
: seam.base
|
|
60
|
-
|
|
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")) {
|
|
@@ -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 (
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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 {
|
|
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 {
|
|
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 = (
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
(
|
|
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 = (
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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 (
|
|
344
|
-
|
|
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
|
|
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
|
|
414
|
-
|
|
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)
|
|
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
|
|
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\""
|