@pmelab/gtd 52.0.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +96 -131
- package/claude/hooks/drive.ts +0 -3
- package/claude/hooks/register.tsx +3 -68
- package/claude/hooks/ship.ts +85 -1595
- package/dist/chunks/{reserved-D0MKVFFa.mjs → reserved-CQ7amcKG.mjs} +87 -133
- package/dist/chunks/{review-ZzUQH1KZ.mjs → review-CNgN_V8T.mjs} +2 -2
- package/dist/chunks/{tail-I1-vVq0P.mjs → tail-JZ9PAOwp.mjs} +2 -14
- package/dist/chunks/{wire-BJuZWR3C.mjs → wire-C12dahrH.mjs} +2 -130
- package/dist/flows.mjs +1 -16
- package/dist/gtd.bundle.mjs +476 -982
- package/dist/testkit.mjs +58 -417
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +159 -363
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +400 -675
- package/package.json +5 -4
- package/src/cli/Cli.test.ts +1 -86
- package/src/cli/Cli.ts +13 -133
- package/src/cli/Metrics.test.ts +21 -31
- package/src/cli/Metrics.ts +25 -36
- package/src/cli/Program.test.ts +1 -35
- package/src/cli/Program.ts +5 -64
- package/src/cli/ToolCommands.ts +0 -3
- package/src/edge/Edge.test.ts +20 -322
- package/src/edge/Edge.ts +10 -181
- package/src/edge/index.ts +0 -1
- package/src/flows/runtime.test.ts +0 -27
- package/src/flows/runtime.ts +1 -49
- package/src/install/Install.ts +15 -24
- package/src/replay/Replay.test.ts +0 -169
- package/src/replay/Replay.ts +4 -93
- package/src/replay/index.ts +0 -1
- package/src/testkit/Layers.ts +0 -1
- package/src/testkit/steps.ts +13 -0
- package/src/wire/BeatDocument.ts +2 -12
- package/src/wire/BeatStatus.ts +0 -3
- package/src/wire/WireSchema.test.ts +0 -18
- package/src/wire/index.ts +0 -2
- package/src/wire/types.ts +0 -7
- package/src/workflow/Workflow.ts +1 -9
- package/src/workflows/feature/acceptance-criteria.feature +159 -3
- package/src/workflows/feature/criteria.test.ts +23 -15
- package/src/workflows/feature/criteria.ts +72 -39
- package/src/workflows/feature/fast-path.feature +4 -0
- package/src/workflows/feature/feature.test.ts +6 -5
- package/src/workflows/feature/feature.ts +34 -118
- package/src/workflows/feature/flag-mode.feature +4 -0
- package/src/workflows/feature/hands-on-skip.feature +37 -65
- package/src/workflows/feature/index.ts +0 -1
- package/src/workflows/feature/lens-models.feature +4 -0
- package/src/workflows/feature/lens-review.feature +4 -0
- package/src/workflows/feature/naive-acceptance.feature +4 -0
- package/src/workflows/feature/one-step-land.feature +4 -4
- package/src/workflows/feature/planning.ts +2 -1
- package/src/workflows/feature/prototype-rounds.feature +5 -1
- package/src/workflows/feature/prototype.feature +11 -3
- package/src/workflows/feature/prototype.ts +18 -29
- package/src/workflows/feature/refactor-absorbs.feature +7 -8
- package/src/workflows/feature/refactor-first.feature +4 -5
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +4 -6
- package/src/workflows/feature/refactor-review.feature +31 -11
- package/src/workflows/feature/refactor.ts +30 -64
- package/src/workflows/feature/review-answers.feature +4 -0
- package/src/workflows/feature/review-gate.feature +13 -1
- package/src/workflows/feature/reviewer-walkthrough.feature +4 -0
- package/src/workflows/feature/risk-tier.feature +32 -0
- package/src/workflows/feature/seam-rejection.feature +8 -4
- package/src/workflows/feature/seam.feature +8 -0
- package/src/workflows/feature/seam.test.ts +11 -28
- package/src/workflows/feature/seam.ts +52 -165
- package/src/workflows/feature/severity-budget.feature +4 -0
- package/src/workflows/feature/severity-sorted-review.feature +4 -0
- package/src/workflows/feature/standards.feature +4 -0
- package/src/workflows/feature/tier-pin.feature +4 -0
- package/src/workflows/index.ts +0 -1
- package/src/workflows/shared/lenses.ts +0 -13
- package/src/workflows/shared/options.test.ts +2 -11
- package/src/workflows/shared/options.ts +4 -21
- package/src/workflows/shared/review.test.ts +3 -3
- package/src/workflows/shared/steps.ts +0 -2
- package/src/workflows/shared/text.test.ts +41 -71
- package/src/workflows/shared/text.ts +82 -212
- package/src/workflows/shared/vars.ts +2 -8
- package/src/workflows/shared/workflows.steps.ts +0 -13
- package/claude/hooks/context.ts +0 -66
- package/src/replay/Slices.test.ts +0 -73
- package/src/replay/Slices.ts +0 -45
- package/src/wire/ContextLimit.fixture.ts +0 -30
- package/src/wire/ContextLimit.test.ts +0 -96
- package/src/wire/ContextLimit.ts +0 -169
- package/src/wire/Slices.ts +0 -25
- package/src/wire/golden/1/slices.json +0 -11
- package/src/workflows/feature/context-limit.feature +0 -37
- package/src/workflows/feature/continuation-cap.feature +0 -65
- package/src/workflows/feature/continuation-rebuild.feature +0 -88
- package/src/workflows/feature/continuation-slices.feature +0 -64
- package/src/workflows/feature/continuation.feature +0 -205
- package/src/workflows/feature/slice-findings.feature +0 -252
- package/src/workflows/feature/slicing-cap.feature +0 -23
- package/src/workflows/feature/slicing.feature +0 -265
- package/src/workflows/feature/slicing.test.ts +0 -49
- package/src/workflows/feature/slicing.ts +0 -105
- package/src/workflows/shared/continuation.ts +0 -92
|
@@ -109,7 +109,8 @@ describe("naivePass", () => {
|
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
describe("naiveAcceptance and requireAcceptance", () => {
|
|
112
|
-
const requirements =
|
|
112
|
+
const requirements =
|
|
113
|
+
"# R\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: adds.\n- AC-2: subtracts.\n"
|
|
113
114
|
const file = (path: string, before: string | undefined, after: string): Change => ({
|
|
114
115
|
path,
|
|
115
116
|
status: before === undefined ? "added" : "modified",
|
|
@@ -170,7 +171,8 @@ describe("naiveAcceptance and requireAcceptance", () => {
|
|
|
170
171
|
})
|
|
171
172
|
|
|
172
173
|
describe("acceptanceFault", () => {
|
|
173
|
-
const requirements =
|
|
174
|
+
const requirements =
|
|
175
|
+
"# R\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: adds.\n- AC-2: subtracts.\n"
|
|
174
176
|
const file = (path: string, status: Change["status"], after: string): Change => ({
|
|
175
177
|
path,
|
|
176
178
|
status,
|
|
@@ -309,9 +311,6 @@ const reviewDoc = [
|
|
|
309
311
|
|
|
310
312
|
interface Seamed {
|
|
311
313
|
readonly names: string[]
|
|
312
|
-
readonly slices: string[]
|
|
313
|
-
/** Slice names entered, with the steps seen while inside each. */
|
|
314
|
-
readonly inside: Record<string, string[]>
|
|
315
314
|
readonly outcome: SeamOutcome | Error
|
|
316
315
|
}
|
|
317
316
|
|
|
@@ -325,10 +324,7 @@ const driveSeam = async (
|
|
|
325
324
|
let red = false
|
|
326
325
|
let redsLeft = reds
|
|
327
326
|
const names: string[] = []
|
|
328
|
-
const slices: string[] = []
|
|
329
|
-
const inside: Record<string, string[]> = {}
|
|
330
327
|
const stack: string[] = []
|
|
331
|
-
const open: string[] = []
|
|
332
328
|
const files = new Map<string, string>([["src/a.ts", "a"]])
|
|
333
329
|
let current = "c0"
|
|
334
330
|
const effects: Record<string, () => void> = {
|
|
@@ -349,12 +345,6 @@ const driveSeam = async (
|
|
|
349
345
|
},
|
|
350
346
|
pushScope: (scope: { name?: string }) => void stack.push(scope.name ?? ""),
|
|
351
347
|
popScope: () => void stack.pop(),
|
|
352
|
-
enterSlice: (name: string) => {
|
|
353
|
-
slices.push(name)
|
|
354
|
-
open.push(name)
|
|
355
|
-
inside[name] = []
|
|
356
|
-
},
|
|
357
|
-
leaveSlice: () => void open.pop(),
|
|
358
348
|
read: (path: string) => files.get(path),
|
|
359
349
|
head: () => current,
|
|
360
350
|
kept: () => tip(stray),
|
|
@@ -370,7 +360,6 @@ const driveSeam = async (
|
|
|
370
360
|
}
|
|
371
361
|
if (full === "seam.clear") files.delete(".gtd/FEEDBACK.md")
|
|
372
362
|
names.push(full)
|
|
373
|
-
for (const name of open) inside[name]!.push(full)
|
|
374
363
|
effects[request.name]?.()
|
|
375
364
|
if (request.kind === "judge")
|
|
376
365
|
return Promise.resolve({ answers: { shape: { answer, p: 0.9 } }, truncated: [] })
|
|
@@ -383,14 +372,13 @@ const driveSeam = async (
|
|
|
383
372
|
} catch (error) {
|
|
384
373
|
outcome = error as Error
|
|
385
374
|
}
|
|
386
|
-
return { names,
|
|
375
|
+
return { names, outcome }
|
|
387
376
|
}
|
|
388
377
|
|
|
389
378
|
describe("seamBuild", () => {
|
|
390
|
-
it("land resolves the kept tip to land, with the caller's base
|
|
379
|
+
it("land resolves the kept tip to land, with the caller's base", async () => {
|
|
391
380
|
const h = await driveSeam("land")
|
|
392
381
|
expect(h.outcome).toEqual({ reference: "abc", base: "c-base", land: true })
|
|
393
|
-
expect(h.slices).toEqual([])
|
|
394
382
|
expect(h.names).toEqual(["seam.naive", "seam.check", "seam.judge"])
|
|
395
383
|
})
|
|
396
384
|
|
|
@@ -430,7 +418,7 @@ describe("seamBuild", () => {
|
|
|
430
418
|
expect(h.outcome).toEqual({ reference: "abc", base: "c-base", land: true })
|
|
431
419
|
})
|
|
432
420
|
|
|
433
|
-
it("refactor-first runs the reviewed refactor
|
|
421
|
+
it("refactor-first runs the reviewed refactor right after the note and resolves HEAD after it", async () => {
|
|
434
422
|
const h = await driveSeam("refactor-first")
|
|
435
423
|
expect(h.outcome).toEqual({
|
|
436
424
|
reference: "abc",
|
|
@@ -438,16 +426,14 @@ describe("seamBuild", () => {
|
|
|
438
426
|
refactored: true,
|
|
439
427
|
rebuild: { note: "Seam: a shared op", acceptance: { hash: "abc", tests: [], ids: [] } },
|
|
440
428
|
})
|
|
441
|
-
expect(h.slices).toEqual(["refactor"])
|
|
442
429
|
expect(h.names.indexOf("seam.note")).toBe(h.names.indexOf("seam.judge") + 1)
|
|
443
|
-
expect(h.
|
|
444
|
-
expect(h.
|
|
445
|
-
expect(h.inside.refactor).toContain("refactor.build.review.closing")
|
|
430
|
+
expect(h.names.indexOf("refactor.deriving")).toBe(h.names.indexOf("seam.note") + 1)
|
|
431
|
+
expect(h.names).toContain("refactor.build.review.closing")
|
|
446
432
|
expect(h.names.some((n) => n.includes("approve"))).toBe(false)
|
|
447
433
|
expect(h.names.some((n) => n.startsWith("implement"))).toBe(false)
|
|
448
434
|
})
|
|
449
435
|
|
|
450
|
-
it("refactor-first with only test-file strays resolves the caller's base with no
|
|
436
|
+
it("refactor-first with only test-file strays resolves the caller's base with no refactor and no by-hand", async () => {
|
|
451
437
|
const h = await driveSeam("refactor-first", change("src/a.test.ts", "modified"), {
|
|
452
438
|
seamAllow: ".gtd/**",
|
|
453
439
|
})
|
|
@@ -456,7 +442,6 @@ describe("seamBuild", () => {
|
|
|
456
442
|
base: "c-base",
|
|
457
443
|
rebuild: { acceptance: { hash: "abc", tests: [], ids: [] } },
|
|
458
444
|
})
|
|
459
|
-
expect(h.slices).toEqual([])
|
|
460
445
|
expect(h.names).not.toContain("seam.by-hand")
|
|
461
446
|
expect(h.names.some((n) => n.startsWith("refactor"))).toBe(false)
|
|
462
447
|
})
|
|
@@ -467,18 +452,16 @@ describe("seamBuild", () => {
|
|
|
467
452
|
{ path: "src/a.test.ts", status: "modified", before: undefined, after: "b" },
|
|
468
453
|
{ seamAllow: ".gtd/**" },
|
|
469
454
|
)
|
|
470
|
-
expect(h.slices).toEqual([])
|
|
471
455
|
expect(h.names).toContain("seam.by-hand")
|
|
472
456
|
})
|
|
473
457
|
|
|
474
|
-
it("rests unreadable strays at seam.by-hand and
|
|
458
|
+
it("rests unreadable strays at seam.by-hand and runs no refactor", async () => {
|
|
475
459
|
const h = await driveSeam("refactor-first", {
|
|
476
460
|
path: "src/a.ts",
|
|
477
461
|
status: "modified",
|
|
478
462
|
before: undefined,
|
|
479
463
|
after: "b",
|
|
480
464
|
})
|
|
481
|
-
expect(h.slices).toEqual([])
|
|
482
465
|
expect(h.names).toContain("seam.by-hand")
|
|
483
466
|
expect(h.outcome).toEqual({
|
|
484
467
|
reference: "abc",
|
|
@@ -17,36 +17,18 @@ import {
|
|
|
17
17
|
removeScript,
|
|
18
18
|
run,
|
|
19
19
|
scope,
|
|
20
|
-
slice as sliced,
|
|
21
|
-
start,
|
|
22
20
|
vars,
|
|
23
21
|
wrote,
|
|
24
22
|
type Change,
|
|
25
23
|
type DiffShapeResult,
|
|
26
24
|
type KeptTip,
|
|
27
25
|
} from "../../flows/index.js"
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
coder,
|
|
31
|
-
FEEDBACK,
|
|
32
|
-
HANDOFF,
|
|
33
|
-
PROTOTYPE,
|
|
34
|
-
REFACTOR_NOTE,
|
|
35
|
-
REQUIREMENTS,
|
|
36
|
-
SLICES,
|
|
37
|
-
} from "../shared/steps.js"
|
|
26
|
+
import { coder, FEEDBACK, PROTOTYPE, REFACTOR_NOTE, REQUIREMENTS } from "../shared/steps.js"
|
|
38
27
|
import * as t from "../shared/text.js"
|
|
39
28
|
import { defaults } from "../shared/vars.js"
|
|
40
29
|
import { green } from "./green.js"
|
|
41
30
|
import { prototypeTest } from "./prototype.js"
|
|
42
|
-
import {
|
|
43
|
-
criterionIds,
|
|
44
|
-
requireCriteria,
|
|
45
|
-
requireCriteriaFrozen,
|
|
46
|
-
requireCriteriaIfEdited,
|
|
47
|
-
sliceIds,
|
|
48
|
-
type CriteriaSlice,
|
|
49
|
-
} from "./criteria.js"
|
|
31
|
+
import { criterionIds, requireCriteria, requireCriteriaFrozen } from "./criteria.js"
|
|
50
32
|
import {
|
|
51
33
|
COMMENT,
|
|
52
34
|
holdsCallSites,
|
|
@@ -72,47 +54,14 @@ export const judgeSeam = (tip: KeptTip): Promise<DiffShapeResult> =>
|
|
|
72
54
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path),
|
|
73
55
|
})
|
|
74
56
|
|
|
75
|
-
export const naivePass = (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
from?: Handoff,
|
|
79
|
-
): Promise<void> =>
|
|
80
|
-
t.agentWithSkills("naive", t.naivePassPrompt(retry, sliceIds(slice), from), {
|
|
81
|
-
label:
|
|
82
|
-
from !== undefined
|
|
83
|
-
? "Continuing the naive pass"
|
|
84
|
-
: retry === undefined
|
|
85
|
-
? "Writing the naive pass"
|
|
86
|
-
: "Rewriting the naive pass",
|
|
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",
|
|
87
60
|
keep: true,
|
|
88
61
|
model: coder(),
|
|
89
62
|
system: t.builderSystem(),
|
|
90
63
|
})
|
|
91
64
|
|
|
92
|
-
const handoffOf = (tip: KeptTip, slice: CriteriaSlice | undefined): Handoff | undefined => {
|
|
93
|
-
const note = tip.changes.find((c) => c.path === HANDOFF && c.after !== undefined)
|
|
94
|
-
if (note === undefined) return undefined
|
|
95
|
-
return {
|
|
96
|
-
snapshot: tip.hash,
|
|
97
|
-
note: note.after!.trim(),
|
|
98
|
-
open: unnamed(idsOf(REQUIREMENTS, slice), acceptanceTests(tip.changes)),
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** A naive pass, continued until a pass ends without a handoff note: the snapshot is its kept tip. */
|
|
103
|
-
const naiveContinued = (
|
|
104
|
-
retry: t.NaiveRetry | undefined,
|
|
105
|
-
slice: CriteriaSlice | undefined,
|
|
106
|
-
naive: Phase,
|
|
107
|
-
) =>
|
|
108
|
-
continuing(naive, async (from, fresh) => {
|
|
109
|
-
const tip = await fresh(async () => {
|
|
110
|
-
await naivePass(retry, slice, from)
|
|
111
|
-
return kept("naive")!
|
|
112
|
-
})
|
|
113
|
-
return { result: tip, handoff: handoffOf(tip, slice) }
|
|
114
|
-
})
|
|
115
|
-
|
|
116
65
|
// Only a string an import, `require` or `mock` call takes: a URL route or path-like data is not one.
|
|
117
66
|
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g
|
|
118
67
|
const EXTENSION = /\.[\w-]+$/
|
|
@@ -158,8 +107,7 @@ const addedLines = (change: Change): string => {
|
|
|
158
107
|
const acceptanceTests = (changes: readonly Change[]): readonly Change[] =>
|
|
159
108
|
changes.filter((c) => c.after !== undefined && isCode(c.path) && isTestFile(c.path))
|
|
160
109
|
|
|
161
|
-
const idsOf = (file: string
|
|
162
|
-
criterionIds(read(file) ?? "", slice)
|
|
110
|
+
const idsOf = (file: string): string[] => criterionIds(read(file) ?? "")
|
|
163
111
|
|
|
164
112
|
// One rule for both gates, so a naive test carried over verbatim always satisfies the rebuild's.
|
|
165
113
|
const unnamed = (ids: readonly string[], tests: readonly Change[]): string[] => {
|
|
@@ -172,13 +120,9 @@ const unnamed = (ids: readonly string[], tests: readonly Change[]): string[] =>
|
|
|
172
120
|
* test line of the pass names, or a test importing a file the pass added —
|
|
173
121
|
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
174
122
|
*/
|
|
175
|
-
export const acceptanceFault = (
|
|
176
|
-
file: string,
|
|
177
|
-
tip: KeptTip,
|
|
178
|
-
slice?: CriteriaSlice,
|
|
179
|
-
): string | undefined => {
|
|
123
|
+
export const acceptanceFault = (file: string, tip: KeptTip): string | undefined => {
|
|
180
124
|
const tests = acceptanceTests(tip.changes)
|
|
181
|
-
const untested = unnamed(idsOf(file
|
|
125
|
+
const untested = unnamed(idsOf(file), tests)
|
|
182
126
|
if (untested.length > 0)
|
|
183
127
|
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]}] …"`
|
|
184
128
|
const added = tip.changes
|
|
@@ -198,8 +142,8 @@ export const acceptanceFault = (
|
|
|
198
142
|
}
|
|
199
143
|
|
|
200
144
|
/** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
|
|
201
|
-
export const naiveAcceptance = (tip: KeptTip
|
|
202
|
-
const ids = idsOf(REQUIREMENTS
|
|
145
|
+
export const naiveAcceptance = (tip: KeptTip): t.Acceptance => {
|
|
146
|
+
const ids = idsOf(REQUIREMENTS)
|
|
203
147
|
const perTest = acceptanceTests(tip.changes).map((c) => {
|
|
204
148
|
const lines = addedLines(c)
|
|
205
149
|
return { path: c.path, ids: ids.filter((id) => named(id, lines)) }
|
|
@@ -281,18 +225,14 @@ export interface ImplementOptions {
|
|
|
281
225
|
readonly reference?: string | undefined
|
|
282
226
|
readonly tier?: RiskTier | undefined
|
|
283
227
|
readonly amend?: boolean | undefined
|
|
284
|
-
/** After a refactor
|
|
228
|
+
/** After a refactor, which may already carry the feature: an empty turn completes the step. */
|
|
285
229
|
readonly afterRefactor?: boolean | undefined
|
|
286
230
|
/** A kept naive tip to apply as the implementation; built fresh only when it no longer applies. */
|
|
287
231
|
readonly naive?: string | undefined
|
|
288
232
|
/** A rebuild after a refactor-first judgment: the note it plugs into, the acceptance tests it must carry. */
|
|
289
233
|
readonly rebuild?: t.Rebuild | undefined
|
|
290
|
-
/** The criteria this slice builds, when the change has more than one slice. */
|
|
291
|
-
readonly slice?: CriteriaSlice | undefined
|
|
292
234
|
/** @deprecated Ignored since the coverage judge is gone. */
|
|
293
235
|
readonly base?: string | undefined
|
|
294
|
-
/** Not the last slice: `.gtd/REQUIREMENTS.md` and `.gtd/SLICES.md` stay for the next one. */
|
|
295
|
-
readonly more?: boolean | (() => boolean) | undefined
|
|
296
236
|
}
|
|
297
237
|
|
|
298
238
|
/** Resolves `false` when the tip no longer applies (the failure is in `.gtd/FEEDBACK.md`) or has nothing outside `.gtd/` to apply. */
|
|
@@ -304,54 +244,31 @@ const applied = async (tip: string): Promise<boolean> => {
|
|
|
304
244
|
}
|
|
305
245
|
|
|
306
246
|
/** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
|
|
307
|
-
const settled = (
|
|
308
|
-
|
|
309
|
-
slice: CriteriaSlice | undefined,
|
|
310
|
-
): readonly string[] => {
|
|
311
|
-
const listed = idsOf(REQUIREMENTS, slice)
|
|
312
|
-
if (carried === undefined) return listed
|
|
247
|
+
const settled = (carried: readonly string[]): readonly string[] => {
|
|
248
|
+
const listed = idsOf(REQUIREMENTS)
|
|
313
249
|
return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id))
|
|
314
250
|
}
|
|
315
251
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
system: t.builderSystem(),
|
|
337
|
-
...(options.afterRefactor === true ? { allowEmpty: true } : {}),
|
|
338
|
-
},
|
|
339
|
-
),
|
|
340
|
-
)
|
|
341
|
-
requireCriteriaFrozen(REQUIREMENTS)
|
|
342
|
-
// Committed by the snapshot, so a turn stopped again may leave it as it was: present means stopped.
|
|
343
|
-
const note = read(HANDOFF)
|
|
344
|
-
if (note === undefined) return { result: undefined }
|
|
345
|
-
const ids = settled(options.rebuild?.acceptance?.ids, options.slice)
|
|
346
|
-
return {
|
|
347
|
-
result: undefined,
|
|
348
|
-
handoff: {
|
|
349
|
-
snapshot: head(),
|
|
350
|
-
note: note.trim(),
|
|
351
|
-
open: unnamed(ids, acceptanceTests(changesSince(start()))),
|
|
352
|
-
},
|
|
353
|
-
}
|
|
354
|
-
})
|
|
252
|
+
const build = async (options: ImplementOptions): Promise<void> => {
|
|
253
|
+
await t.agentWithSkills(
|
|
254
|
+
"building",
|
|
255
|
+
t.implementBuildingPrompt(
|
|
256
|
+
options.reference,
|
|
257
|
+
options.tier,
|
|
258
|
+
options.amend,
|
|
259
|
+
options.afterRefactor,
|
|
260
|
+
flagModeOn(),
|
|
261
|
+
options.rebuild,
|
|
262
|
+
),
|
|
263
|
+
{
|
|
264
|
+
label: "Implementing",
|
|
265
|
+
model: coder(),
|
|
266
|
+
system: t.builderSystem(),
|
|
267
|
+
...(options.afterRefactor === true ? { allowEmpty: true } : {}),
|
|
268
|
+
},
|
|
269
|
+
)
|
|
270
|
+
requireCriteriaFrozen(REQUIREMENTS)
|
|
271
|
+
}
|
|
355
272
|
|
|
356
273
|
export function implement(options?: ImplementOptions): Promise<void>
|
|
357
274
|
/** @deprecated Pass `implement({ reference, tier, amend })`. */
|
|
@@ -378,38 +295,21 @@ export const implementing = (options: ImplementOptions): Promise<readonly string
|
|
|
378
295
|
const built = options.naive === undefined || !(await applied(options.naive))
|
|
379
296
|
if (built) await build(options)
|
|
380
297
|
else requireCriteriaFrozen(REQUIREMENTS)
|
|
381
|
-
// Only the criteria
|
|
298
|
+
// Only the criteria the requirements still list.
|
|
382
299
|
if (built && acceptance !== undefined)
|
|
383
|
-
requireAcceptance({ ...acceptance, ids: settled(acceptance.ids
|
|
300
|
+
requireAcceptance({ ...acceptance, ids: settled(acceptance.ids) }, since)
|
|
384
301
|
// Fix turns are held to the carried tests too: deleting a red one must not buy a green suite.
|
|
385
302
|
const keepsTests =
|
|
386
303
|
built && acceptance !== undefined ? () => requireAcceptanceKept(acceptance) : undefined
|
|
387
304
|
await green(keepsTests)
|
|
388
|
-
await prototypeTest(
|
|
305
|
+
await prototypeTest(keepsTests)
|
|
389
306
|
const ids = criterionIds(read(REQUIREMENTS) ?? "")
|
|
390
|
-
await closing(
|
|
307
|
+
await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE, PROTOTYPE]), {
|
|
308
|
+
label: "Closing out the implementation",
|
|
309
|
+
})
|
|
391
310
|
return ids
|
|
392
311
|
})
|
|
393
312
|
|
|
394
|
-
// Read at closing: a narrowing at `implement.oversized` may have emptied every later slice.
|
|
395
|
-
const moreToBuild = (more: ImplementOptions["more"]): boolean =>
|
|
396
|
-
typeof more === "function" ? more() : more === true
|
|
397
|
-
|
|
398
|
-
/** The last slice's closing, for a last slice a steer left with no criterion to build. */
|
|
399
|
-
export const closeOut = (): Promise<void> => scope("implement", () => closing(false))
|
|
400
|
-
|
|
401
|
-
/** A slice with `more` after it keeps the criteria and the slicing for the next one. */
|
|
402
|
-
const closing = (more: boolean): Promise<void> =>
|
|
403
|
-
run(
|
|
404
|
-
"closing",
|
|
405
|
-
removeScript(
|
|
406
|
-
more
|
|
407
|
-
? [REFACTOR_NOTE, HANDOFF, PROTOTYPE]
|
|
408
|
-
: [REQUIREMENTS, REFACTOR_NOTE, SLICES, HANDOFF, PROTOTYPE],
|
|
409
|
-
),
|
|
410
|
-
{ label: "Closing out the implementation" },
|
|
411
|
-
)
|
|
412
|
-
|
|
413
313
|
interface Decision {
|
|
414
314
|
readonly reference: string
|
|
415
315
|
readonly tip: KeptTip
|
|
@@ -459,12 +359,8 @@ interface Vetting {
|
|
|
459
359
|
}
|
|
460
360
|
|
|
461
361
|
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
462
|
-
const vetted = async (
|
|
463
|
-
|
|
464
|
-
vetting: Vetting,
|
|
465
|
-
slice: CriteriaSlice | undefined,
|
|
466
|
-
): Promise<Verdict | undefined> => {
|
|
467
|
-
const fault = vetting.waived ? undefined : acceptanceFault(REQUIREMENTS, tip, slice)
|
|
362
|
+
const vetted = async (tip: KeptTip, vetting: Vetting): Promise<Verdict | undefined> => {
|
|
363
|
+
const fault = vetting.waived ? undefined : acceptanceFault(REQUIREMENTS, tip)
|
|
468
364
|
if (fault !== undefined) {
|
|
469
365
|
const verdict = await vetting.again({ fault, hash: tip.hash })
|
|
470
366
|
if (verdict.kind !== "overruled") return verdict
|
|
@@ -484,15 +380,15 @@ const ruled = async (tip: KeptTip, again: Vetting["again"]): Promise<Decision |
|
|
|
484
380
|
return next.kind === "overruled" ? { reference: tip.hash, tip } : next
|
|
485
381
|
}
|
|
486
382
|
|
|
487
|
-
const decide = (
|
|
383
|
+
const decide = (): Promise<Decision> =>
|
|
488
384
|
scope("seam", async () => {
|
|
489
385
|
const vetting: Vetting = { again: budget(), waived: false }
|
|
490
|
-
const naive = phase("naive pass", requireCriteriaIfEdited)
|
|
491
386
|
let retry: t.NaiveRetry | undefined
|
|
492
387
|
for (;;) {
|
|
493
|
-
|
|
388
|
+
await naivePass(retry)
|
|
389
|
+
const tip = kept("naive")!
|
|
494
390
|
const outcome =
|
|
495
|
-
(await vetted(tip, vetting
|
|
391
|
+
(await vetted(tip, vetting)) ??
|
|
496
392
|
(flagModeOn() ? await ruled(tip, vetting.again) : { reference: tip.hash, tip })
|
|
497
393
|
if (!("kind" in outcome)) return outcome
|
|
498
394
|
retry = outcome.kind === "retry" ? outcome.retry : undefined
|
|
@@ -504,7 +400,7 @@ export interface SeamOutcome {
|
|
|
504
400
|
readonly reference: string
|
|
505
401
|
/** The naive pass lands as it is: no build turn. */
|
|
506
402
|
readonly land?: boolean
|
|
507
|
-
/** The feature
|
|
403
|
+
/** The feature's base: HEAD after the refactor on refactor-first, else the caller's base. */
|
|
508
404
|
readonly base: string
|
|
509
405
|
readonly refactored?: boolean
|
|
510
406
|
/** Set on refactor-first: what the rebuild carries over from the naive pass (the note only after a refactor). */
|
|
@@ -533,14 +429,8 @@ export const refactorNote = async (
|
|
|
533
429
|
return note
|
|
534
430
|
}
|
|
535
431
|
|
|
536
|
-
export
|
|
537
|
-
|
|
538
|
-
readonly criteria?: CriteriaSlice | undefined
|
|
539
|
-
readonly refactor?: string | undefined
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
export const seamBuild = async (base: string, slice: SeamSlice = {}): Promise<SeamOutcome> => {
|
|
543
|
-
const { reference, tip, shape } = await decide(slice.criteria)
|
|
432
|
+
export const seamBuild = async (base: string): Promise<SeamOutcome> => {
|
|
433
|
+
const { reference, tip, shape } = await decide()
|
|
544
434
|
if (shape === undefined) return { reference, base, land: true }
|
|
545
435
|
if (shape.unreadable.length > 0) {
|
|
546
436
|
await scope("seam", () =>
|
|
@@ -550,20 +440,17 @@ export const seamBuild = async (base: string, slice: SeamSlice = {}): Promise<Se
|
|
|
550
440
|
acceptClean: true,
|
|
551
441
|
}),
|
|
552
442
|
)
|
|
553
|
-
return { reference, base, rebuild: { acceptance: naiveAcceptance(tip
|
|
443
|
+
return { reference, base, rebuild: { acceptance: naiveAcceptance(tip) } }
|
|
554
444
|
}
|
|
555
445
|
const strays = refactorStrays(shape)
|
|
556
|
-
if (strays.length === 0)
|
|
557
|
-
return { reference, base, rebuild: { acceptance: naiveAcceptance(tip, slice.criteria) } }
|
|
446
|
+
if (strays.length === 0) return { reference, base, rebuild: { acceptance: naiveAcceptance(tip) } }
|
|
558
447
|
const note = await scope("seam", () => refactorNote(reference, strays))
|
|
559
|
-
const refactored = await
|
|
560
|
-
reviewedRefactor(shape, { reference, note }),
|
|
561
|
-
)
|
|
448
|
+
const refactored = await reviewedRefactor(shape, { reference, note })
|
|
562
449
|
return {
|
|
563
450
|
reference,
|
|
564
451
|
base: head(),
|
|
565
452
|
refactored,
|
|
566
453
|
// Fixed at `seam.note`, like the package's scope: a later edit of the file reaches neither turn.
|
|
567
|
-
rebuild: { note, acceptance: naiveAcceptance(tip
|
|
454
|
+
rebuild: { note, acceptance: naiveAcceptance(tip) },
|
|
568
455
|
}
|
|
569
456
|
}
|
package/src/workflows/index.ts
CHANGED
|
@@ -41,19 +41,6 @@ export interface LensReport {
|
|
|
41
41
|
|
|
42
42
|
export const NO_FINDINGS: LensReport = { notes: [], fixed: [], noise: [] }
|
|
43
43
|
|
|
44
|
-
/** `reports` as one, each finding labelled with the slice it came from, in slice order. */
|
|
45
|
-
export const joinReports = (
|
|
46
|
-
reports: readonly { readonly slice: string; readonly report: LensReport }[],
|
|
47
|
-
): LensReport => {
|
|
48
|
-
const tag = <F extends Origin>(slice: string, fs: readonly F[]): F[] =>
|
|
49
|
-
fs.map((f) => ({ ...f, slice }))
|
|
50
|
-
return {
|
|
51
|
-
notes: reports.flatMap((r) => tag(r.slice, r.report.notes)),
|
|
52
|
-
fixed: reports.flatMap((r) => tag(r.slice, r.report.fixed)),
|
|
53
|
-
noise: reports.flatMap((r) => tag(r.slice, r.report.noise)),
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
44
|
const UNTITLED = "Quality findings"
|
|
58
45
|
|
|
59
46
|
const below = (section: string): string => section.split("\n").slice(1).join("\n").trim()
|
|
@@ -8,9 +8,8 @@ const { defaults, envDefaults, feature, fix, review, ShippedOptions } = workflow
|
|
|
8
8
|
const { flagShipped } = options
|
|
9
9
|
|
|
10
10
|
describe("the shipped workflow factories", () => {
|
|
11
|
-
it("take exactly the settings the shipped workflows declare as options
|
|
12
|
-
|
|
13
|
-
expect(settings.sort()).toEqual(
|
|
11
|
+
it("take exactly the settings the shipped workflows declare as options", () => {
|
|
12
|
+
expect(Object.keys(ShippedOptions.fields).sort()).toEqual(
|
|
14
13
|
[...Object.keys(defaults), ...Object.keys(envDefaults)]
|
|
15
14
|
.filter((name) => !name.startsWith("flag_"))
|
|
16
15
|
.sort(),
|
|
@@ -24,14 +23,6 @@ describe("the shipped workflow factories", () => {
|
|
|
24
23
|
expect(defaults).toHaveProperty(`flag_${name}`, defaultOn ? "1" : "0")
|
|
25
24
|
})
|
|
26
25
|
|
|
27
|
-
it("keep contextLimit out of the settings", () => {
|
|
28
|
-
const built = configure(feature({ contextLimit: { hard: 0.8 } }))
|
|
29
|
-
expect(built).toMatchObject({ ok: true })
|
|
30
|
-
const build = built?.ok === true ? built.build : undefined
|
|
31
|
-
expect(build?.defaults).not.toHaveProperty("contextLimit")
|
|
32
|
-
expect(build?.envDefaults).not.toHaveProperty("contextLimit")
|
|
33
|
-
})
|
|
34
|
-
|
|
35
26
|
it("declare a seamAllow default a factory option overrides", () => {
|
|
36
27
|
expect(configure(feature({ seamAllow: "docs/**" }))).toMatchObject({
|
|
37
28
|
ok: true,
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
type WorkflowDoors,
|
|
7
7
|
type WorkflowFactory,
|
|
8
8
|
refuse,
|
|
9
|
-
scope,
|
|
10
9
|
vars,
|
|
11
10
|
} from "../../flows/index.js"
|
|
12
11
|
import { defaultFlagRegistry, defaults, envDefaults } from "./vars.js"
|
|
@@ -23,23 +22,13 @@ export const ShippedOptions = Schema.Struct({
|
|
|
23
22
|
standardMinP: setting,
|
|
24
23
|
refactorFlagPattern: setting,
|
|
25
24
|
seamAllow: setting,
|
|
26
|
-
sliceMaxCriteria: setting,
|
|
27
25
|
qualityReviews: setting,
|
|
28
26
|
refactorQualityReviews: setting,
|
|
29
|
-
continuations: setting,
|
|
30
27
|
prototypeRounds: setting,
|
|
31
|
-
continuationStall: setting,
|
|
32
28
|
testCommand: setting,
|
|
33
29
|
plannerModel: setting,
|
|
34
30
|
coderModel: setting,
|
|
35
31
|
flagMode: Schema.optionalWith(Schema.Boolean, { exact: true }),
|
|
36
|
-
contextLimit: Schema.optionalWith(
|
|
37
|
-
Schema.Struct({
|
|
38
|
-
soft: Schema.optionalWith(Schema.Number, { exact: true }),
|
|
39
|
-
hard: Schema.optionalWith(Schema.Number, { exact: true }),
|
|
40
|
-
}),
|
|
41
|
-
{ exact: true },
|
|
42
|
-
),
|
|
43
32
|
})
|
|
44
33
|
|
|
45
34
|
export type ShippedOptions = Schema.Schema.Encoded<typeof ShippedOptions>
|
|
@@ -64,12 +53,6 @@ export const flagModeOn = (): boolean => {
|
|
|
64
53
|
return value === "true"
|
|
65
54
|
}
|
|
66
55
|
|
|
67
|
-
// Not a setting: a factory option is known at load, so the scope wraps the
|
|
68
|
-
// whole flow without reading a setting before its first step. The engine
|
|
69
|
-
// refuses limits it cannot use when it reaches an agent step.
|
|
70
|
-
const limited = (flow: Flow, context: ShippedOptions["contextLimit"]): Flow =>
|
|
71
|
-
context === undefined ? flow : () => scope({ context }, flow)
|
|
72
|
-
|
|
73
56
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
74
57
|
export const shipped = (
|
|
75
58
|
flow: Flow,
|
|
@@ -78,8 +61,8 @@ export const shipped = (
|
|
|
78
61
|
): WorkflowFactory<ShippedOptions> =>
|
|
79
62
|
defineWorkflow(
|
|
80
63
|
Schema.standardSchemaV1(ShippedOptions, { onExcessProperty: "error", errors: "all" }),
|
|
81
|
-
(
|
|
82
|
-
flow
|
|
64
|
+
(options) => ({
|
|
65
|
+
flow,
|
|
83
66
|
defaults: over(defaults, options),
|
|
84
67
|
envDefaults: over(envDefaults, options),
|
|
85
68
|
doors,
|
|
@@ -97,10 +80,10 @@ export const flagShipped = (
|
|
|
97
80
|
): WorkflowFactory<FlagOptions> =>
|
|
98
81
|
defineWorkflow(
|
|
99
82
|
Schema.standardSchemaV1(FlagOptions, { onExcessProperty: "error", errors: "all" }),
|
|
100
|
-
(
|
|
83
|
+
(options) => {
|
|
101
84
|
const flagDefaults = { ...defaults, flag: "", flagRegistry: defaultFlagRegistry }
|
|
102
85
|
return {
|
|
103
|
-
flow
|
|
86
|
+
flow,
|
|
104
87
|
defaults: over(flagDefaults, options),
|
|
105
88
|
envDefaults: over(envDefaults, options),
|
|
106
89
|
doors: { [name]: { args: [{ name: "flag" }], vars: ({ flag }) => ({ flag: flag ?? "" }) } },
|