@pmelab/gtd 17.3.0 → 18.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 +3 -2
- package/claude/hooks/drive.ts +5 -0
- package/claude/hooks/persona.ts +28 -0
- package/claude/hooks/register.tsx +23 -19
- package/claude/hooks/wording.ts +3 -0
- package/claude/types/index.d.ts +5 -1
- package/dist/gtd.bundle.mjs +248 -263
- package/package.json +1 -1
- package/schema.json +1 -1
- package/skills/authoring/SKILL.md +6 -4
- package/src/flows/runtime.ts +6 -14
- package/src/workflows/review.test.ts +13 -13
- package/src/workflows/review.ts +4 -4
- package/src/workflows/skills.test.ts +67 -81
- package/src/workflows/skills.ts +33 -37
- package/src/workflows/steps.test.ts +56 -78
- package/src/workflows/steps.ts +10 -19
- package/src/workflows/text.fixture.ts +14 -29
- package/src/workflows/text.test.ts +6 -14
- package/src/workflows/text.ts +3 -14
- package/src/workflows/vars.ts +2 -4
|
@@ -5,95 +5,83 @@ import * as steps from "./steps.js"
|
|
|
5
5
|
|
|
6
6
|
const capture = (
|
|
7
7
|
fn: () => Promise<void>,
|
|
8
|
+
scope: string,
|
|
8
9
|
skills?: Readonly<Record<string, readonly string[]>>,
|
|
9
|
-
): Promise<StepRequest> => captureStep(fn, skills !== undefined ? { skills } : {})
|
|
10
|
+
): Promise<StepRequest> => captureStep(fn, { scope, ...(skills !== undefined ? { skills } : {}) })
|
|
10
11
|
|
|
11
12
|
const agentPrompt = (request: StepRequest): string => {
|
|
12
13
|
if (request.kind !== "agent") throw new Error(`expected an agent step, got ${request.kind}`)
|
|
13
14
|
return request.prompt
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
// The preamble (`agentPrompt`) reflects `skillsFor`'s resolved map — but the
|
|
17
|
-
// WIRE `skills` field a request actually carries is whatever `options.skills`
|
|
18
|
-
// the step itself passed; `Replay.ts`'s `resolve()` only overrides it from
|
|
19
|
-
// `.gtdrc` (never reachable here — this fixture skips replay entirely). Most
|
|
20
|
-
// bundled steps pass no `skills` option of their own, so this is normally
|
|
21
|
-
// `undefined`; `reviewQuality` is the one step that does.
|
|
22
|
-
const agentSkillsOption = (request: StepRequest): readonly string[] | undefined => {
|
|
23
|
-
if (request.kind !== "agent") throw new Error(`expected an agent step, got ${request.kind}`)
|
|
24
|
-
return request.options.skills
|
|
25
|
-
}
|
|
26
|
-
|
|
27
17
|
describe("the bundled workflow's steps declare skills — a bundled step's rendered prompt carries its bundled skills.ts preamble", () => {
|
|
28
18
|
it("triage carries design.triage's bundled skills", async () => {
|
|
29
|
-
const prompt = agentPrompt(await capture(() => steps.triage("base")))
|
|
19
|
+
const prompt = agentPrompt(await capture(() => steps.triage("base"), "design"))
|
|
30
20
|
expect(prompt).toContain("spec-driven-development, planning-and-task-breakdown")
|
|
31
21
|
})
|
|
32
22
|
|
|
33
23
|
it("author carries architecture.author's bundled skills", async () => {
|
|
34
|
-
const prompt = agentPrompt(await capture(() => steps.author()))
|
|
24
|
+
const prompt = agentPrompt(await capture(() => steps.author(), "architecture"))
|
|
35
25
|
expect(prompt).toContain("api-and-interface-design, documentation-and-adrs, ponytail")
|
|
36
26
|
})
|
|
37
27
|
|
|
38
28
|
it("decompose carries architecture.decompose's bundled skills", async () => {
|
|
39
|
-
const prompt = agentPrompt(await capture(() => steps.decompose()))
|
|
29
|
+
const prompt = agentPrompt(await capture(() => steps.decompose(), "architecture"))
|
|
40
30
|
expect(prompt).toContain("incremental-implementation, planning-and-task-breakdown")
|
|
41
31
|
})
|
|
42
32
|
|
|
43
33
|
it("build carries packages.item.building's bundled skills", async () => {
|
|
44
|
-
const prompt = agentPrompt(await capture(() => steps.build("pkg")))
|
|
34
|
+
const prompt = agentPrompt(await capture(() => steps.build("pkg"), "packages.item"))
|
|
45
35
|
expect(prompt).toContain("test-driven-development, incremental-implementation")
|
|
46
36
|
})
|
|
47
37
|
|
|
48
|
-
it("fixSuite carries packages.item.fix
|
|
49
|
-
const prompt = agentPrompt(await capture(() => steps.fixSuite()))
|
|
38
|
+
it("fixSuite carries packages.item.fix.suite's bundled skills", async () => {
|
|
39
|
+
const prompt = agentPrompt(await capture(() => steps.fixSuite(), "packages.item"))
|
|
50
40
|
expect(prompt).toContain("debugging-and-error-recovery")
|
|
51
41
|
})
|
|
52
42
|
|
|
53
|
-
it("fixSpec carries packages.item.fix
|
|
54
|
-
const prompt = agentPrompt(await capture(() => steps.fixSpec("pkg")))
|
|
43
|
+
it("fixSpec carries packages.item.fix.spec's bundled skills", async () => {
|
|
44
|
+
const prompt = agentPrompt(await capture(() => steps.fixSpec("pkg"), "packages.item"))
|
|
55
45
|
expect(prompt).toContain("incremental-implementation, code-simplification")
|
|
56
46
|
})
|
|
57
47
|
|
|
58
48
|
it("reviewPackage carries packages.item.spec.review's bundled skills", async () => {
|
|
59
|
-
const prompt = agentPrompt(await capture(() => steps.reviewPackage("pkg")))
|
|
49
|
+
const prompt = agentPrompt(await capture(() => steps.reviewPackage("pkg"), "packages.item"))
|
|
60
50
|
expect(prompt).toContain("code-review-and-quality, spec-driven-development")
|
|
61
51
|
})
|
|
62
52
|
|
|
63
53
|
it("fix carries build.fix's bundled skills", async () => {
|
|
64
|
-
const prompt = agentPrompt(await capture(() => steps.fix()))
|
|
54
|
+
const prompt = agentPrompt(await capture(() => steps.fix(), "build"))
|
|
65
55
|
expect(prompt).toContain("debugging-and-error-recovery")
|
|
66
56
|
})
|
|
67
57
|
|
|
68
58
|
it("describeEscalation carries health.describe's bundled skills", async () => {
|
|
69
|
-
const prompt = agentPrompt(await capture(() => steps.describeEscalation()))
|
|
59
|
+
const prompt = agentPrompt(await capture(() => steps.describeEscalation(), "build"))
|
|
70
60
|
expect(prompt).toContain("debugging-and-error-recovery")
|
|
71
61
|
})
|
|
72
62
|
|
|
73
|
-
it("reviewQuality
|
|
74
|
-
const
|
|
75
|
-
|
|
63
|
+
it("reviewQuality names its lens scope, build.quality.<lens>.reviewing", async () => {
|
|
64
|
+
const request = await capture(() => steps.reviewQuality("owasp-security"), "build")
|
|
65
|
+
if (request.kind !== "agent") throw new Error("unreachable")
|
|
66
|
+
expect(request.name).toBe("quality.owasp-security.reviewing")
|
|
76
67
|
})
|
|
77
68
|
|
|
78
|
-
it("
|
|
79
|
-
const
|
|
80
|
-
expect(
|
|
69
|
+
it("an unknown lens's preamble names the lens itself, from the bundled per-lens key", async () => {
|
|
70
|
+
const prompt = agentPrompt(await capture(() => steps.reviewQuality("owasp-security"), "build"))
|
|
71
|
+
expect(prompt).toContain("missing one: owasp-security")
|
|
81
72
|
})
|
|
82
73
|
|
|
83
|
-
it("
|
|
74
|
+
it("a configured build.quality.<lens> entry replaces the lens's skills in the preamble, every other lens untouched", async () => {
|
|
75
|
+
const skills = { "build.quality.owasp-security": ["my-org-checklist"] }
|
|
84
76
|
const prompt = agentPrompt(
|
|
85
|
-
await capture(() => steps.reviewQuality("owasp-security"),
|
|
86
|
-
"quality.reviewing": ["my-org-checklist"],
|
|
87
|
-
}),
|
|
77
|
+
await capture(() => steps.reviewQuality("owasp-security"), "build", skills),
|
|
88
78
|
)
|
|
89
79
|
expect(prompt).toContain("my-org-checklist")
|
|
90
|
-
// The body still names the lens — what makes N otherwise identical prompts distinguishable.
|
|
91
80
|
expect(prompt).toContain("owasp-security")
|
|
92
81
|
})
|
|
93
82
|
|
|
94
|
-
it("correctness
|
|
95
|
-
const request = await capture(() => steps.reviewQuality("correctness"))
|
|
96
|
-
expect(agentSkillsOption(request)).toEqual(["code-review-and-quality"])
|
|
83
|
+
it("correctness carries all four trace points", async () => {
|
|
84
|
+
const request = await capture(() => steps.reviewQuality("correctness"), "build")
|
|
97
85
|
const body = JSON.stringify(request)
|
|
98
86
|
for (const point of [
|
|
99
87
|
"partial-failure",
|
|
@@ -104,57 +92,44 @@ describe("the bundled workflow's steps declare skills — a bundled step's rende
|
|
|
104
92
|
expect(body).toContain(point)
|
|
105
93
|
})
|
|
106
94
|
|
|
107
|
-
it.each(["conventions", "spec-challenge"])(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
expect(JSON.stringify(request)).toContain(
|
|
113
|
-
steps.builtInLenses[lens]!.brief.split("\n")[0]!.slice(0, 40),
|
|
114
|
-
)
|
|
115
|
-
},
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
it("an unknown lens loads itself as a skill, with no brief", async () => {
|
|
119
|
-
const request = await capture(() => steps.reviewQuality("my-lens"))
|
|
120
|
-
expect(agentSkillsOption(request)).toEqual(["my-lens"])
|
|
121
|
-
expect(JSON.stringify(request)).not.toContain("This lens's brief")
|
|
95
|
+
it.each(["conventions", "spec-challenge"])("%s carries its brief", async (lens) => {
|
|
96
|
+
const request = await capture(() => steps.reviewQuality(lens), "build")
|
|
97
|
+
expect(JSON.stringify(request)).toContain(
|
|
98
|
+
steps.builtInLenses[lens]!.brief.split("\n")[0]!.slice(0, 40),
|
|
99
|
+
)
|
|
122
100
|
})
|
|
123
101
|
|
|
124
|
-
it("
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
"quality.reviewing": ["my-org-checklist"],
|
|
128
|
-
}),
|
|
129
|
-
)
|
|
130
|
-
expect(prompt).toContain("my-org-checklist")
|
|
131
|
-
expect(prompt).toContain("AGENTS.md")
|
|
102
|
+
it("an unknown lens has no brief", async () => {
|
|
103
|
+
const request = await capture(() => steps.reviewQuality("my-lens"), "build")
|
|
104
|
+
expect(JSON.stringify(request)).not.toContain("This lens's brief")
|
|
132
105
|
})
|
|
133
106
|
|
|
134
|
-
it("fixQuality carries build.fix
|
|
135
|
-
const prompt = agentPrompt(await capture(() => steps.fixQuality()))
|
|
107
|
+
it("fixQuality carries build.fix.quality's bundled skills", async () => {
|
|
108
|
+
const prompt = agentPrompt(await capture(() => steps.fixQuality(), "build"))
|
|
136
109
|
expect(prompt).toContain("incremental-implementation, code-simplification")
|
|
137
110
|
})
|
|
138
111
|
|
|
139
112
|
it("reviewing carries build.review.reviewing's bundled skills", async () => {
|
|
140
|
-
const prompt = agentPrompt(await capture(() => steps.reviewing("base")))
|
|
113
|
+
const prompt = agentPrompt(await capture(() => steps.reviewing("base"), "build"))
|
|
141
114
|
expect(prompt).toContain("code-review-and-quality")
|
|
142
115
|
})
|
|
143
116
|
|
|
144
117
|
it("collecting carries build.review.collecting's bundled skills", async () => {
|
|
145
|
-
const prompt = agentPrompt(await capture(() => steps.collecting("capture")))
|
|
118
|
+
const prompt = agentPrompt(await capture(() => steps.collecting("capture"), "build"))
|
|
146
119
|
expect(prompt).toContain("code-review-and-quality")
|
|
147
120
|
})
|
|
148
121
|
|
|
149
122
|
it("an empty configured list leaves that step's skills empty, and its rendered prompt bare", async () => {
|
|
150
|
-
const request = await capture(() => steps.fix(), {
|
|
123
|
+
const request = await capture(() => steps.fix(), "build", { build: [] })
|
|
151
124
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
152
125
|
expect(request.prompt).not.toContain("Load whatever's listed here")
|
|
153
126
|
})
|
|
154
127
|
|
|
155
128
|
it("answerReviewQuestions answers inline in .gtd/REVIEW.md as the reviewer, with its bundled skills", async () => {
|
|
156
|
-
const request = await capture(
|
|
157
|
-
|
|
129
|
+
const request = await capture(
|
|
130
|
+
() =>
|
|
131
|
+
steps.answerReviewQuestions([{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "why?" }]),
|
|
132
|
+
"build",
|
|
158
133
|
)
|
|
159
134
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
160
135
|
expect(request.name).toBe("review.answer-review-questions")
|
|
@@ -169,14 +144,16 @@ describe("the bundled workflow's steps declare skills — a bundled step's rende
|
|
|
169
144
|
})
|
|
170
145
|
|
|
171
146
|
it("fixNits fixes every nit in one turn and leaves .gtd/REVIEW.md alone", async () => {
|
|
172
|
-
const request = await capture(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
147
|
+
const request = await capture(
|
|
148
|
+
() =>
|
|
149
|
+
steps.fixNits([
|
|
150
|
+
{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "typo" },
|
|
151
|
+
{ id: "note-2", anchor: "calc ./a.ts#2-2", text: "semicolon" },
|
|
152
|
+
]),
|
|
153
|
+
"build",
|
|
177
154
|
)
|
|
178
155
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
179
|
-
expect(request.name).toBe("review.fix
|
|
156
|
+
expect(request.name).toBe("review.fix.nits.fixing")
|
|
180
157
|
expect(request.prompt).toContain("incremental-implementation, code-simplification")
|
|
181
158
|
expect(request.prompt).toContain("typo")
|
|
182
159
|
expect(request.prompt).toContain("semicolon")
|
|
@@ -184,11 +161,12 @@ describe("the bundled workflow's steps declare skills — a bundled step's rende
|
|
|
184
161
|
})
|
|
185
162
|
|
|
186
163
|
it("fixRisks fixes every risk, tolerates an empty turn, and leaves .gtd/REVIEW.md alone", async () => {
|
|
187
|
-
const request = await capture(
|
|
188
|
-
steps.fixRisks([{ id: "risk-1", anchor: "calc ./a.ts#1-1", text: "Risk: drops it" }]),
|
|
164
|
+
const request = await capture(
|
|
165
|
+
() => steps.fixRisks([{ id: "risk-1", anchor: "calc ./a.ts#1-1", text: "Risk: drops it" }]),
|
|
166
|
+
"build",
|
|
189
167
|
)
|
|
190
168
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
191
|
-
expect(request.name).toBe("review.fix
|
|
169
|
+
expect(request.name).toBe("review.fix.risks.fixing")
|
|
192
170
|
expect(request.options).toMatchObject({ allowEmpty: true })
|
|
193
171
|
expect(request.prompt).toContain("debugging-and-error-recovery, incremental-implementation")
|
|
194
172
|
expect(request.prompt).toContain("risk-1 — calc ./a.ts#1-1")
|
|
@@ -197,8 +175,8 @@ describe("the bundled workflow's steps declare skills — a bundled step's rende
|
|
|
197
175
|
})
|
|
198
176
|
|
|
199
177
|
it("reviewing names the carry-over commit only when given one", async () => {
|
|
200
|
-
const bare = await capture(() => steps.reviewing("base"))
|
|
201
|
-
const carried = await capture(() => steps.reviewing("base", "abc1234"))
|
|
178
|
+
const bare = await capture(() => steps.reviewing("base"), "build")
|
|
179
|
+
const carried = await capture(() => steps.reviewing("base", "abc1234"), "build")
|
|
202
180
|
if (bare.kind !== "agent" || carried.kind !== "agent") throw new Error("unreachable")
|
|
203
181
|
expect(bare.prompt).not.toContain("Carry-over")
|
|
204
182
|
expect(carried.prompt).toContain("Carry-over: commit `abc1234`")
|
package/src/workflows/steps.ts
CHANGED
|
@@ -39,7 +39,7 @@ export const author = (): Promise<void> =>
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
export const decompose = (): Promise<void> =>
|
|
42
|
-
t.agentWithSkills("decompose", t.architectureDecomposePrompt(), {
|
|
42
|
+
t.agentWithSkills("decompose.decomposing", t.architectureDecomposePrompt(), {
|
|
43
43
|
label: "Decomposing into packages",
|
|
44
44
|
model: planner(),
|
|
45
45
|
system: t.architectSystem(),
|
|
@@ -73,7 +73,7 @@ export const build = (pkg: string): Promise<void> =>
|
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
export const fixSuite = (): Promise<void> =>
|
|
76
|
-
t.agentWithSkills("fix
|
|
76
|
+
t.agentWithSkills("fix.suite.fixing", t.packagesItemFixSuitePrompt(), {
|
|
77
77
|
label: "Fixing the check",
|
|
78
78
|
file: FEEDBACK,
|
|
79
79
|
model: coder(),
|
|
@@ -81,7 +81,7 @@ export const fixSuite = (): Promise<void> =>
|
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
export const fixSpec = (pkg: string): Promise<void> =>
|
|
84
|
-
t.agentWithSkills("fix
|
|
84
|
+
t.agentWithSkills("fix.spec.fixing", t.packagesItemFixSpecPrompt(pkg), {
|
|
85
85
|
label: "Fixing review feedback",
|
|
86
86
|
file: SPEC_FEEDBACK,
|
|
87
87
|
model: coder(),
|
|
@@ -146,15 +146,10 @@ export const builtInLenses: Readonly<Record<string, BuiltInLens>> = {
|
|
|
146
146
|
"spec-challenge": { skills: [], brief: t.specChallengeBrief },
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
/**
|
|
150
|
-
* One quality review, through the skill `lens`. `lens` rides as this turn's
|
|
151
|
-
* own `skills` option — a `.gtdrc` `build.quality.reviewing` entry still
|
|
152
|
-
* overrides it (config beats a flow-supplied list same as any other step),
|
|
153
|
-
* but absent one the lens itself is what the turn loads by default.
|
|
154
|
-
*/
|
|
149
|
+
/** One quality review through `lens`; each lens is its own scope, so its own conversation and skills. */
|
|
155
150
|
export const reviewQuality = (lens: string): Promise<void> =>
|
|
156
151
|
t.agentWithSkills(
|
|
157
|
-
|
|
152
|
+
`quality.${lens}.reviewing`,
|
|
158
153
|
t.buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief),
|
|
159
154
|
{
|
|
160
155
|
label: "Reviewing (one quality lens)",
|
|
@@ -162,12 +157,11 @@ export const reviewQuality = (lens: string): Promise<void> =>
|
|
|
162
157
|
model: planner(),
|
|
163
158
|
system: t.reviewerSystem(),
|
|
164
159
|
allowEmpty: true,
|
|
165
|
-
skills: builtInLenses[lens]?.skills ?? [lens],
|
|
166
160
|
},
|
|
167
161
|
)
|
|
168
162
|
|
|
169
163
|
export const fixQuality = (): Promise<void> =>
|
|
170
|
-
t.agentWithSkills("fix
|
|
164
|
+
t.agentWithSkills("fix.quality.fixing", t.buildFixQualityPrompt(), {
|
|
171
165
|
label: "Fixing quality findings",
|
|
172
166
|
file: QUALITY,
|
|
173
167
|
model: coder(),
|
|
@@ -196,21 +190,18 @@ export const answerReviewQuestions = (notes: readonly t.NoteInput[]): Promise<vo
|
|
|
196
190
|
system: t.reviewerSystem(),
|
|
197
191
|
})
|
|
198
192
|
|
|
199
|
-
/**
|
|
200
|
-
* Fix every `nit` note in one turn. Its name puts it in the `build.review`
|
|
201
|
-
* conversation, which has one identity — so it runs as the reviewer, not the coder.
|
|
202
|
-
*/
|
|
193
|
+
/** Fix every `nit` note in one turn. */
|
|
203
194
|
export const fixNits = (notes: readonly t.NoteInput[]): Promise<void> =>
|
|
204
|
-
t.agentWithSkills("review.fix
|
|
195
|
+
t.agentWithSkills("review.fix.nits.fixing", t.buildReviewFixNitsPrompt(notes), {
|
|
205
196
|
label: "Fixing your nits",
|
|
206
197
|
file: REVIEW,
|
|
207
198
|
model: planner(),
|
|
208
199
|
system: t.reviewerSystem(),
|
|
209
200
|
})
|
|
210
201
|
|
|
211
|
-
/** Fix the `Risk:`-marked notes the reviewer named
|
|
202
|
+
/** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
|
|
212
203
|
export const fixRisks = (notes: readonly t.NoteInput[]): Promise<void> =>
|
|
213
|
-
t.agentWithSkills("review.fix
|
|
204
|
+
t.agentWithSkills("review.fix.risks.fixing", t.buildReviewFixRisksPrompt(notes), {
|
|
214
205
|
label: "Fixing the reviewer's risks",
|
|
215
206
|
file: REVIEW,
|
|
216
207
|
model: planner(),
|
|
@@ -14,15 +14,9 @@ export interface TextContext {
|
|
|
14
14
|
readonly start?: string
|
|
15
15
|
readonly codeThreads?: readonly CodeThreadInfo[]
|
|
16
16
|
readonly read?: (path: string) => string | undefined
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* name is also the only name here) — stands in for a `.gtdrc` entry, so
|
|
21
|
-
* only the key it names is overridden; every other step still falls back
|
|
22
|
-
* to the call's own `skills` option, then the bundled map (`./skills.ts`),
|
|
23
|
-
* so an existing step test keeps its skills without having to declare them
|
|
24
|
-
* itself.
|
|
25
|
-
*/
|
|
17
|
+
/** The scope the step runs in (`"build.review"`); the fixture pushes no scopes, so this stands in for the caller's. */
|
|
18
|
+
readonly scope?: string
|
|
19
|
+
/** Stands in for `.gtdrc` `skills:`, keyed by scope full name. */
|
|
26
20
|
readonly skills?: Readonly<Record<string, readonly string[]>>
|
|
27
21
|
}
|
|
28
22
|
|
|
@@ -30,28 +24,19 @@ const unavailable = (): never => {
|
|
|
30
24
|
throw new Error("not available while rendering a text outside a replay")
|
|
31
25
|
}
|
|
32
26
|
|
|
33
|
-
/**
|
|
34
|
-
const bundledSkillsFor = (localName: string): readonly string[] => {
|
|
35
|
-
const exact = bundledSkills[localName]
|
|
36
|
-
if (exact !== undefined) return exact
|
|
37
|
-
const suffix = `.${localName}`
|
|
38
|
-
const hit = Object.entries(bundledSkills).find(([key]) => key.endsWith(suffix))
|
|
39
|
-
return hit?.[1] ?? []
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Mirrors `Replay.ts`'s `resolveSkills` three-way precedence: `context.skills`
|
|
44
|
-
* (a per-key override, standing in for `.gtdrc`) beats `ownSkills` (a call's
|
|
45
|
-
* own `skills` option), which beats the bundled default — so a step test that
|
|
46
|
-
* passes its own `skills` option (like `reviewQuality`'s lens) sees it in the
|
|
47
|
-
* preamble too, the same as a real replay would.
|
|
48
|
-
*/
|
|
27
|
+
/** Mirrors `Replay.ts`'s scope walk: for each prefix of the step's memory scope, innermost first, a `.gtdrc` entry, then the bundled export. */
|
|
49
28
|
const skillsForOf =
|
|
50
29
|
(context: TextContext) =>
|
|
51
|
-
(localName: string
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
30
|
+
(localName: string): readonly string[] => {
|
|
31
|
+
const full = [context.scope, localName].filter((part) => part !== undefined && part !== "")
|
|
32
|
+
const parts = full.join(".").split(".").slice(0, -1)
|
|
33
|
+
const bundled = bundledSkills({ ...defaults, ...context.vars })
|
|
34
|
+
for (let n = parts.length; n >= 0; n--) {
|
|
35
|
+
const prefix = parts.slice(0, n).join(".")
|
|
36
|
+
const hit = context.skills?.[prefix] ?? bundled[prefix]
|
|
37
|
+
if (hit !== undefined) return hit
|
|
38
|
+
}
|
|
39
|
+
return []
|
|
55
40
|
}
|
|
56
41
|
|
|
57
42
|
/** The fixed replay context texts render against; steps, refusals and scopes are unavailable unless overridden. */
|
|
@@ -33,11 +33,11 @@ describe("withSkills", () => {
|
|
|
33
33
|
|
|
34
34
|
describe("agentWithSkills", () => {
|
|
35
35
|
const capture = (fn: () => Promise<void>, skills?: Readonly<Record<string, readonly string[]>>) =>
|
|
36
|
-
captureStep(fn, skills !== undefined ? { skills } : {})
|
|
36
|
+
captureStep(fn, { scope: "a", ...(skills !== undefined ? { skills } : {}) })
|
|
37
37
|
|
|
38
|
-
it("puts
|
|
38
|
+
it("puts the scope's list, joined, in the preamble", async () => {
|
|
39
39
|
const request = await capture(() => agentWithSkills("name", "do-the-work"), {
|
|
40
|
-
|
|
40
|
+
a: ["code-review", "testing"],
|
|
41
41
|
})
|
|
42
42
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
43
43
|
expect(request.prompt).toBe(withSkills("code-review, testing", "do-the-work"))
|
|
@@ -46,22 +46,14 @@ describe("agentWithSkills", () => {
|
|
|
46
46
|
it("passes no skills option itself — the replay resolver fills the wire field", async () => {
|
|
47
47
|
const request = await capture(() => agentWithSkills("name", "do-the-work"))
|
|
48
48
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
49
|
-
expect(request.options
|
|
49
|
+
expect(request.options).not.toHaveProperty("skills")
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
it("leaves the prompt bare when the
|
|
53
|
-
const request = await capture(() => agentWithSkills("name", "do-the-work"), {
|
|
52
|
+
it("leaves the prompt bare when the scope resolves to an empty list", async () => {
|
|
53
|
+
const request = await capture(() => agentWithSkills("name", "do-the-work"), { a: [] })
|
|
54
54
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
55
55
|
expect(request.prompt).toBe("do-the-work")
|
|
56
56
|
})
|
|
57
|
-
|
|
58
|
-
it("still accepts an explicit options.skills of its own, untouched", async () => {
|
|
59
|
-
const request = await capture(() =>
|
|
60
|
-
agentWithSkills("name", "do-the-work", { skills: ["own-skill"] }),
|
|
61
|
-
)
|
|
62
|
-
if (request.kind !== "agent") throw new Error("unreachable")
|
|
63
|
-
expect(request.options.skills).toEqual(["own-skill"])
|
|
64
|
-
})
|
|
65
57
|
})
|
|
66
58
|
|
|
67
59
|
describe("the shared open-question instruction", () => {
|
package/src/workflows/text.ts
CHANGED
|
@@ -37,23 +37,12 @@ export const withSkills = (skills: string | undefined, prompt: string): string =
|
|
|
37
37
|
return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* An `agent()` step whose preamble names its resolved skills: `skillsFor`
|
|
42
|
-
* resolves `name` (scoped from here, same as `agent()`'s own resolver) for
|
|
43
|
-
* the preamble prose, passing `options.skills` through as `ownSkills` — the
|
|
44
|
-
* same precedence tier `agent()`'s own wire resolver gives a call's own
|
|
45
|
-
* `skills` option, so a step that declares one (`reviewQuality`'s per-turn
|
|
46
|
-
* lens) gets it in the preamble too, not just on the wire, whenever no
|
|
47
|
-
* bundled or configured entry outranks it. Both reads share one resolver
|
|
48
|
-
* (`Replay.ts`'s `resolveSkills`), so the preamble and the wire can't drift
|
|
49
|
-
* apart.
|
|
50
|
-
*/
|
|
40
|
+
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills. */
|
|
51
41
|
export const agentWithSkills = (
|
|
52
42
|
name: string,
|
|
53
43
|
prompt: string,
|
|
54
44
|
options: AgentOptions = {},
|
|
55
|
-
): Promise<void> =>
|
|
56
|
-
agent(name, withSkills(skillsFor(name, options.skills).join(", "), prompt), options)
|
|
45
|
+
): Promise<void> => agent(name, withSkills(skillsFor(name).join(", "), prompt), options)
|
|
57
46
|
|
|
58
47
|
export const unwindFailure = (commit: string): string =>
|
|
59
48
|
`gtd could not unwind ${commit} out of your working tree.`
|
|
@@ -612,7 +601,7 @@ When you've been through the whole diff, run \`gtd land\`:
|
|
|
612
601
|
- \`question\` — answered inline under the note in
|
|
613
602
|
\`.gtd/REVIEW.md\` (**review.answer-review-questions**); the process
|
|
614
603
|
rests at this gate again, no lap
|
|
615
|
-
- \`nit\` — fixed in one batched turn (**review.fix
|
|
604
|
+
- \`nit\` — fixed in one batched turn (**review.fix.nits.fixing**), then a
|
|
616
605
|
fresh review of the change rests at this gate again, no re-plan
|
|
617
606
|
- \`praise\` — dropped; a round of only praise signs off
|
|
618
607
|
When a round mixes \`edit\` with \`question\` or \`nit\`, questions get
|
package/src/workflows/vars.ts
CHANGED
|
@@ -12,10 +12,8 @@ export const defaults: Readonly<Record<string, string>> = {
|
|
|
12
12
|
specPreJudge: "0.9",
|
|
13
13
|
reviewNoteActionable: "0.7",
|
|
14
14
|
architectureSkipMinP: "0.85",
|
|
15
|
-
//
|
|
16
|
-
// `./
|
|
17
|
-
// setting — pairing with `build.quality.reviewing`'s entry in
|
|
18
|
-
// `./skills.ts`, which replaces the lens on every one of those turns.
|
|
15
|
+
// Decides the turn count: one `build.quality.<lens>` scope per entry, each
|
|
16
|
+
// keyed in `./skills.ts`.
|
|
19
17
|
qualityReviews:
|
|
20
18
|
"correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
21
19
|
}
|