@pmelab/gtd 27.2.0 → 27.3.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/dist/chunks/{reserved-B2QaImnN.mjs → reserved-BPdVZkgv.mjs} +29 -4
- package/dist/chunks/{tail-BnhIxCYv.mjs → tail-iLAMoigl.mjs} +1 -1
- package/dist/testkit.mjs +12 -0
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +119 -6
- 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 +146 -8
- package/package.json +1 -1
- package/src/testkit/EmittedScriptRecognizer.ts +21 -0
- package/src/testkit/EmittedScripts.test.ts +9 -0
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor.test.ts +466 -0
- package/src/workflows/feature/refactor.ts +178 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/access.ts +2 -1
- package/src/workflows/shared/health.test.ts +67 -3
- package/src/workflows/shared/health.ts +5 -3
- package/src/workflows/shared/options.test.ts +8 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/skills.test.ts +6 -0
- package/src/workflows/shared/skills.ts +5 -1
- package/src/workflows/shared/text.ts +35 -0
- package/src/workflows/shared/vars.ts +1 -0
|
@@ -13,6 +13,8 @@ describe("retryQuestion", () => {
|
|
|
13
13
|
})
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
+
type W = { write(p: string, c: string): void }
|
|
17
|
+
|
|
16
18
|
describe("gate guard", () => {
|
|
17
19
|
const config = (guard: string) =>
|
|
18
20
|
[
|
|
@@ -23,9 +25,8 @@ describe("gate guard", () => {
|
|
|
23
25
|
].join("\n")
|
|
24
26
|
|
|
25
27
|
const red = {
|
|
26
|
-
first: (_: unknown, r:
|
|
27
|
-
"suite.check": (_: unknown, r:
|
|
28
|
-
r.write(".gtd/FEEDBACK.md", "red\n"),
|
|
28
|
+
first: (_: unknown, r: W) => r.write("f", "1\n"),
|
|
29
|
+
"suite.check": (_: unknown, r: W) => r.write(".gtd/FEEDBACK.md", "red\n"),
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
it("runs the guard after a blocked landing and lets its refusal stop it", async () => {
|
|
@@ -50,3 +51,66 @@ describe("gate guard", () => {
|
|
|
50
51
|
expect(rest.state).toBe("suite.blocked")
|
|
51
52
|
})
|
|
52
53
|
})
|
|
54
|
+
|
|
55
|
+
describe("healthy describeGuard", () => {
|
|
56
|
+
const config = (options: string) =>
|
|
57
|
+
[
|
|
58
|
+
`import { healthy } from "@pmelab/gtd/workflows"`,
|
|
59
|
+
`import { agent, read, refuse } from "@pmelab/gtd/flows"`,
|
|
60
|
+
`const describeGuard = () => { refuse("describe-guarded: nope") }`,
|
|
61
|
+
`const guard = () => { refuse("guarded: nope") }`,
|
|
62
|
+
`export const held = async () => { await agent("first", "go"); await healthy(() => agent("fix", "go"), ${options}) }`,
|
|
63
|
+
`export default held`,
|
|
64
|
+
].join("\n")
|
|
65
|
+
|
|
66
|
+
let runs = 0
|
|
67
|
+
const handlers = (extra: Record<string, unknown>) => ({
|
|
68
|
+
"health.check": (_: unknown, r: W) => r.write(".gtd/FEEDBACK.md", `red ${runs++}\n`),
|
|
69
|
+
"health.judge": () => '[{"id":"verdict","answer":"progress","p":0.9}]',
|
|
70
|
+
fix: (_: unknown, r: W) => r.write("f", `${runs}\n`),
|
|
71
|
+
first: (_: unknown, r: W) => r.write("start", "1\n"),
|
|
72
|
+
...extra,
|
|
73
|
+
})
|
|
74
|
+
const make = (options: string) =>
|
|
75
|
+
testRepo({ workflow: config(options), env: { GTD_TEST_COMMAND: "true" } })
|
|
76
|
+
|
|
77
|
+
it("runs after a describe landing and its refusal stops the turn", async () => {
|
|
78
|
+
runs = 0
|
|
79
|
+
const repo = make("{ describeGuard }")
|
|
80
|
+
await repo.start("held")
|
|
81
|
+
await expect(repo.drive(handlers({ "health.describe": () => undefined }))).rejects.toThrow(
|
|
82
|
+
"describe-guarded: nope",
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("never runs after the human stop rest", async () => {
|
|
87
|
+
runs = 0
|
|
88
|
+
// Refuses only once a person's edit exists, which only a post-stop run can see.
|
|
89
|
+
const repo = make(
|
|
90
|
+
'{ describeGuard: () => { if (read("human.txt") !== undefined) refuse("describe-guarded: late") } }',
|
|
91
|
+
)
|
|
92
|
+
await repo.start("held")
|
|
93
|
+
const rest = await repo.drive(handlers({ "health.describe": () => undefined }))
|
|
94
|
+
expect(rest.state).toBe("health.stop")
|
|
95
|
+
const after = await repo.drive(
|
|
96
|
+
handlers({ "health.stop": (_: unknown, r: W) => r.write("human.txt", "edit\n") }),
|
|
97
|
+
)
|
|
98
|
+
expect(after.state).not.toBe("health.stop")
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("keeps guard running after describe", async () => {
|
|
102
|
+
runs = 0
|
|
103
|
+
// Refuses only once the describe turn has written its analysis.
|
|
104
|
+
const repo = make(
|
|
105
|
+
'{ guard: () => { if (read(".gtd/ESCALATION.md") !== undefined) refuse("guarded: nope") } }',
|
|
106
|
+
)
|
|
107
|
+
await repo.start("held")
|
|
108
|
+
await expect(
|
|
109
|
+
repo.drive(
|
|
110
|
+
handlers({
|
|
111
|
+
"health.describe": (_: unknown, r: W) => r.write(".gtd/ESCALATION.md", "why\n"),
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
).rejects.toThrow("guarded: nope")
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -38,9 +38,9 @@ export const guarded =
|
|
|
38
38
|
guard?.()
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const guardedSteps = (guard?: Guard): kit.EscalationSteps => ({
|
|
41
|
+
const guardedSteps = (guard?: Guard, describeGuard?: Guard): kit.EscalationSteps => ({
|
|
42
42
|
limit: escalationSteps.limit,
|
|
43
|
-
describe: guarded(escalationSteps.describe, guard),
|
|
43
|
+
describe: guarded(guarded(escalationSteps.describe, guard), describeGuard),
|
|
44
44
|
stop: guarded(escalationSteps.stop, guard),
|
|
45
45
|
exhausted: guarded(escalationSteps.exhausted, guard),
|
|
46
46
|
})
|
|
@@ -98,6 +98,8 @@ export interface HealthOptions {
|
|
|
98
98
|
readonly sweepOnGreen?: readonly string[]
|
|
99
99
|
/** Runs after every fix and escalation turn. */
|
|
100
100
|
readonly guard?: Guard | undefined
|
|
101
|
+
/** Runs after the escalation's agent turn only; the human rests stay unguarded. */
|
|
102
|
+
readonly describeGuard?: Guard | undefined
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
const runSuite = (options: HealthOptions): Promise<boolean> =>
|
|
@@ -121,7 +123,7 @@ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}):
|
|
|
121
123
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
122
124
|
sameFailure,
|
|
123
125
|
escalations: options.escalations ?? { rounds: 0 },
|
|
124
|
-
escalation: guardedSteps(options.guard),
|
|
126
|
+
escalation: guardedSteps(options.guard, options.describeGuard),
|
|
125
127
|
})
|
|
126
128
|
|
|
127
129
|
/**
|
|
@@ -32,6 +32,14 @@ describe("the shipped workflow factories", () => {
|
|
|
32
32
|
})
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
+
it("default refactorFlagPattern to GTD_FLAG_ matches and let a factory override win", () => {
|
|
36
|
+
expect(defaults.refactorFlagPattern).toBe("\\bGTD_FLAG_\\w+")
|
|
37
|
+
expect(configure(feature({ refactorFlagPattern: "\\bFF_\\w+" }))).toMatchObject({
|
|
38
|
+
ok: true,
|
|
39
|
+
build: { defaults: { refactorFlagPattern: "\\bFF_\\w+" } },
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
35
43
|
it("build the shipped settings unchanged with no options", () => {
|
|
36
44
|
for (const factory of [feature, fix, review]) {
|
|
37
45
|
expect(configure(factory)).toMatchObject({ ok: true, build: { defaults, envDefaults } })
|
|
@@ -26,6 +26,9 @@ describe("the bundled workflow's skills export", () => {
|
|
|
26
26
|
"build.review.fix.nits",
|
|
27
27
|
"build.review.fix.risks",
|
|
28
28
|
"notes",
|
|
29
|
+
"refactor",
|
|
30
|
+
"refactor.fix.suite",
|
|
31
|
+
"refactor.health",
|
|
29
32
|
"build.quality.correctness",
|
|
30
33
|
"build.quality.owasp-security",
|
|
31
34
|
"build.quality.ponytail-review",
|
|
@@ -57,6 +60,9 @@ describe("the bundled workflow's skills export", () => {
|
|
|
57
60
|
"build.review.fix.nits": ["incremental-implementation", "code-simplification"],
|
|
58
61
|
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
|
|
59
62
|
notes: ["incremental-implementation", "code-simplification"],
|
|
63
|
+
refactor: ["ponytail"],
|
|
64
|
+
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
65
|
+
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
60
66
|
})
|
|
61
67
|
})
|
|
62
68
|
|
|
@@ -4,7 +4,7 @@ import { lensesOf, lensScopes } from "../../workflow-kit/index.js"
|
|
|
4
4
|
// The bundled workflow's default skills, one list per SCOPE, keyed by the
|
|
5
5
|
// scope's full name — the same name a `config.skills` entry addresses. A
|
|
6
6
|
// nested scope with no entry inherits its parent's; group scopes
|
|
7
|
-
// (`packages.item.fix`, `build.fix`, `build.review.fix`, `build.quality`) run
|
|
7
|
+
// (`packages.item.fix`, `build.fix`, `build.review.fix`, `build.quality`, `refactor.fix`) run
|
|
8
8
|
// no turn and carry no key. `build.quality.<lens>` is one key per
|
|
9
9
|
// `qualityReviews` entry, so the export is a function of the vars.
|
|
10
10
|
export const skills = (
|
|
@@ -25,6 +25,10 @@ export const skills = (
|
|
|
25
25
|
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
|
|
26
26
|
// `fix`'s turn over the notes a review handed on.
|
|
27
27
|
notes: ["incremental-implementation", "code-simplification"],
|
|
28
|
+
// Every refactor agent turn loads `ponytail`.
|
|
29
|
+
refactor: ["ponytail"],
|
|
30
|
+
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
31
|
+
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
28
32
|
...lensScopes(
|
|
29
33
|
"build.quality",
|
|
30
34
|
lensesOf(vars.qualityReviews ?? ""),
|
|
@@ -422,6 +422,41 @@ ${fixFeedbackPrompt}
|
|
|
422
422
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
423
423
|
`
|
|
424
424
|
|
|
425
|
+
const refactorRules = (strays: readonly string[], pattern: string): string =>
|
|
426
|
+
`- Behaviour-neutral: the suite's outcome must not change
|
|
427
|
+
- Change only these paths, nothing else — a test file only when it is
|
|
428
|
+
one of them:
|
|
429
|
+
${strays.map((path) => ` - \`${path}\``).join("\n")}
|
|
430
|
+
- Introduce no new flag read and add no second read of an existing one:
|
|
431
|
+
every match of \`${pattern}\` may move, never increase in number`
|
|
432
|
+
|
|
433
|
+
export const refactorBuildingPrompt = (
|
|
434
|
+
pkg: string,
|
|
435
|
+
strays: readonly string[],
|
|
436
|
+
pattern: string,
|
|
437
|
+
reference?: string,
|
|
438
|
+
): string =>
|
|
439
|
+
`${stateFileRules}
|
|
440
|
+
- The package to implement is \`${pkg}\`; never delete it
|
|
441
|
+
${refactorRules(strays, pattern)}
|
|
442
|
+
${
|
|
443
|
+
reference === undefined
|
|
444
|
+
? ""
|
|
445
|
+
: `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
446
|
+
earlier attempt at this change. Read it for ideas; never apply,
|
|
447
|
+
cherry-pick or check it out — write a fresh refactor
|
|
448
|
+
`
|
|
449
|
+
}- Leave everything uncommitted and finish your turn — do not commit
|
|
450
|
+
`
|
|
451
|
+
|
|
452
|
+
export const refactorFixPrompt = (strays: readonly string[], pattern: string): string =>
|
|
453
|
+
`${stateFileRules}
|
|
454
|
+
${fixFeedbackPrompt}
|
|
455
|
+
${refactorRules(strays, pattern)}
|
|
456
|
+
- Never do another package's work, even when it would green the suite
|
|
457
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
458
|
+
`
|
|
459
|
+
|
|
425
460
|
export const healthJudgeMessage = (): string =>
|
|
426
461
|
`The check is still red, and this isn't the first round —
|
|
427
462
|
\`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
|
|
@@ -11,6 +11,7 @@ export const defaults: Readonly<Record<string, string>> = {
|
|
|
11
11
|
judgeIdenticalMinP: "0.7",
|
|
12
12
|
reviewNoteActionable: "0.7",
|
|
13
13
|
reviewSeverityMinP: "0.7",
|
|
14
|
+
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
14
15
|
// Decides the turn count: one `build.quality.<lens>` scope per entry, each
|
|
15
16
|
// keyed in `./skills.ts`.
|
|
16
17
|
qualityReviews:
|