@pmelab/gtd 27.1.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/README.md +16 -9
- package/dist/chunks/registry-CP3YuV9j.mjs +101 -0
- package/dist/chunks/{reserved-BL9krPao.mjs → reserved-BPdVZkgv.mjs} +66 -73
- package/dist/chunks/tail-iLAMoigl.mjs +81 -0
- package/dist/gtd.bundle.mjs +5 -2
- package/dist/testkit.mjs +17 -2
- package/dist/workflows/cleanup.mjs +37 -0
- package/dist/workflows/feature.mjs +119 -5
- package/dist/workflows/fix.mjs +2 -1
- package/dist/workflows/review.mjs +2 -1
- package/dist/workflows/rollout.mjs +33 -0
- package/dist/workflows.mjs +362 -25
- package/package.json +11 -1
- package/src/flags/index.test.ts +28 -0
- package/src/flags/index.ts +9 -4
- package/src/testkit/EmittedScriptRecognizer.ts +21 -0
- package/src/testkit/EmittedScripts.test.ts +9 -0
- package/src/testkit/steps.ts +26 -0
- package/src/testkit/testkit.feature +8 -1
- package/src/workflows/cleanup/cleanup.feature +323 -0
- package/src/workflows/cleanup/cleanup.test.ts +272 -0
- package/src/workflows/cleanup/cleanup.ts +52 -0
- package/src/workflows/cleanup/index.ts +6 -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 +4 -1
- package/src/workflows/rollout/index.ts +6 -0
- package/src/workflows/rollout/rollout.feature +214 -0
- package/src/workflows/rollout/rollout.test.ts +167 -0
- package/src/workflows/rollout/rollout.ts +44 -0
- package/src/workflows/shared/access.ts +2 -1
- package/src/workflows/shared/health.test.ts +103 -0
- package/src/workflows/shared/health.ts +32 -7
- package/src/workflows/shared/options.test.ts +30 -0
- package/src/workflows/shared/options.ts +20 -0
- package/src/workflows/shared/registry.test.ts +305 -0
- package/src/workflows/shared/registry.ts +158 -0
- package/src/workflows/shared/skills.test.ts +6 -0
- package/src/workflows/shared/skills.ts +5 -1
- package/src/workflows/shared/tail.test.ts +36 -2
- package/src/workflows/shared/tail.ts +30 -18
- package/src/workflows/shared/text.ts +35 -0
- package/src/workflows/shared/vars.ts +1 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { read, refuse } from "../../flows/index.js"
|
|
2
|
+
|
|
3
|
+
// The registry is read as TEXT: workflows may not import `src/flags/`, and this
|
|
4
|
+
// also lets a repository point at its own registry file.
|
|
5
|
+
|
|
6
|
+
export interface RegistryEntry {
|
|
7
|
+
readonly kind: string
|
|
8
|
+
readonly defaultOn: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Filler for masked text: a non-word char, so `/*x*/kind` still has a `\b` before `kind`.
|
|
12
|
+
const FILL = "\u0001"
|
|
13
|
+
|
|
14
|
+
// Same length as `text`, with string contents and comments filled with FILL, so
|
|
15
|
+
// a hit inside either (a JSDoc example, `owner: "defaultOn: true"`) is rejected.
|
|
16
|
+
const commentLength = (rest: string): number => {
|
|
17
|
+
const end = rest.startsWith("//") ? rest.indexOf("\n") : rest.indexOf("*/") + 2
|
|
18
|
+
return end <= 0 ? rest.length : end
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Index of the closing quote of the string opened at `i` (or text end).
|
|
22
|
+
const stringEnd = (text: string, i: number, quote: string): number => {
|
|
23
|
+
let j = i + 1
|
|
24
|
+
while (j < text.length && text[j] !== quote) j += text[j] === "\\" ? 2 : 1
|
|
25
|
+
return Math.min(j, text.length)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const mask = (text: string): string => {
|
|
29
|
+
let out = ""
|
|
30
|
+
let i = 0
|
|
31
|
+
while (i < text.length) {
|
|
32
|
+
const c = text[i] as string
|
|
33
|
+
const rest = text.slice(i)
|
|
34
|
+
if (rest.startsWith("//") || rest.startsWith("/*")) {
|
|
35
|
+
const len = commentLength(rest)
|
|
36
|
+
out += FILL.repeat(len)
|
|
37
|
+
i += len
|
|
38
|
+
} else if (c === '"' || c === "'" || c === "`") {
|
|
39
|
+
const j = stringEnd(text, i, c)
|
|
40
|
+
out += c + FILL.repeat(j - i - 1) + (j < text.length ? c : "")
|
|
41
|
+
i = j + 1
|
|
42
|
+
} else {
|
|
43
|
+
out += c
|
|
44
|
+
i += 1
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
51
|
+
|
|
52
|
+
// Entries are flat object literals, so the nearest braces around the name delimit one.
|
|
53
|
+
const entrySpan = (text: string, name: string) => {
|
|
54
|
+
const quoted = escapeRegExp(name)
|
|
55
|
+
const re = new RegExp(`\\bname\\s*:\\s*(["'\`])${quoted}\\1`, "g")
|
|
56
|
+
const masked = mask(text)
|
|
57
|
+
for (const hit of text.matchAll(re)) {
|
|
58
|
+
if (masked[hit.index] === FILL) continue
|
|
59
|
+
const start = masked.lastIndexOf("{", hit.index)
|
|
60
|
+
const close = masked.indexOf("}", hit.index)
|
|
61
|
+
return start < 0 || close < 0 ? undefined : { start, end: close + 1 }
|
|
62
|
+
}
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const DEFAULT_ON = /\bdefaultOn\s*:\s*(true|false)\s*,?/
|
|
67
|
+
|
|
68
|
+
// `defaultOn` as a property of `body`, never one inside a string or comment.
|
|
69
|
+
const defaultOnHit = (body: string) => DEFAULT_ON.exec(mask(body))
|
|
70
|
+
|
|
71
|
+
const defaultOnValue = (body: string): string | undefined => defaultOnHit(body)?.[1]
|
|
72
|
+
|
|
73
|
+
const KIND = /\bkind\s*:\s*["'`]/
|
|
74
|
+
|
|
75
|
+
export const registryEntry = (text: string, name: string): RegistryEntry | undefined => {
|
|
76
|
+
const span = entrySpan(text, name)
|
|
77
|
+
if (span === undefined) return undefined
|
|
78
|
+
const body = text.slice(span.start, span.end)
|
|
79
|
+
const hit = KIND.exec(mask(body))
|
|
80
|
+
const kind = hit === null ? null : /^(\w+)["'`]/.exec(body.slice(hit.index + hit[0].length))
|
|
81
|
+
return {
|
|
82
|
+
kind: kind?.[1] ?? "",
|
|
83
|
+
defaultOn: defaultOnValue(body) === "true",
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Drops the `defaultOn` property, then whitespace and trailing commas outside strings.
|
|
88
|
+
const canonical = (body: string): string => {
|
|
89
|
+
const hit = defaultOnHit(body)
|
|
90
|
+
const kept =
|
|
91
|
+
hit === null ? body : body.slice(0, hit.index) + body.slice(hit.index + hit[0].length)
|
|
92
|
+
const masked = mask(kept)
|
|
93
|
+
// UTF-16 units, to match `masked` indexing; code points would drift after a surrogate pair
|
|
94
|
+
return kept
|
|
95
|
+
.split("")
|
|
96
|
+
.filter(
|
|
97
|
+
(c, i) =>
|
|
98
|
+
masked[i] === FILL || !(/\s/.test(c) || (c === "," && /^\s*}/.test(masked.slice(i + 1)))),
|
|
99
|
+
)
|
|
100
|
+
.join("")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* [] when `after` differs from `before` only by the named entry's `defaultOn`
|
|
105
|
+
* going absent/false → true (whitespace and trailing commas inside that entry
|
|
106
|
+
* ignored, so oxfmt re-wrapping it passes); else one line per fault.
|
|
107
|
+
*/
|
|
108
|
+
export const flipProblems = (before: string, after: string, name: string): readonly string[] => {
|
|
109
|
+
const was = entrySpan(before, name)
|
|
110
|
+
const now = entrySpan(after, name)
|
|
111
|
+
if (was === undefined) return [`no entry "${name}" before the turn`]
|
|
112
|
+
if (now === undefined) return [`entry "${name}" is gone`]
|
|
113
|
+
const faults: string[] = []
|
|
114
|
+
if (before.slice(0, was.start) !== after.slice(0, now.start)) {
|
|
115
|
+
faults.push("text before the entry changed")
|
|
116
|
+
}
|
|
117
|
+
if (before.slice(was.end) !== after.slice(now.end)) faults.push("text after the entry changed")
|
|
118
|
+
const oldBody = before.slice(was.start, was.end)
|
|
119
|
+
const newBody = after.slice(now.start, now.end)
|
|
120
|
+
if (canonical(oldBody) !== canonical(newBody))
|
|
121
|
+
faults.push(`entry "${name}" changed beyond defaultOn`)
|
|
122
|
+
if (defaultOnValue(newBody) !== "true") faults.push(`"${name}" is not set to defaultOn: true`)
|
|
123
|
+
return faults
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** `path:line` for every `GTD_FLAG_<NAME>` in shipped sources; re-states scripts/lint-flags.ts' rule by hand. */
|
|
127
|
+
export const flagReferences = (
|
|
128
|
+
files: readonly { readonly path: string; readonly text: string }[],
|
|
129
|
+
name: string,
|
|
130
|
+
): readonly string[] => {
|
|
131
|
+
const pattern = new RegExp(`\\bGTD_FLAG_${escapeRegExp(name.toUpperCase())}\\b`)
|
|
132
|
+
return files
|
|
133
|
+
.filter(({ path }) => !/\.(test|fixture|stories)\.tsx?$/.test(path))
|
|
134
|
+
.flatMap(({ path, text }) =>
|
|
135
|
+
text.split("\n").flatMap((line, i) => (pattern.test(line) ? [`${path}:${i + 1}`] : [])),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const requireFlag = (
|
|
140
|
+
workflow: "rollout" | "cleanup",
|
|
141
|
+
registryPath: string,
|
|
142
|
+
name: string,
|
|
143
|
+
defaultOn: boolean,
|
|
144
|
+
): void => {
|
|
145
|
+
if (name === "") return refuse(`gtd: ${workflow} needs --var flag=<name>`)
|
|
146
|
+
const entry = registryEntry(read(registryPath) ?? "", name)
|
|
147
|
+
if (entry === undefined) return refuse(`gtd: ${workflow}: no flag "${name}" in ${registryPath}`)
|
|
148
|
+
if (entry.kind !== "release") {
|
|
149
|
+
return refuse(`gtd: ${workflow}: flag "${name}" is ${entry.kind}, not release`)
|
|
150
|
+
}
|
|
151
|
+
if (entry.defaultOn !== defaultOn) {
|
|
152
|
+
return refuse(
|
|
153
|
+
defaultOn
|
|
154
|
+
? `gtd: ${workflow}: flag "${name}" is not default-on — roll it out first`
|
|
155
|
+
: `gtd: ${workflow}: flag "${name}" is already default-on`,
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -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 ?? ""),
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type CodeThreadInfo,
|
|
6
6
|
type StepRequest,
|
|
7
7
|
} from "../../flows/index.js"
|
|
8
|
+
import type { Guard } from "./health.js"
|
|
8
9
|
import { handOffReview, REVIEW_ORIG } from "./tail.js"
|
|
9
10
|
import { fixtureContext } from "./text.fixture.js"
|
|
10
11
|
|
|
@@ -30,7 +31,11 @@ const reviewDoc = (note: string): string =>
|
|
|
30
31
|
*/
|
|
31
32
|
const handOff = async (
|
|
32
33
|
after: string | "deleted" | "unwritten",
|
|
33
|
-
extra: {
|
|
34
|
+
extra: {
|
|
35
|
+
paths?: readonly string[]
|
|
36
|
+
codeThreads?: readonly CodeThreadInfo[]
|
|
37
|
+
guard?: Guard
|
|
38
|
+
} = {},
|
|
34
39
|
) => {
|
|
35
40
|
const log: string[] = []
|
|
36
41
|
const scripts = new Map<string, string>()
|
|
@@ -87,7 +92,7 @@ const handOff = async (
|
|
|
87
92
|
},
|
|
88
93
|
),
|
|
89
94
|
)
|
|
90
|
-
const refusal = await handOffReview("base", { rounds: 0 }).then(
|
|
95
|
+
const refusal = await handOffReview("base", { rounds: 0 }, extra.guard).then(
|
|
91
96
|
() => undefined,
|
|
92
97
|
(e: unknown) => (e as Error).message,
|
|
93
98
|
)
|
|
@@ -162,4 +167,33 @@ describe("the hand-off review", () => {
|
|
|
162
167
|
expect(run.refusal).toMatch(/was deleted — restore it, or leave a note to hand to fix/)
|
|
163
168
|
expect(run.log).toEqual(["review.reviewing", "review.await-review"])
|
|
164
169
|
})
|
|
170
|
+
|
|
171
|
+
describe("guard", () => {
|
|
172
|
+
it("runs after the reviewing turn and never after the human's await-review landing", async () => {
|
|
173
|
+
const seen: string[] = []
|
|
174
|
+
const run = await handOff(reviewDoc("new add function"), {
|
|
175
|
+
guard: () => seen.push("guard"),
|
|
176
|
+
})
|
|
177
|
+
expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.closing"])
|
|
178
|
+
expect(seen).toEqual(["guard"])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it("a refusal after reviewing stops the round before the human gate", async () => {
|
|
182
|
+
const run = await handOff(reviewDoc("new add function"), {
|
|
183
|
+
guard: () => {
|
|
184
|
+
throw new Error("guarded: nope")
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
expect(run.refusal).toBe("guarded: nope")
|
|
188
|
+
expect(run.log).toEqual(["review.reviewing"])
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it("does not run after review-missing", async () => {
|
|
192
|
+
let calls = 0
|
|
193
|
+
const run = await handOff("unwritten", { guard: () => void calls++ })
|
|
194
|
+
expect(run.log).toContain("review.review-missing")
|
|
195
|
+
// the fixture aborts the second reviewing turn: only the first was guarded
|
|
196
|
+
expect(calls).toBe(1)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
165
199
|
})
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../../flows/index.js"
|
|
2
2
|
import * as kit from "../../workflow-kit/index.js"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
escalation,
|
|
5
|
+
FIX_CAP,
|
|
6
|
+
guarded,
|
|
7
|
+
healthy,
|
|
8
|
+
type EscalationCount,
|
|
9
|
+
type Guard,
|
|
10
|
+
} from "./health.js"
|
|
4
11
|
import {
|
|
5
12
|
awaitReview,
|
|
6
13
|
fixCheck,
|
|
@@ -22,16 +29,16 @@ export const REVIEW_ORIG = ".gtd/REVIEW.orig.md"
|
|
|
22
29
|
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
23
30
|
* has any.
|
|
24
31
|
*/
|
|
25
|
-
export const qualityLap = (): Promise<"clean" | "findings"> =>
|
|
26
|
-
kit.qualityLap(kit.lensesOf(vars.qualityReviews ?? ""), reviewQuality, QUALITY)
|
|
32
|
+
export const qualityLap = (guard?: Guard): Promise<"clean" | "findings"> =>
|
|
33
|
+
kit.qualityLap(kit.lensesOf(vars.qualityReviews ?? ""), guarded(reviewQuality, guard), QUALITY)
|
|
27
34
|
|
|
28
35
|
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
29
|
-
export const fixQualityFindings = (escalations: EscalationCount): Promise<boolean> =>
|
|
36
|
+
export const fixQualityFindings = (escalations: EscalationCount, guard?: Guard): Promise<boolean> =>
|
|
30
37
|
kit.fixFindings({
|
|
31
|
-
fix: fixQuality,
|
|
38
|
+
fix: guarded(fixQuality, guard),
|
|
32
39
|
findings: QUALITY,
|
|
33
40
|
cap: FIX_CAP,
|
|
34
|
-
escalate: () => escalation(escalations),
|
|
41
|
+
escalate: () => escalation(escalations, guard),
|
|
35
42
|
})
|
|
36
43
|
|
|
37
44
|
/** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
|
|
@@ -59,21 +66,22 @@ export const tail = <T>(
|
|
|
59
66
|
fixFirst: boolean,
|
|
60
67
|
finish: (escalations: EscalationCount) => Promise<T>,
|
|
61
68
|
green = false,
|
|
69
|
+
guard?: Guard,
|
|
62
70
|
): Promise<T> =>
|
|
63
71
|
scope("build", async () => {
|
|
64
72
|
const escalations: EscalationCount = { rounds: 0 }
|
|
65
73
|
const fix = fixCheck
|
|
66
74
|
const repair = async () => {
|
|
67
|
-
await fix()
|
|
68
|
-
await healthy(fix, { fixesSoFar: 1, escalations })
|
|
75
|
+
await guarded(fix, guard)()
|
|
76
|
+
await healthy(fix, { fixesSoFar: 1, escalations, guard })
|
|
69
77
|
}
|
|
70
78
|
// fixFirst's own fix + healthy below already is the full run.
|
|
71
79
|
if (fixFirst) await repair()
|
|
72
|
-
else if (!green) await healthy(fix, { escalations })
|
|
80
|
+
else if (!green) await healthy(fix, { escalations, guard })
|
|
73
81
|
// The lap runs once a tail: after its findings are fixed, review follows.
|
|
74
|
-
if ((await qualityLap()) === "findings") {
|
|
75
|
-
if (await fixQualityFindings(escalations)) {
|
|
76
|
-
await healthy(fix, { escalations })
|
|
82
|
+
if ((await qualityLap(guard)) === "findings") {
|
|
83
|
+
if (await fixQualityFindings(escalations, guard)) {
|
|
84
|
+
await healthy(fix, { escalations, guard })
|
|
77
85
|
} else await repair()
|
|
78
86
|
}
|
|
79
87
|
return finish(escalations)
|
|
@@ -84,12 +92,16 @@ export const tail = <T>(
|
|
|
84
92
|
* `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
|
|
85
93
|
* `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
|
|
86
94
|
*/
|
|
87
|
-
export const handOffReview = (
|
|
95
|
+
export const handOffReview = (
|
|
96
|
+
base: string,
|
|
97
|
+
escalations: EscalationCount,
|
|
98
|
+
guard?: Guard,
|
|
99
|
+
): Promise<void> =>
|
|
88
100
|
kit.reviewToHandOff(base, {
|
|
89
101
|
file: REVIEW,
|
|
90
|
-
reviewing,
|
|
91
|
-
fixRisks,
|
|
92
|
-
keepGreen: () => healthy(fixCheck, { escalations }),
|
|
102
|
+
reviewing: guarded(reviewing, guard),
|
|
103
|
+
fixRisks: guarded(fixRisks, guard),
|
|
104
|
+
keepGreen: () => healthy(fixCheck, { escalations, guard }),
|
|
93
105
|
awaitReview: (b) => awaitReview(b, t.handOffAwaitReviewMessage(b)),
|
|
94
106
|
reviewMissing,
|
|
95
107
|
close: closeReview,
|
|
@@ -107,5 +119,5 @@ export const handOffReview = (base: string, escalations: EscalationCount): Promi
|
|
|
107
119
|
})
|
|
108
120
|
|
|
109
121
|
/** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
|
|
110
|
-
export const handOffTail = (fixFirst: boolean, base: string): Promise<void> =>
|
|
111
|
-
tail(fixFirst, (escalations) => handOffReview(base, escalations))
|
|
122
|
+
export const handOffTail = (fixFirst: boolean, base: string, guard?: Guard): Promise<void> =>
|
|
123
|
+
tail(fixFirst, (escalations) => handOffReview(base, escalations, guard), false, guard)
|
|
@@ -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:
|