@pmelab/gtd 28.1.0 → 29.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 +28 -17
- package/claude/hooks/assist.ts +1 -1
- package/claude/hooks/register.tsx +1 -1
- package/claude/hooks/ship.ts +963 -143
- package/dist/chunks/{reserved-DeBS4Ymh.mjs → reserved-DoHUII2D.mjs} +206 -121
- package/dist/chunks/{tail-B2SER2Ih.mjs → tail-C6YBG5zh.mjs} +19 -12
- package/dist/testkit.mjs +2 -1
- package/dist/workflow-kit.mjs +51 -21
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +247 -96
- 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 +528 -262
- package/package.json +1 -1
- package/src/testkit/TestRepo.ts +3 -0
- package/src/testkit/index.ts +1 -1
- package/src/testkit/steps.ts +44 -0
- package/src/workflow-kit/review.test.ts +366 -8
- package/src/workflow-kit/review.ts +109 -26
- package/src/workflows/feature/feature.test.ts +313 -1
- package/src/workflows/feature/feature.ts +48 -43
- package/src/workflows/feature/one-step-land.feature +4 -0
- package/src/workflows/feature/refactor-first.feature +54 -24
- package/src/workflows/feature/refactor-review.feature +1706 -0
- package/src/workflows/feature/refactor.test.ts +592 -2
- package/src/workflows/feature/refactor.ts +160 -42
- package/src/workflows/feature/review.test.ts +322 -5
- package/src/workflows/feature/review.ts +77 -10
- package/src/workflows/feature/seam-rejection.feature +4 -0
- package/src/workflows/feature/seam.feature +3 -4
- package/src/workflows/feature/seam.test.ts +196 -1
- package/src/workflows/feature/seam.ts +23 -9
- package/src/workflows/feature/steps.test.ts +54 -2
- package/src/workflows/feature/steps.ts +45 -9
- package/src/workflows/feature/tier.test.ts +4 -0
- package/src/workflows/feature/tier.ts +2 -20
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +97 -0
- package/src/workflows/shared/access.test.ts +31 -1
- package/src/workflows/shared/access.ts +7 -1
- package/src/workflows/shared/fences.ts +19 -0
- package/src/workflows/shared/health.ts +32 -12
- package/src/workflows/shared/options.test.ts +18 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/prose.ts +4 -2
- package/src/workflows/shared/skills.test.ts +20 -1
- package/src/workflows/shared/skills.ts +43 -32
- package/src/workflows/shared/steps.ts +33 -19
- package/src/workflows/shared/tail.test.ts +84 -1
- package/src/workflows/shared/tail.ts +30 -12
- package/src/workflows/shared/text.test.ts +266 -4
- package/src/workflows/shared/text.ts +183 -70
- package/src/workflows/shared/vars.ts +12 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
- A deliverable, not a chat reply — size follows the work; cut padding
|
|
2
|
+
- Lead with the answer; never circle back to restate it
|
|
3
|
+
- Flat, commanding sentences — commit to the claim, never hedge
|
|
4
|
+
- Everyday words; define an unavoidable term in five words or fewer, on
|
|
5
|
+
first use
|
|
6
|
+
- Bold carries the load: bold the claim, not the sentence around it —
|
|
7
|
+
the bold text alone must yield the full point and every risk
|
|
8
|
+
- **Never trim a risk, a number, a threshold, or a scoped condition to
|
|
9
|
+
save space — this outranks every other rule here**
|
|
10
|
+
- Ship the artifact bare — no lead-in, no sign-off
|
|
11
|
+
- Compressing is not dropping: three load-bearing parts ship as three,
|
|
12
|
+
each shorter, never as two
|
|
13
|
+
- One idea per block; break when the idea shifts
|
|
14
|
+
- Flag risk in one blunt line, never hedged prose; never narrate — do it
|
|
15
|
+
|
|
16
|
+
- Machine-read: its format contract outranks every style rule above —
|
|
17
|
+
keep every `##`/`###` heading, checkbox row, and marker line exactly
|
|
18
|
+
as specified. Never renumber or rename a heading; a parser reads
|
|
19
|
+
these literally and a violation refuses the turn
|
|
20
|
+
- Voice rules above govern only the prose between these elements
|
|
21
|
+
|
|
22
|
+
You are judging and classifying a round of review feedback.
|
|
23
|
+
|
|
24
|
+
- You are an autonomous coding agent
|
|
25
|
+
- This workflow's own state files are its private scratchpad, never
|
|
26
|
+
project code or documentation
|
|
27
|
+
- A footnote (`[^name]` plus its `[^name]:` definition) is a comment on
|
|
28
|
+
its exact anchor — fold it in as a mandatory concern described
|
|
29
|
+
against that anchor's hunk or paragraph, never flattened into a
|
|
30
|
+
whole-file remark
|
|
31
|
+
- A one-shot footnote needs no reply: fold it in and DELETE it in this
|
|
32
|
+
same turn — marker and definition together. Never re-read a footnote
|
|
33
|
+
already acted on. A one-shot footnote that asks a question becomes a
|
|
34
|
+
thread instead: rewrite its definition as `- H: <its body>` then
|
|
35
|
+
`- A: <your reply>`, keeping the marker
|
|
36
|
+
- A thread is a definition whose body is a list of `- H:`/`- A:`
|
|
37
|
+
entries. Handle each by its last entry:
|
|
38
|
+
- last is `- H:` and asks something — reply with exactly one
|
|
39
|
+
`- A: <reply>` entry and change nothing else for it
|
|
40
|
+
- last concludes or advises — fold it in like a one-shot footnote,
|
|
41
|
+
then delete marker and definition
|
|
42
|
+
- last is `- A:` — it waits on the human; leave it alone
|
|
43
|
+
- Never start a thread yourself, and never leave a thread whose last
|
|
44
|
+
entry is `- H:` — a turn that does is refused
|
|
45
|
+
- This turn writes `.gtd/REQUIREMENTS.md` (the folded concerns) and
|
|
46
|
+
replies inside `.gtd/REVIEW.md` (thread replies only; the file stays
|
|
47
|
+
in the tree) — you classify, you do not build
|
|
48
|
+
- Fold every concluded thread, ticked answer and hand-edit — and each
|
|
49
|
+
`edit` note the capture lists, no other note — into
|
|
50
|
+
`.gtd/REQUIREMENTS.md` and delete the folded threads from
|
|
51
|
+
`.gtd/REVIEW.md`; append one `- A:` reply to each thread
|
|
52
|
+
whose last entry is a `- H:` question
|
|
53
|
+
- When the capture names an answering commit, carry each answered
|
|
54
|
+
question into `.gtd/REQUIREMENTS.md`'s `## Answered Questions` as
|
|
55
|
+
`### <the note>` plus its answer — the lap replaces `.gtd/REVIEW.md`
|
|
56
|
+
- Keep capture order, and carry each note's `(tier: reason)`
|
|
57
|
+
label into the concern folded from it
|
|
58
|
+
- Nit notes were already fixed: never re-raise a fixed nit as a concern
|
|
59
|
+
- Finish by running `gtd check review .gtd/REVIEW.md` and fix
|
|
60
|
+
what it reports
|
|
61
|
+
|
|
62
|
+
The raw review material is:
|
|
63
|
+
|
|
64
|
+
capture-text
|
|
65
|
+
It names a commit. Work from what you already reviewed if you
|
|
66
|
+
wrote today's review earlier this conversation; otherwise read
|
|
67
|
+
that commit's diff yourself first.
|
|
68
|
+
|
|
69
|
+
The round is actionable if any of these hold:
|
|
70
|
+
|
|
71
|
+
- The human left a note on `.gtd/REVIEW.md`. A note is a mandatory
|
|
72
|
+
concern below
|
|
73
|
+
- The human added a code comment this round. A comment run whose
|
|
74
|
+
first line starts `H:` is a THREAD: when its last entry is an
|
|
75
|
+
`H:` question, write exactly one `A:` comment line, same token
|
|
76
|
+
and indentation, directly below it — no fold-in; a concluded
|
|
77
|
+
thread is folded into the requirements and its comment lines
|
|
78
|
+
deleted. Any other comment, even a plain-prose one, is a one-shot
|
|
79
|
+
concern — describe it, and note the comment line itself is
|
|
80
|
+
transient: it must not survive the lap that satisfies it
|
|
81
|
+
- The human hand-edited non-comment code this round — no longer
|
|
82
|
+
a committed intent to build on, but a sketch like the entry
|
|
83
|
+
commit's own diff. Describe what it was reaching for; expect
|
|
84
|
+
the next lap to re-derive it from scratch, never call it final
|
|
85
|
+
|
|
86
|
+
Not actionable only when none of the above holds — nothing but an
|
|
87
|
+
approving remark, no code edit, no substantive note. Never invent
|
|
88
|
+
actionability, and never dismiss a real note or edit as approval.
|
|
89
|
+
|
|
90
|
+
- If actionable: write `.gtd/REQUIREMENTS.md` with an ordered
|
|
91
|
+
list of concerns, each PRODUCT or TECHNICAL — the shape
|
|
92
|
+
`design.triage` builds, one `## <heading>` per concern in
|
|
93
|
+
build order. Fold every note, comment, and hand-edit in under
|
|
94
|
+
its concern. Raise no open questions here — `design.triage`
|
|
95
|
+
owns that later. Then finish
|
|
96
|
+
- If not: finish without writing anything — changing nothing is
|
|
97
|
+
the sign-off
|
|
@@ -22,9 +22,21 @@ describe("the bundled workflow's access export", () => {
|
|
|
22
22
|
"build.quality.test-audit": { write: [] },
|
|
23
23
|
"build.quality.conventions": { write: [] },
|
|
24
24
|
"build.quality.spec-challenge": { write: [] },
|
|
25
|
+
"refactor.build.review": { write: [".gtd/REVIEW.md"] },
|
|
26
|
+
"refactor.build.review.defer": { write: [".gtd/TODO.md"] },
|
|
27
|
+
"refactor.build.quality.correctness": { write: [] },
|
|
28
|
+
"refactor.build.quality.ponytail-review": { write: [] },
|
|
29
|
+
"refactor.build.quality.conventions": { write: [] },
|
|
25
30
|
})
|
|
26
31
|
})
|
|
27
32
|
|
|
33
|
+
it("leaves the refactor review's fix scopes to the refactor's own scope option", () => {
|
|
34
|
+
expect(bundled).not.toHaveProperty(["refactor.build.review.fix.nits"])
|
|
35
|
+
expect(bundled).not.toHaveProperty(["refactor.build.review.fix.risks"])
|
|
36
|
+
expect(bundled["refactor.build.review"]).toEqual({ write: [".gtd/REVIEW.md"] })
|
|
37
|
+
expect(bundled["refactor.build.review.defer"]).toEqual({ write: [".gtd/TODO.md"] })
|
|
38
|
+
})
|
|
39
|
+
|
|
28
40
|
it("keys one lens scope per qualityReviews entry", () => {
|
|
29
41
|
const custom = access({ ...defaults, qualityReviews: " my-lens ,, correctness" })
|
|
30
42
|
expect(
|
|
@@ -34,9 +46,27 @@ describe("the bundled workflow's access export", () => {
|
|
|
34
46
|
).toEqual(["build.quality.correctness", "build.quality.my-lens"])
|
|
35
47
|
})
|
|
36
48
|
|
|
49
|
+
it("keys one refactor lens scope per refactorQualityReviews entry beside the refactor review's own", () => {
|
|
50
|
+
const custom = access({ ...defaults, refactorQualityReviews: " my-lens ,, correctness" })
|
|
51
|
+
expect(
|
|
52
|
+
Object.keys(custom)
|
|
53
|
+
.filter((k) => k.startsWith("refactor."))
|
|
54
|
+
.sort(),
|
|
55
|
+
).toEqual([
|
|
56
|
+
"refactor.build.quality.correctness",
|
|
57
|
+
"refactor.build.quality.my-lens",
|
|
58
|
+
"refactor.build.review",
|
|
59
|
+
"refactor.build.review.defer",
|
|
60
|
+
])
|
|
61
|
+
})
|
|
62
|
+
|
|
37
63
|
it("keys only turn or script scopes", () => {
|
|
38
64
|
// `build.review.defer` is a script scope: no turn, so no skills entry.
|
|
39
|
-
const known = [
|
|
65
|
+
const known = [
|
|
66
|
+
...Object.keys(workflow.skills(defaults)),
|
|
67
|
+
"build.review.defer",
|
|
68
|
+
"refactor.build.review.defer",
|
|
69
|
+
]
|
|
40
70
|
for (const key of Object.keys(bundled)) expect(known).toContain(key)
|
|
41
71
|
})
|
|
42
72
|
})
|
|
@@ -6,7 +6,8 @@ import { lensesOf, lensScopes } from "../../workflow-kit/index.js"
|
|
|
6
6
|
// are restricted, and only on writes; no scope restricts reads. `refactor`'s
|
|
7
7
|
// write restriction lives in its own `scope()` option, not here. A step's own
|
|
8
8
|
// steering file is folded in per step, so it is not listed here. Build and fix
|
|
9
|
-
// scopes carry no entry: no project layout is assumed.
|
|
9
|
+
// scopes carry no entry: no project layout is assumed. `refactor.build.review`'s
|
|
10
|
+
// fix scopes carry none either: `reviewedRefactor` sets their write limit at run time.
|
|
10
11
|
export const access = (
|
|
11
12
|
vars: Readonly<Record<string, string>>,
|
|
12
13
|
): Readonly<Record<string, ScopeAccess>> => ({
|
|
@@ -18,5 +19,10 @@ export const access = (
|
|
|
18
19
|
// `{}` reopens what `build.review` restricted: the fixes edit code.
|
|
19
20
|
"build.review.fix.nits": {},
|
|
20
21
|
"build.review.fix.risks": {},
|
|
22
|
+
"refactor.build.review": { write: [".gtd/REVIEW.md"] },
|
|
23
|
+
"refactor.build.review.defer": { write: [".gtd/TODO.md"] },
|
|
21
24
|
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
|
|
25
|
+
...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), () => ({
|
|
26
|
+
write: [],
|
|
27
|
+
})),
|
|
22
28
|
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// An unclosed fence runs to end of file. A backtick
|
|
2
|
+
// opener with a backtick in its info string still counts as a fence: deviates from CommonMark,
|
|
3
|
+
// worst case is `high`.
|
|
4
|
+
export const markFenced = (text: string): readonly { line: string; fenced: boolean }[] => {
|
|
5
|
+
let fence: string | undefined // the opener run, e.g. "````"
|
|
6
|
+
return text.split(/\r?\n/).map((line) => {
|
|
7
|
+
const run = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1]
|
|
8
|
+
const fenced = fence !== undefined || run !== undefined
|
|
9
|
+
if (fence === undefined) fence = run
|
|
10
|
+
else if (
|
|
11
|
+
run !== undefined &&
|
|
12
|
+
run[0] === fence[0] &&
|
|
13
|
+
run.length >= fence.length &&
|
|
14
|
+
/^ {0,3}(`+|~+)[ \t]*$/.test(line)
|
|
15
|
+
)
|
|
16
|
+
fence = undefined
|
|
17
|
+
return { line, fenced }
|
|
18
|
+
})
|
|
19
|
+
}
|
|
@@ -21,12 +21,12 @@ export const baseline = (name: string, label = "Checking the baseline"): Promise
|
|
|
21
21
|
|
|
22
22
|
export type { EscalationCount } from "../../workflow-kit/index.js"
|
|
23
23
|
|
|
24
|
-
const escalationSteps: kit.EscalationSteps
|
|
24
|
+
const escalationSteps = (rules?: string): kit.EscalationSteps => ({
|
|
25
25
|
limit: 2,
|
|
26
|
-
describe: describeEscalation,
|
|
26
|
+
describe: () => describeEscalation(rules),
|
|
27
27
|
stop: escalate,
|
|
28
28
|
exhausted: escalationExhausted,
|
|
29
|
-
}
|
|
29
|
+
})
|
|
30
30
|
|
|
31
31
|
/** Runs after an agent or human turn; refuses it by throwing (`refuse`). */
|
|
32
32
|
export type Guard = () => void
|
|
@@ -38,20 +38,32 @@ export const guarded =
|
|
|
38
38
|
guard?.()
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const guardedSteps = (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
const guardedSteps = (
|
|
42
|
+
guard?: Guard,
|
|
43
|
+
describeGuard?: Guard,
|
|
44
|
+
rests = true,
|
|
45
|
+
rules?: string,
|
|
46
|
+
): kit.EscalationSteps => {
|
|
47
|
+
const steps = escalationSteps(rules)
|
|
48
|
+
return {
|
|
49
|
+
limit: steps.limit,
|
|
50
|
+
describe: guarded(guarded(steps.describe, guard), describeGuard),
|
|
51
|
+
stop: guarded(steps.stop, rests ? guard : undefined),
|
|
52
|
+
exhausted: guarded(steps.exhausted, rests ? guard : undefined),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
47
55
|
|
|
48
56
|
/**
|
|
49
57
|
* Hand a red suite that repeated attempts could not fix to a person: an
|
|
50
58
|
* agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
|
|
51
59
|
* on, the human is told the budget is spent instead.
|
|
52
60
|
*/
|
|
53
|
-
export const escalation = (
|
|
54
|
-
kit.
|
|
61
|
+
export const escalation = (
|
|
62
|
+
count: kit.EscalationCount,
|
|
63
|
+
guard?: Guard,
|
|
64
|
+
guardRests = true,
|
|
65
|
+
rules?: string,
|
|
66
|
+
): Promise<void> => kit.escalation(count, guardedSteps(guard, undefined, guardRests, rules))
|
|
55
67
|
|
|
56
68
|
/** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
|
|
57
69
|
export const retryQuestion = (comparable: boolean): JudgeQuestion =>
|
|
@@ -100,6 +112,9 @@ export interface HealthOptions {
|
|
|
100
112
|
readonly guard?: Guard | undefined
|
|
101
113
|
/** Runs after the escalation's agent turn only; the human rests stay unguarded. */
|
|
102
114
|
readonly describeGuard?: Guard | undefined
|
|
115
|
+
/** `false`: `guard` skips the human rests `health.stop` and `health.exhausted`. */
|
|
116
|
+
readonly guardRests?: boolean | undefined
|
|
117
|
+
readonly rules?: string | undefined
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
const runSuite = (options: HealthOptions): Promise<boolean> =>
|
|
@@ -123,7 +138,12 @@ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}):
|
|
|
123
138
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
124
139
|
sameFailure,
|
|
125
140
|
escalations: options.escalations ?? { rounds: 0 },
|
|
126
|
-
escalation: guardedSteps(
|
|
141
|
+
escalation: guardedSteps(
|
|
142
|
+
options.guard,
|
|
143
|
+
options.describeGuard,
|
|
144
|
+
options.guardRests,
|
|
145
|
+
options.rules,
|
|
146
|
+
),
|
|
127
147
|
})
|
|
128
148
|
|
|
129
149
|
/**
|
|
@@ -56,6 +56,24 @@ describe("the shipped workflow factories", () => {
|
|
|
56
56
|
})
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
+
it("build seamAllow's default from the test-file globs, byte-equal to the shipped string", () => {
|
|
60
|
+
expect(defaults.seamAllow).toBe(
|
|
61
|
+
"**/*.test.*, **/*.spec.*, **/*.feature, **/tests/**, **/test/**, **/__tests__/**, .gtd/**",
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("default refactorQualityReviews to the three refactor lenses and let a factory option win", () => {
|
|
66
|
+
expect(defaults.refactorQualityReviews).toBe("correctness, ponytail-review, conventions")
|
|
67
|
+
expect(configure(feature({ refactorQualityReviews: "correctness" }))).toMatchObject({
|
|
68
|
+
ok: true,
|
|
69
|
+
build: { defaults: { refactorQualityReviews: "correctness" } },
|
|
70
|
+
})
|
|
71
|
+
expect(configure(workflows.rollout({ refactorQualityReviews: "" }))).toMatchObject({
|
|
72
|
+
ok: true,
|
|
73
|
+
build: { defaults: { refactorQualityReviews: "" } },
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
59
77
|
it("build the shipped settings unchanged with no options", () => {
|
|
60
78
|
for (const factory of [feature, fix, review]) {
|
|
61
79
|
expect(configure(factory)).toMatchObject({ ok: true, build: { defaults, envDefaults } })
|
|
@@ -248,11 +248,13 @@ export const footnoteRules = `- Leave a footnote anywhere: mark the exact spot w
|
|
|
248
248
|
file changed in this process (\`#\`, \`--\` or \`;\` by language); the phone UI does not show them,
|
|
249
249
|
and \`gtd check --open-threads\` lists the open ones`
|
|
250
250
|
|
|
251
|
-
export const
|
|
251
|
+
export const footnoteAnchor = `- A footnote (\`[^name]\` plus its \`[^name]:\` definition) is a comment on
|
|
252
252
|
its exact anchor — fold it in as a mandatory concern described
|
|
253
253
|
against that anchor's hunk or paragraph, never flattened into a
|
|
254
254
|
whole-file remark
|
|
255
|
-
|
|
255
|
+
`
|
|
256
|
+
|
|
257
|
+
export const footnoteFoldIn = `${footnoteAnchor}- A one-shot footnote needs no reply: fold it in and DELETE it in this
|
|
256
258
|
same turn — marker and definition together. Never re-read a footnote
|
|
257
259
|
already acted on. A one-shot footnote that asks a question becomes a
|
|
258
260
|
thread instead: rewrite its definition as \`- H: <its body>\` then
|
|
@@ -39,6 +39,15 @@ describe("the bundled workflow's skills export", () => {
|
|
|
39
39
|
"build.quality.test-audit",
|
|
40
40
|
"build.quality.conventions",
|
|
41
41
|
"build.quality.spec-challenge",
|
|
42
|
+
"refactor.build",
|
|
43
|
+
"refactor.build.health",
|
|
44
|
+
"refactor.build.fix.quality",
|
|
45
|
+
"refactor.build.review",
|
|
46
|
+
"refactor.build.review.fix.nits",
|
|
47
|
+
"refactor.build.review.fix.risks",
|
|
48
|
+
"refactor.build.quality.correctness",
|
|
49
|
+
"refactor.build.quality.ponytail-review",
|
|
50
|
+
"refactor.build.quality.conventions",
|
|
42
51
|
].sort(),
|
|
43
52
|
)
|
|
44
53
|
})
|
|
@@ -51,7 +60,6 @@ describe("the bundled workflow's skills export", () => {
|
|
|
51
60
|
"build.quality",
|
|
52
61
|
"implement.fix",
|
|
53
62
|
"seam.judge",
|
|
54
|
-
"seam.approve",
|
|
55
63
|
"seam.steer",
|
|
56
64
|
"seam.by-hand",
|
|
57
65
|
]) {
|
|
@@ -84,6 +92,17 @@ describe("the bundled workflow's skills export", () => {
|
|
|
84
92
|
})
|
|
85
93
|
})
|
|
86
94
|
|
|
95
|
+
it("keys one refactor lens scope per refactorQualityReviews entry", () => {
|
|
96
|
+
const custom = skills({ ...defaults, refactorQualityReviews: " my-lens ,, correctness" })
|
|
97
|
+
expect(
|
|
98
|
+
Object.keys(custom)
|
|
99
|
+
.filter((k) => k.startsWith("refactor.build.quality."))
|
|
100
|
+
.sort(),
|
|
101
|
+
).toEqual(["refactor.build.quality.correctness", "refactor.build.quality.my-lens"])
|
|
102
|
+
const none = skills({ ...defaults, refactorQualityReviews: "" })
|
|
103
|
+
expect(Object.keys(none).filter((k) => k.startsWith("refactor.build.quality."))).toEqual([])
|
|
104
|
+
})
|
|
105
|
+
|
|
87
106
|
it("loads one lens skill per quality lens: correctness its review skill, conventions and spec-challenge none, any other itself", () => {
|
|
88
107
|
expect(bundled["build.quality.correctness"]).toEqual(["code-review-and-quality"])
|
|
89
108
|
expect(bundled["build.quality.conventions"]).toEqual([])
|
|
@@ -9,35 +9,46 @@ import { lensesOf, lensScopes } from "../../workflow-kit/index.js"
|
|
|
9
9
|
// `qualityReviews` entry, so the export is a function of the vars.
|
|
10
10
|
export const skills = (
|
|
11
11
|
vars: Readonly<Record<string, string>>,
|
|
12
|
-
): Readonly<Record<string, readonly string[]>> =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
12
|
+
): Readonly<Record<string, readonly string[]>> => {
|
|
13
|
+
const build: Record<string, readonly string[]> = {
|
|
14
|
+
build: ["debugging-and-error-recovery"],
|
|
15
|
+
"build.health": ["debugging-and-error-recovery"],
|
|
16
|
+
"build.fix.quality": ["incremental-implementation", "code-simplification"],
|
|
17
|
+
"build.review": ["code-review-and-quality"],
|
|
18
|
+
"build.review.fix.nits": ["incremental-implementation", "code-simplification"],
|
|
19
|
+
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
23
|
+
architecture: ["api-and-interface-design", "documentation-and-adrs", "ponytail"],
|
|
24
|
+
"architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
|
|
25
|
+
"packages.item": ["test-driven-development", "incremental-implementation"],
|
|
26
|
+
"packages.item.fix.suite": ["debugging-and-error-recovery"],
|
|
27
|
+
// `health` is shared code reached from both `packages.item` and `build`.
|
|
28
|
+
"packages.item.health": ["debugging-and-error-recovery"],
|
|
29
|
+
// `fix`'s turn over the notes a review handed on.
|
|
30
|
+
notes: ["incremental-implementation", "code-simplification"],
|
|
31
|
+
// The refactor's own turns load `ponytail`; `refactor.build.*` carry the `build.*` lists.
|
|
32
|
+
...build,
|
|
33
|
+
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|
|
34
|
+
refactor: ["ponytail"],
|
|
35
|
+
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
36
|
+
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
37
|
+
// Declared whether or not `naive_pass` is on, so a config keyed on them keeps
|
|
38
|
+
// loading when the flag flips off.
|
|
39
|
+
seam: ["incremental-implementation"],
|
|
40
|
+
implement: ["test-driven-development", "incremental-implementation"],
|
|
41
|
+
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
42
|
+
"implement.health": ["debugging-and-error-recovery"],
|
|
43
|
+
...lensScopes(
|
|
44
|
+
"build.quality",
|
|
45
|
+
lensesOf(vars.qualityReviews ?? ""),
|
|
46
|
+
(lens) => builtInLenses[lens]?.skills ?? [lens],
|
|
47
|
+
),
|
|
48
|
+
...lensScopes(
|
|
49
|
+
"refactor.build.quality",
|
|
50
|
+
lensesOf(vars.refactorQualityReviews ?? ""),
|
|
51
|
+
(lens) => builtInLenses[lens]?.skills ?? [lens],
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { changesSince, env, human } from "../../flows/index.js"
|
|
1
|
+
import { changesSince, env, human, scope, type ScopeAccess } from "../../flows/index.js"
|
|
2
2
|
import { removedAssertions } from "../../workflow-kit/index.js"
|
|
3
3
|
import * as t from "./text.js"
|
|
4
4
|
|
|
@@ -12,6 +12,7 @@ export const ESCALATION = ".gtd/ESCALATION.md"
|
|
|
12
12
|
export const REQUIREMENTS = ".gtd/REQUIREMENTS.md"
|
|
13
13
|
export const ARCHITECTURE = ".gtd/ARCHITECTURE.md"
|
|
14
14
|
export const REVIEW = ".gtd/REVIEW.md"
|
|
15
|
+
export const REFACTOR = ".gtd/REFACTOR.md"
|
|
15
16
|
export const QUALITY = ".gtd/QUALITY.md"
|
|
16
17
|
|
|
17
18
|
export const planner = (): string => env.plannerModel ?? ""
|
|
@@ -19,16 +20,16 @@ export const coder = (): string => env.coderModel ?? ""
|
|
|
19
20
|
|
|
20
21
|
// ── Keeping the suite green ─────────────────────────────────────────────────
|
|
21
22
|
|
|
22
|
-
export const fixCheck = (): Promise<void> =>
|
|
23
|
-
t.agentWithSkills("fix", t.buildFixPrompt(), {
|
|
23
|
+
export const fixCheck = (rules?: string): Promise<void> =>
|
|
24
|
+
t.agentWithSkills("fix", t.buildFixPrompt(rules), {
|
|
24
25
|
label: "Fixing the check",
|
|
25
26
|
file: FEEDBACK,
|
|
26
27
|
model: coder(),
|
|
27
28
|
system: t.finisherSystem(),
|
|
28
29
|
})
|
|
29
30
|
|
|
30
|
-
export const describeEscalation = (): Promise<void> =>
|
|
31
|
-
t.agentWithSkills("health.describe", t.healthDescribePrompt(), {
|
|
31
|
+
export const describeEscalation = (rules?: string): Promise<void> =>
|
|
32
|
+
t.agentWithSkills("health.describe", t.healthDescribePrompt(rules), {
|
|
32
33
|
label: "Describing the escalation",
|
|
33
34
|
file: FEEDBACK,
|
|
34
35
|
model: coder(),
|
|
@@ -67,10 +68,10 @@ export const builtInLenses: Readonly<Record<string, BuiltInLens>> = {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/** One quality review through `lens`; each lens is its own scope, so its own conversation and skills. */
|
|
70
|
-
export const reviewQuality = (lens: string): Promise<void> =>
|
|
71
|
+
export const reviewQuality = (lens: string, base?: string): Promise<void> =>
|
|
71
72
|
t.agentWithSkills(
|
|
72
73
|
`quality.${lens}.reviewing`,
|
|
73
|
-
t.buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief),
|
|
74
|
+
t.buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base),
|
|
74
75
|
{
|
|
75
76
|
label: "Reviewing (one quality lens)",
|
|
76
77
|
file: QUALITY,
|
|
@@ -80,8 +81,8 @@ export const reviewQuality = (lens: string): Promise<void> =>
|
|
|
80
81
|
},
|
|
81
82
|
)
|
|
82
83
|
|
|
83
|
-
export const fixQuality = (): Promise<void> =>
|
|
84
|
-
t.agentWithSkills("fix.quality.fixing", t.buildFixQualityPrompt(), {
|
|
84
|
+
export const fixQuality = (rules?: string): Promise<void> =>
|
|
85
|
+
t.agentWithSkills("fix.quality.fixing", t.buildFixQualityPrompt(rules), {
|
|
85
86
|
label: "Fixing quality findings",
|
|
86
87
|
file: QUALITY,
|
|
87
88
|
model: coder(),
|
|
@@ -90,10 +91,14 @@ export const fixQuality = (): Promise<void> =>
|
|
|
90
91
|
})
|
|
91
92
|
|
|
92
93
|
/** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
|
|
93
|
-
export const reviewing = (
|
|
94
|
+
export const reviewing = (
|
|
95
|
+
base: string,
|
|
96
|
+
carry?: string,
|
|
97
|
+
lap: t.ReviewRange = "first",
|
|
98
|
+
): Promise<void> =>
|
|
94
99
|
t.agentWithSkills(
|
|
95
100
|
"review.reviewing",
|
|
96
|
-
t.buildReviewReviewingPrompt(base, carry, removedAssertions(changesSince(base))),
|
|
101
|
+
t.buildReviewReviewingPrompt(base, lap, carry, removedAssertions(changesSince(base))),
|
|
97
102
|
{
|
|
98
103
|
label: "Reviewing",
|
|
99
104
|
file: REVIEW,
|
|
@@ -105,14 +110,23 @@ export const reviewing = (base: string, carry?: string): Promise<void> =>
|
|
|
105
110
|
)
|
|
106
111
|
|
|
107
112
|
/** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
|
|
108
|
-
export const fixRisks = (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
export const fixRisks = (
|
|
114
|
+
notes: readonly t.NoteInput[],
|
|
115
|
+
access?: ScopeAccess,
|
|
116
|
+
rules?: string,
|
|
117
|
+
): Promise<void> => {
|
|
118
|
+
const fixing = (name: string): Promise<void> =>
|
|
119
|
+
t.agentWithSkills(name, t.buildReviewFixRisksPrompt(notes, rules), {
|
|
120
|
+
label: "Fixing the reviewer's risks",
|
|
121
|
+
file: REVIEW,
|
|
122
|
+
model: planner(),
|
|
123
|
+
system: t.reviewerSystem(),
|
|
124
|
+
allowEmpty: true,
|
|
125
|
+
})
|
|
126
|
+
return access === undefined
|
|
127
|
+
? fixing("review.fix.risks.fixing")
|
|
128
|
+
: scope({ name: "review.fix.risks", access }, () => fixing("fixing"))
|
|
129
|
+
}
|
|
116
130
|
|
|
117
131
|
export const awaitReview = (
|
|
118
132
|
base: string,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type StepRequest,
|
|
7
7
|
} from "../../flows/index.js"
|
|
8
8
|
import type { Guard } from "./health.js"
|
|
9
|
-
import { handOffReview, REVIEW_ORIG } from "./tail.js"
|
|
9
|
+
import { handOffReview, qualityLap, REVIEW_ORIG, tail } from "./tail.js"
|
|
10
10
|
import { fixtureContext } from "./text.fixture.js"
|
|
11
11
|
|
|
12
12
|
afterEach(() => installContext(undefined))
|
|
@@ -206,3 +206,86 @@ describe("the hand-off review", () => {
|
|
|
206
206
|
})
|
|
207
207
|
})
|
|
208
208
|
})
|
|
209
|
+
|
|
210
|
+
const prompts: string[] = []
|
|
211
|
+
|
|
212
|
+
describe("the quality lap's lenses", () => {
|
|
213
|
+
const lap = async (run: () => Promise<unknown>, vars: Record<string, string> = {}) => {
|
|
214
|
+
const log: string[] = []
|
|
215
|
+
prompts.length = 0
|
|
216
|
+
installContext(
|
|
217
|
+
fixtureContext(
|
|
218
|
+
{ vars },
|
|
219
|
+
{
|
|
220
|
+
step: (request: StepRequest) => {
|
|
221
|
+
if (request.kind !== "restart") log.push(request.name)
|
|
222
|
+
if (request.kind === "agent") prompts.push(request.prompt)
|
|
223
|
+
return Promise.resolve()
|
|
224
|
+
},
|
|
225
|
+
pushScope: () => undefined,
|
|
226
|
+
popScope: () => undefined,
|
|
227
|
+
start: () => "START-SHA",
|
|
228
|
+
},
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
await run()
|
|
232
|
+
return log
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
it("default to the qualityReviews setting", async () => {
|
|
236
|
+
const log = await lap(() => qualityLap(), { qualityReviews: "correctness, conventions" })
|
|
237
|
+
expect(log).toEqual(["quality.correctness.reviewing", "quality.conventions.reviewing"])
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it("lenses override qualityReviews", async () => {
|
|
241
|
+
const log = await lap(() => qualityLap(undefined, ["ponytail-review"]), {
|
|
242
|
+
qualityReviews: "correctness, conventions",
|
|
243
|
+
})
|
|
244
|
+
expect(log).toEqual(["quality.ponytail-review.reviewing"])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it("an empty list runs no lens turn", async () => {
|
|
248
|
+
const log = await lap(() => qualityLap(undefined, []), { qualityReviews: "correctness" })
|
|
249
|
+
expect(log).toEqual([])
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it("lensBase reaches every lens turn's prompt; absent, they name start()", async () => {
|
|
253
|
+
await lap(() => qualityLap(undefined, ["conventions", "correctness"], "BASE-SHA"))
|
|
254
|
+
expect(prompts).toHaveLength(2)
|
|
255
|
+
for (const prompt of prompts) expect(prompt).toContain("from `BASE-SHA`")
|
|
256
|
+
await lap(() => qualityLap(undefined, ["conventions", "correctness"]))
|
|
257
|
+
expect(prompts).toHaveLength(2)
|
|
258
|
+
for (const prompt of prompts) expect(prompt).toContain("from `START-SHA`")
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it("tail threads lensBase to every lens turn", async () => {
|
|
262
|
+
await lap(() =>
|
|
263
|
+
tail(
|
|
264
|
+
false,
|
|
265
|
+
() => Promise.resolve(),
|
|
266
|
+
true,
|
|
267
|
+
undefined,
|
|
268
|
+
["conventions", "correctness"],
|
|
269
|
+
true,
|
|
270
|
+
"BASE-SHA",
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
expect(prompts).toHaveLength(2)
|
|
274
|
+
for (const prompt of prompts) expect(prompt).toContain("from `BASE-SHA`")
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it("tail passes lenses to its lap and a guard after each lens turn", async () => {
|
|
278
|
+
let guards = 0
|
|
279
|
+
const log = await lap(() =>
|
|
280
|
+
tail(
|
|
281
|
+
false,
|
|
282
|
+
() => Promise.resolve(),
|
|
283
|
+
true,
|
|
284
|
+
() => void guards++,
|
|
285
|
+
["conventions"],
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
expect(log).toEqual(["quality.conventions.reviewing"])
|
|
289
|
+
expect(guards).toBe(1)
|
|
290
|
+
})
|
|
291
|
+
})
|