@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.
@@ -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-suite's bundled skills", async () => {
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-spec's bundled skills", async () => {
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's preamble falls back to the lens itself by default — build.quality.reviewing bundles no fixed skills of its own", async () => {
74
- const prompt = agentPrompt(await capture(() => steps.reviewQuality("owasp-security")))
75
- expect(prompt).toContain("missing one: owasp-security")
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("reviewQuality's own wire skills option IS the lens by default — the same list its preamble now names, never one without the other", async () => {
79
- const request = await capture(() => steps.reviewQuality("owasp-security"))
80
- expect(agentSkillsOption(request)).toEqual(["owasp-security"])
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("reviewQuality's preamble reflects a configured build.quality.reviewing entry, every turn, regardless of lens", async () => {
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 loads code-review-and-quality and carries all four trace points", async () => {
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
- "%s sends no skills and carries its brief",
109
- async (lens) => {
110
- const request = await capture(() => steps.reviewQuality(lens))
111
- expect(agentSkillsOption(request)).toEqual([])
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("a configured build.quality.reviewing entry replaces a built-in lens's skills; the brief stays", async () => {
125
- const prompt = agentPrompt(
126
- await capture(() => steps.reviewQuality("conventions"), {
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-quality's bundled skills", async () => {
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(), { 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
- steps.answerReviewQuestions([{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "why?" }]),
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
- steps.fixNits([
174
- { id: "note-1", anchor: "calc ./a.ts#1-1", text: "typo" },
175
- { id: "note-2", anchor: "calc ./a.ts#2-2", text: "semicolon" },
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-nits")
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-risks")
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`")
@@ -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-suite", t.packagesItemFixSuitePrompt(), {
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-spec", t.packagesItemFixSpecPrompt(pkg), {
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
- "quality.reviewing",
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-quality", t.buildFixQualityPrompt(), {
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-nits", t.buildReviewFixNitsPrompt(notes), {
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. Shares the `build.review` conversation like `fixNits`; an empty turn means the risk was judged false. */
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-risks", t.buildReviewFixRisksPrompt(notes), {
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
- * A PER-KEY `skillsFor` override, keyed by the LOCAL name a bundled step
19
- * calls itself with (`captureStep`/`renderText` push no scopes, so a local
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
- /** The bundled map is keyed by FULL name, but a bundled step calls itself by its own LOCAL name and this fixture pushes no scope to prefix it with — so the default lookup matches on the local name alone, or as the last `.`-separated segment of a bundled key. `health.describe`'s two bundled entries (`packages.item.health.describe`, `build.health.describe`) share one list, so the ambiguity this leaves resolves to the same answer either way. */
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, ownSkills?: readonly string[]): readonly string[] => {
52
- const configured = context.skills?.[localName]
53
- if (configured !== undefined) return configured
54
- return ownSkills ?? bundledSkillsFor(localName)
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 skillsFor(name)'s list, joined, in the preamble", async () => {
38
+ it("puts the scope's list, joined, in the preamble", async () => {
39
39
  const request = await capture(() => agentWithSkills("name", "do-the-work"), {
40
- name: ["code-review", "testing"],
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.skills).toBeUndefined()
49
+ expect(request.options).not.toHaveProperty("skills")
50
50
  })
51
51
 
52
- it("leaves the prompt bare when the name resolves to no configured skills", async () => {
53
- const request = await capture(() => agentWithSkills("name", "do-the-work"), { name: [] })
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", () => {
@@ -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-nits**), then a
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
@@ -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
- // `qualityReviews` fans out into one turn per entry (`qualityLenses`, in
16
- // `./review.ts`) rather than naming one step's skill list, so it is a
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
  }