@pmelab/gtd 29.1.0 → 29.2.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 +17 -9
- package/dist/chunks/{reserved-DoHUII2D.mjs → reserved-BzeofpoC.mjs} +25 -9
- package/dist/chunks/{tail-C6YBG5zh.mjs → tail-eS_ZSImR.mjs} +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +128 -41
- 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 +151 -48
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +13 -5
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/feature.ts +6 -5
- package/src/workflows/feature/index.ts +1 -1
- package/src/workflows/feature/packages.test.ts +299 -5
- package/src/workflows/feature/packages.ts +109 -26
- package/src/workflows/feature/refactor.ts +9 -2
- package/src/workflows/feature/review.test.ts +103 -3
- package/src/workflows/feature/review.ts +34 -8
- package/src/workflows/feature/seam.test.ts +15 -1
- package/src/workflows/feature/seam.ts +28 -8
- package/src/workflows/index.ts +1 -1
- package/src/workflows/shared/options.test.ts +9 -3
- package/src/workflows/shared/text.test.ts +47 -3
- package/src/workflows/shared/text.ts +36 -14
|
@@ -256,6 +256,10 @@ export interface FeedbackTarget {
|
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
export interface BuildTailOptions {
|
|
259
|
+
/** Fix first when entered red; default `false`. */
|
|
260
|
+
readonly fixFirst?: boolean | undefined
|
|
261
|
+
/** Asserts the tree is already green, so the tail skips keep-green; default `false`. */
|
|
262
|
+
readonly green?: boolean | undefined
|
|
259
263
|
/** Runs after every code-changing turn: fix-check, quality fix, risk fix, nit fix, escalation describe. */
|
|
260
264
|
readonly guard?: Guard
|
|
261
265
|
/** `false`: `guard` skips the escalation's human rests. */
|
|
@@ -274,22 +278,44 @@ export interface BuildTailOptions {
|
|
|
274
278
|
}
|
|
275
279
|
|
|
276
280
|
/** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
|
|
277
|
-
export
|
|
281
|
+
export function buildTail(base: string, options?: BuildTailOptions): Promise<kit.ReviewOutcome>
|
|
282
|
+
/** @deprecated Pass `buildTail(base, { fixFirst, green, ...options })`. */
|
|
283
|
+
export function buildTail(
|
|
278
284
|
fixFirst: boolean,
|
|
279
285
|
base: string,
|
|
280
|
-
green
|
|
281
|
-
options
|
|
282
|
-
): Promise<kit.ReviewOutcome>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
green?: boolean,
|
|
287
|
+
options?: Omit<BuildTailOptions, "fixFirst" | "green">,
|
|
288
|
+
): Promise<kit.ReviewOutcome>
|
|
289
|
+
export function buildTail(
|
|
290
|
+
first: boolean | string,
|
|
291
|
+
second?: string | BuildTailOptions,
|
|
292
|
+
third = false,
|
|
293
|
+
fourth: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
|
|
294
|
+
): Promise<kit.ReviewOutcome> {
|
|
295
|
+
// Before any step runs, so a bad call never leaves a half-walked flow.
|
|
296
|
+
if (typeof first !== "string" && typeof first !== "boolean" && typeof second !== "string") {
|
|
297
|
+
return Promise.reject(
|
|
298
|
+
new Error(
|
|
299
|
+
"buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither",
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
const [base, options]: [string, BuildTailOptions] =
|
|
304
|
+
typeof first === "string"
|
|
305
|
+
? [first, (second as BuildTailOptions | undefined) ?? {}]
|
|
306
|
+
: [second as string, { ...fourth, fixFirst: first, green: third }]
|
|
307
|
+
const { fixFirst, green, ...reviewOptions } = options
|
|
308
|
+
return tail(
|
|
309
|
+
fixFirst ?? false,
|
|
310
|
+
(escalations) => review(base, escalations, reviewOptions),
|
|
311
|
+
green ?? false,
|
|
287
312
|
options.guard,
|
|
288
313
|
options.lenses,
|
|
289
314
|
options.guardRests,
|
|
290
315
|
options.lensBase,
|
|
291
316
|
options.rules,
|
|
292
317
|
)
|
|
318
|
+
}
|
|
293
319
|
|
|
294
320
|
/**
|
|
295
321
|
* Undo the human's review-round code edits. A path changed since the human
|
|
@@ -225,6 +225,7 @@ describe("implement", () => {
|
|
|
225
225
|
reference: string | undefined,
|
|
226
226
|
tier?: "low" | "high",
|
|
227
227
|
amend?: boolean,
|
|
228
|
+
call: () => Promise<void> = () => implement({ reference, tier, amend }),
|
|
228
229
|
) => {
|
|
229
230
|
const requests: StepRequest[] = []
|
|
230
231
|
installContext(
|
|
@@ -241,12 +242,25 @@ describe("implement", () => {
|
|
|
241
242
|
},
|
|
242
243
|
),
|
|
243
244
|
)
|
|
244
|
-
await
|
|
245
|
+
await call()
|
|
245
246
|
const request = requests.find((r) => r.kind === "agent" && r.name === "building")
|
|
246
247
|
if (request?.kind !== "agent") throw new Error("no building turn")
|
|
247
248
|
return request.prompt
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
it("the deprecated positional form renders the same prompt as the options form", async () => {
|
|
252
|
+
const modern = await building("abc", "low")
|
|
253
|
+
const legacy = await building("abc", "low", undefined, () => implement("abc", "low"))
|
|
254
|
+
expect(legacy).toBe(modern)
|
|
255
|
+
expect(legacy).toContain("git show abc")
|
|
256
|
+
const amended = await building(undefined, undefined, true)
|
|
257
|
+
expect(
|
|
258
|
+
await building(undefined, undefined, undefined, () => implement(undefined, undefined, true)),
|
|
259
|
+
).toBe(amended)
|
|
260
|
+
const bare = await building(undefined, undefined, undefined, () => implement())
|
|
261
|
+
expect(bare).toBe(await building(undefined))
|
|
262
|
+
})
|
|
263
|
+
|
|
250
264
|
it("names the reference with a git show line", async () => {
|
|
251
265
|
expect(await building("abc")).toContain("git show abc")
|
|
252
266
|
})
|
|
@@ -41,17 +41,36 @@ export const naivePass = (rejection?: t.SeamRejection): Promise<void> =>
|
|
|
41
41
|
system: t.builderSystem(),
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
export
|
|
44
|
+
export interface ImplementOptions {
|
|
45
|
+
readonly reference?: string | undefined
|
|
46
|
+
readonly tier?: RiskTier | undefined
|
|
47
|
+
readonly amend?: boolean | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function implement(options?: ImplementOptions): Promise<void>
|
|
51
|
+
/** @deprecated Pass `implement({ reference, tier, amend })`. */
|
|
52
|
+
export function implement(
|
|
45
53
|
reference: string | undefined,
|
|
46
54
|
tier?: RiskTier,
|
|
47
55
|
amend?: boolean,
|
|
48
|
-
): Promise<void>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
): Promise<void>
|
|
57
|
+
export function implement(
|
|
58
|
+
first?: string | ImplementOptions,
|
|
59
|
+
tier?: RiskTier,
|
|
60
|
+
amend?: boolean,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const options: ImplementOptions =
|
|
63
|
+
typeof first === "object" && first !== null ? first : { reference: first, tier, amend }
|
|
64
|
+
return scope("implement", async () => {
|
|
65
|
+
await t.agentWithSkills(
|
|
66
|
+
"building",
|
|
67
|
+
t.implementBuildingPrompt(options.reference, options.tier, options.amend),
|
|
68
|
+
{
|
|
69
|
+
label: "Implementing",
|
|
70
|
+
model: coder(),
|
|
71
|
+
system: t.builderSystem(),
|
|
72
|
+
},
|
|
73
|
+
)
|
|
55
74
|
await healthy(() =>
|
|
56
75
|
t.agentWithSkills("fix.suite.fixing", t.implementFixPrompt(), {
|
|
57
76
|
label: "Fixing the check",
|
|
@@ -62,6 +81,7 @@ export const implement = (
|
|
|
62
81
|
)
|
|
63
82
|
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" })
|
|
64
83
|
})
|
|
84
|
+
}
|
|
65
85
|
|
|
66
86
|
interface Decision {
|
|
67
87
|
readonly reference: string
|
package/src/workflows/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export * from "./feature/planning.js"
|
|
|
24
24
|
export * from "./feature/packages.js"
|
|
25
25
|
export * from "./feature/refactor.js"
|
|
26
26
|
export * from "./feature/seam.js"
|
|
27
|
-
export { buildTail, type ReviewOutcome } from "./feature/review.js"
|
|
27
|
+
export { buildTail, type BuildTailOptions, type ReviewOutcome } from "./feature/review.js"
|
|
28
28
|
export * from "./feature/feature.js"
|
|
29
29
|
export { base, review, reviewFlow } from "./review/review.js"
|
|
30
30
|
export { fix, fixFlow, fixReviewNotes } from "./fix/fix.js"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { FLAGS, type FlagDeclaration } from "../../flags/index.js"
|
|
2
3
|
import { configure, type WorkflowDoors } from "../../flows/index.js"
|
|
3
4
|
import * as workflows from "../index.js"
|
|
4
5
|
import * as options from "./options.js"
|
|
@@ -10,13 +11,19 @@ describe("the shipped workflow factories", () => {
|
|
|
10
11
|
it("take exactly the settings the shipped workflows declare as options", () => {
|
|
11
12
|
expect(Object.keys(ShippedOptions.fields).sort()).toEqual(
|
|
12
13
|
[...Object.keys(defaults), ...Object.keys(envDefaults)]
|
|
13
|
-
.filter((name) => name
|
|
14
|
+
.filter((name) => !name.startsWith("flag_"))
|
|
14
15
|
.sort(),
|
|
15
16
|
)
|
|
16
17
|
})
|
|
17
18
|
|
|
19
|
+
it("declare a flag_<name> default for exactly the registered flags", () => {
|
|
20
|
+
const declared = Object.keys(defaults).filter((name) => name.startsWith("flag_"))
|
|
21
|
+
expect(declared.sort()).toEqual(FLAGS.map(({ name }) => `flag_${name}`).sort())
|
|
22
|
+
for (const { name, defaultOn } of FLAGS as readonly FlagDeclaration[])
|
|
23
|
+
expect(defaults).toHaveProperty(`flag_${name}`, defaultOn ? "1" : "0")
|
|
24
|
+
})
|
|
25
|
+
|
|
18
26
|
it("declare seamAllow and flag_naive_pass defaults; flag_naive_pass is no option", () => {
|
|
19
|
-
expect(defaults.flag_naive_pass).toBe("0")
|
|
20
27
|
expect(Object.keys(ShippedOptions.fields)).not.toContain("flag_naive_pass")
|
|
21
28
|
expect(configure(feature({ seamAllow: "docs/**" }))).toMatchObject({
|
|
22
29
|
ok: true,
|
|
@@ -25,7 +32,6 @@ describe("the shipped workflow factories", () => {
|
|
|
25
32
|
})
|
|
26
33
|
|
|
27
34
|
it("declare flag_risk_tier off by default; it is no factory option", () => {
|
|
28
|
-
expect(defaults.flag_risk_tier).toBe("0")
|
|
29
35
|
expect(Object.keys(ShippedOptions.fields)).not.toContain("flag_risk_tier")
|
|
30
36
|
})
|
|
31
37
|
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
buildReviewReviewingPrompt,
|
|
10
10
|
agentWithSkills,
|
|
11
11
|
architectureAuthorPrompt,
|
|
12
|
+
architectureDecomposePrompt,
|
|
13
|
+
packagesItemBuildingPrompt,
|
|
12
14
|
architectureGateAnswerMessage,
|
|
13
15
|
buildFixPrompt,
|
|
14
16
|
buildFixQualityPrompt,
|
|
@@ -1061,7 +1063,7 @@ describe("the refactor prompts", () => {
|
|
|
1061
1063
|
|
|
1062
1064
|
it("building and fix rules forbid every test file, with no stray exception", () => {
|
|
1063
1065
|
for (const prompt of renderText(() => [
|
|
1064
|
-
refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern),
|
|
1066
|
+
refactorBuildingPrompt({ pkg: ".gtd/REFACTOR.md", strays, pattern }),
|
|
1065
1067
|
refactorFixPrompt(strays, pattern),
|
|
1066
1068
|
])) {
|
|
1067
1069
|
expect(prompt).toContain("never a test file")
|
|
@@ -1071,8 +1073,8 @@ describe("the refactor prompts", () => {
|
|
|
1071
1073
|
|
|
1072
1074
|
it("building adds the act-on-review-feedback line only when asked", () => {
|
|
1073
1075
|
const [plain, feedback] = renderText(() => [
|
|
1074
|
-
refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern),
|
|
1075
|
-
refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern,
|
|
1076
|
+
refactorBuildingPrompt({ pkg: ".gtd/REFACTOR.md", strays, pattern }),
|
|
1077
|
+
refactorBuildingPrompt({ pkg: ".gtd/REFACTOR.md", strays, pattern, feedback: true }),
|
|
1076
1078
|
])
|
|
1077
1079
|
expect(plain).not.toContain("## Review feedback")
|
|
1078
1080
|
expect(feedback).toContain("## Review feedback")
|
|
@@ -1266,3 +1268,45 @@ describe("buildReviewReviewingPrompt lap wording", () => {
|
|
|
1266
1268
|
},
|
|
1267
1269
|
)
|
|
1268
1270
|
})
|
|
1271
|
+
|
|
1272
|
+
describe("architectureDecomposePrompt tests rule", () => {
|
|
1273
|
+
it("states one rule: write-or-change, relied-on marked (unchanged), no other untouched", () => {
|
|
1274
|
+
const prompt = renderText(() => architectureDecomposePrompt()).replace(/\s+/g, " ")
|
|
1275
|
+
expect(prompt).toContain("List every test the package writes or changes")
|
|
1276
|
+
expect(prompt).toContain("also list an existing test a criterion relies on")
|
|
1277
|
+
expect(prompt).toContain("list no other untouched test")
|
|
1278
|
+
expect(prompt).toContain("- unit: \\`<path>\\` (unchanged)")
|
|
1279
|
+
expect(prompt).toContain("- e2e: \\`<path>\\` (unchanged)")
|
|
1280
|
+
expect(prompt).toContain("only decompose places the marker")
|
|
1281
|
+
})
|
|
1282
|
+
})
|
|
1283
|
+
|
|
1284
|
+
describe("packagesItemBuildingPrompt", () => {
|
|
1285
|
+
const prompt = renderText(() => packagesItemBuildingPrompt(".gtd/packages/01-a.md")).replace(
|
|
1286
|
+
/\s+/g,
|
|
1287
|
+
" ",
|
|
1288
|
+
)
|
|
1289
|
+
it("carries the evidence kinds and the already-done check", () => {
|
|
1290
|
+
for (const w of [
|
|
1291
|
+
"evidence per criterion",
|
|
1292
|
+
"commit, file, symbol, issue URL, or command output — and change nothing else",
|
|
1293
|
+
"it was not already done",
|
|
1294
|
+
"via `gh`",
|
|
1295
|
+
])
|
|
1296
|
+
expect(prompt).toContain(w)
|
|
1297
|
+
})
|
|
1298
|
+
it("states the already-met, no-tests out-of-tree and implement-normally rules flat", () => {
|
|
1299
|
+
expect(prompt).toContain("Every criterion already met in the tree")
|
|
1300
|
+
expect(prompt).toContain("No `## Tests` and work outside the tree")
|
|
1301
|
+
expect(prompt).toContain(
|
|
1302
|
+
"Any other package**: implement normally, write no `.gtd/SATISFIED.md`",
|
|
1303
|
+
)
|
|
1304
|
+
})
|
|
1305
|
+
it("says an (unchanged) line must exist and stay untouched", () => {
|
|
1306
|
+
expect(prompt).toContain("(unchanged)")
|
|
1307
|
+
expect(prompt).toContain("stay untouched")
|
|
1308
|
+
expect(prompt).toContain("creating, editing or deleting it refuses the turn")
|
|
1309
|
+
expect(prompt).toContain("must exist before the turn")
|
|
1310
|
+
expect(prompt).toContain("stop without creating it")
|
|
1311
|
+
})
|
|
1312
|
+
})
|
|
@@ -376,7 +376,12 @@ ${stateFileRules}
|
|
|
376
376
|
- Every package with tests has a \`## Tests\` section, one line per
|
|
377
377
|
declared test, copied from the architecture entries:
|
|
378
378
|
\`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
|
|
379
|
-
none
|
|
379
|
+
none. List every test the package writes or changes; also list an
|
|
380
|
+
existing test a criterion relies on, marked \`(unchanged)\` —
|
|
381
|
+
\`- unit: \\\`<path>\\\` (unchanged)\` or
|
|
382
|
+
\`- e2e: \\\`<path>\\\` (unchanged)\`; list no other untouched test;
|
|
383
|
+
never mark a file the package also writes or changes — list it once,
|
|
384
|
+
unmarked; only decompose places the marker
|
|
380
385
|
- Leave everything uncommitted and finish
|
|
381
386
|
`
|
|
382
387
|
|
|
@@ -413,15 +418,26 @@ export const packagesItemBuildingPrompt = (pkg: string): string =>
|
|
|
413
418
|
never delete the package file (the declared-tests guard reads it
|
|
414
419
|
after you)
|
|
415
420
|
- The package to implement is \`${pkg}\`
|
|
416
|
-
- First check
|
|
417
|
-
an earlier fix turn may already
|
|
418
|
-
|
|
419
|
-
with
|
|
420
|
-
symbol
|
|
421
|
-
|
|
421
|
+
- First check the acceptance criteria against the current tree —
|
|
422
|
+
an earlier fix turn may already meet them
|
|
423
|
+
- **Every criterion already met in the tree**: write
|
|
424
|
+
\`.gtd/SATISFIED.md\` with evidence per criterion — commit, file,
|
|
425
|
+
symbol, issue URL, or command output — and change nothing else
|
|
426
|
+
- **No \`## Tests\` and work outside the tree**: first check it was
|
|
427
|
+
not already done by an earlier attempt (e.g. an existing issue
|
|
428
|
+
with the same title via \`gh\`) and reuse that as evidence, or do
|
|
429
|
+
the work; then write \`.gtd/SATISFIED.md\` with that evidence and
|
|
430
|
+
change nothing else in the tree
|
|
431
|
+
- **Any other package**: implement normally, write no
|
|
432
|
+
\`.gtd/SATISFIED.md\`
|
|
422
433
|
- Write the tests the package declares under \`## Tests\` first,
|
|
423
434
|
then the code that makes them pass; the turn is refused unless
|
|
424
|
-
every declared test is in your diff
|
|
435
|
+
every declared test is in your diff — except a line marked
|
|
436
|
+
\`(unchanged)\`, which must exist before the turn and stay
|
|
437
|
+
untouched — creating, editing or deleting it refuses the turn. If
|
|
438
|
+
it does not exist yet, or the marker is wrong because the package
|
|
439
|
+
must change that test, stop without creating it and leave it to
|
|
440
|
+
a person
|
|
425
441
|
- A declared e2e test is a scenario in the repository's own e2e
|
|
426
442
|
framework for the behaviour its \`## E2E Scenarios\` entry names.
|
|
427
443
|
Word it to fit the existing steps — reuse and generalise them
|
|
@@ -457,13 +473,19 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
457
473
|
|
|
458
474
|
const withRules = (rules?: string): string => (rules === undefined ? "" : `${rules}\n`)
|
|
459
475
|
|
|
460
|
-
export const refactorBuildingPrompt = (
|
|
461
|
-
pkg
|
|
462
|
-
strays
|
|
463
|
-
pattern
|
|
464
|
-
reference
|
|
476
|
+
export const refactorBuildingPrompt = ({
|
|
477
|
+
pkg,
|
|
478
|
+
strays,
|
|
479
|
+
pattern,
|
|
480
|
+
reference,
|
|
465
481
|
feedback = false,
|
|
466
|
-
|
|
482
|
+
}: {
|
|
483
|
+
pkg: string
|
|
484
|
+
strays: readonly string[]
|
|
485
|
+
pattern: string
|
|
486
|
+
reference?: string | undefined
|
|
487
|
+
feedback?: boolean | undefined
|
|
488
|
+
}): string =>
|
|
467
489
|
`${stateFileRules}
|
|
468
490
|
- The package to implement is \`${pkg}\`; never delete it
|
|
469
491
|
${
|