@pmelab/gtd 15.9.0 → 16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "15.9.0",
3
+ "version": "16.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
package/schema.json CHANGED
@@ -63,6 +63,16 @@
63
63
  "additionalProperties": false,
64
64
  "description": "Settings for `gtd ui`: where it listens, and optional TLS."
65
65
  },
66
+ "skills": {
67
+ "type": "object",
68
+ "description": "Flat step full-name -> skill-name array map. Each entry REPLACES the named step's bundled skill list wholesale (never merges into it); [] means no skills at all for that step, and no preamble. A key naming a step the workflow in play does not declare is a load error listing the known names — the schema itself cannot validate a key, only a value's shape.",
69
+ "additionalProperties": {
70
+ "type": "array",
71
+ "items": {
72
+ "type": "string"
73
+ }
74
+ }
75
+ },
66
76
  "$schema": {
67
77
  "type": "string",
68
78
  "description": "Editor-only pointer at this schema. Stripped by gtd before validation."
@@ -138,6 +138,15 @@ export interface FlowContext {
138
138
  readonly vars: Readonly<Record<string, string>>
139
139
  readonly head: () => string
140
140
  readonly start: () => string
141
+ /**
142
+ * The skill list `localName` (scoped from here, same as `agent()`) resolves
143
+ * to — for a prompt preamble. `ownSkills`, when given, is the same
144
+ * precedence tier as a call's own `skills` option: it stands in for the
145
+ * bundled default when there isn't one, but a `.gtdrc` entry still beats
146
+ * it. Shares its resolution with `agent()`'s own wire resolver (both read
147
+ * `Workflow.ts`'s `configuredSkills`/`skills`), so the two can never drift.
148
+ */
149
+ readonly skillsFor: (localName: string, ownSkills?: readonly string[]) => readonly string[]
141
150
  }
142
151
 
143
152
  const CONTEXT_KEY = Symbol.for("@pmelab/gtd/flow-context")
@@ -243,6 +252,10 @@ export const head = (): string => ctx().head()
243
252
  /** The process's diff base: the commit before it began, or the base `gtd --entry` fixed. */
244
253
  export const start = (): string => ctx().start()
245
254
 
255
+ /** The skill list `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. See `FlowContext.skillsFor` for `ownSkills`. */
256
+ export const skillsFor = (localName: string, ownSkills?: readonly string[]): readonly string[] =>
257
+ ctx().skillsFor(localName, ownSkills)
258
+
246
259
  /** The merged workflow variables. */
247
260
  export const vars: Readonly<Record<string, string>> = new Proxy(
248
261
  {},
@@ -35,8 +35,12 @@ import {
35
35
  } from "./steps.js"
36
36
  import * as t from "./text.js"
37
37
 
38
- /** The skills the quality lap reviews with, one turn each: the `qualityReviews` var. */
39
- export const qualityLenses = (): readonly string[] => t.splitSkills(vars.qualityReviews)
38
+ /** The lenses the quality lap reviews with, one turn each: the `qualityReviews` var, split on `,` and trimmed. Unlike a `skills:` entry, this fans out into one whole turn per entry rather than naming one step's skill list — see `build.quality.reviewing` in `./skills.ts` for the (separate) skills a lens turn itself loads. */
39
+ export const qualityLenses = (): readonly string[] =>
40
+ (vars.qualityReviews ?? "")
41
+ .split(",")
42
+ .map((lens) => lens.trim())
43
+ .filter((lens) => lens.length > 0)
40
44
 
41
45
  /**
42
46
  * One review turn per lens over the whole change, each appending what it
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { skills } from "./skills.js"
3
+
4
+ // Pins every bundled step's own declared list, keyed by its full name. This
5
+ // file alone only catches a LOCAL-name rename (the list literally changes);
6
+ // it cannot catch a SCOPE rename (e.g. `scope("architecture")` renamed to
7
+ // `scope("design.architecture")`), because it never runs the real flow — it
8
+ // compares the map against itself. Every one of the sixteen keys has its
9
+ // own e2e grounding closing that gap instead — each runs the actual scoped
10
+ // name through replay, so a scope rename desyncs a preamble assertion there,
11
+ // not just silently drops a step's skills (see AGENTS.md's "workflow is
12
+ // code" rule: a step's full name is public API):
13
+ // - design.triage, architecture.author, architecture.decompose,
14
+ // packages.item.building — default-workflow.feature's main scenario
15
+ // - packages.item.spec.review, packages.item.fix-spec — the same
16
+ // scenario, continued
17
+ // - packages.item.fix-suite, packages.item.health.describe — the
18
+ // "escalation loop ... under packages.item" scenario
19
+ // - build.fix — state-skills.feature
20
+ // - build.health.describe — the "repeated check failures escalate"
21
+ // scenario
22
+ // - build.quality.reviewing — per-step-skills.feature
23
+ // - build.fix-quality — quality-review-lap.feature
24
+ // - build.review.reviewing, build.review.collecting —
25
+ // default-workflow.feature's review-loop scenarios
26
+ // - build.review.answer-review-questions, build.review.fix-nits — NOT yet
27
+ // e2e-grounded; only steps.test.ts's preamble checks, which use local names
28
+ describe("the bundled workflow's skills map", () => {
29
+ it("declares exactly the sixteen bundled agent steps, by full name", () => {
30
+ expect(Object.keys(skills).sort()).toEqual(
31
+ [
32
+ "design.triage",
33
+ "architecture.author",
34
+ "architecture.decompose",
35
+ "packages.item.building",
36
+ "packages.item.fix-suite",
37
+ "packages.item.fix-spec",
38
+ "packages.item.spec.review",
39
+ "packages.item.health.describe",
40
+ "build.fix",
41
+ "build.health.describe",
42
+ "build.quality.reviewing",
43
+ "build.fix-quality",
44
+ "build.review.reviewing",
45
+ "build.review.answer-review-questions",
46
+ "build.review.fix-nits",
47
+ "build.review.collecting",
48
+ ].sort(),
49
+ )
50
+ })
51
+
52
+ it("packages.item.health.describe and build.health.describe are both present, with the same list — the shared escalation loop resolves under both scopes", () => {
53
+ expect(skills["packages.item.health.describe"]).toEqual(skills["build.health.describe"])
54
+ expect(skills["packages.item.health.describe"]).toEqual(["debugging-and-error-recovery"])
55
+ })
56
+
57
+ it("pins each step's own declared list", () => {
58
+ expect(skills["design.triage"]).toEqual([
59
+ "spec-driven-development",
60
+ "planning-and-task-breakdown",
61
+ ])
62
+ expect(skills["architecture.author"]).toEqual([
63
+ "api-and-interface-design",
64
+ "documentation-and-adrs",
65
+ "ponytail",
66
+ ])
67
+ expect(skills["architecture.decompose"]).toEqual([
68
+ "incremental-implementation",
69
+ "planning-and-task-breakdown",
70
+ ])
71
+ expect(skills["packages.item.building"]).toEqual([
72
+ "test-driven-development",
73
+ "incremental-implementation",
74
+ ])
75
+ expect(skills["packages.item.fix-suite"]).toEqual(["debugging-and-error-recovery"])
76
+ expect(skills["build.fix"]).toEqual(["debugging-and-error-recovery"])
77
+ expect(skills["packages.item.fix-spec"]).toEqual([
78
+ "incremental-implementation",
79
+ "code-simplification",
80
+ ])
81
+ expect(skills["build.fix-quality"]).toEqual([
82
+ "incremental-implementation",
83
+ "code-simplification",
84
+ ])
85
+ expect(skills["packages.item.spec.review"]).toEqual([
86
+ "code-review-and-quality",
87
+ "spec-driven-development",
88
+ ])
89
+ expect(skills["build.review.reviewing"]).toEqual(["code-review-and-quality"])
90
+ expect(skills["build.review.collecting"]).toEqual(["code-review-and-quality"])
91
+ expect(skills["build.quality.reviewing"]).toEqual([])
92
+ })
93
+ })
@@ -0,0 +1,37 @@
1
+ // The bundled workflow's default skills, one list per agent step, keyed by
2
+ // the step's FULL name (its scope prefixes included) — the same name a
3
+ // `.gtdrc` `skills:` entry addresses. A step's name is not derivable from
4
+ // flow code alone (loops, branches, shared calls), so this map is the only
5
+ // place every addressable name is declared up front; `steps.test.ts` pins
6
+ // each step's declared list against a renamed map key.
7
+ //
8
+ // `packages.item.health.describe` and `build.health.describe` are not a
9
+ // duplicate: the escalation loop (`healthy()`, in `./health.ts`) is shared
10
+ // code reached from both `scope("packages") -> scope("item")` and
11
+ // `scope("build")`, so the one `health.describe` step name resolves to two
12
+ // different full names depending on which caller is looping.
13
+ export const skills: Readonly<Record<string, readonly string[]>> = {
14
+ "design.triage": ["spec-driven-development", "planning-and-task-breakdown"],
15
+ "architecture.author": ["api-and-interface-design", "documentation-and-adrs", "ponytail"],
16
+ "architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
17
+ "packages.item.building": ["test-driven-development", "incremental-implementation"],
18
+ "packages.item.fix-suite": ["debugging-and-error-recovery"],
19
+ "packages.item.fix-spec": ["incremental-implementation", "code-simplification"],
20
+ "packages.item.spec.review": ["code-review-and-quality", "spec-driven-development"],
21
+ "packages.item.health.describe": ["debugging-and-error-recovery"],
22
+ "build.fix": ["debugging-and-error-recovery"],
23
+ "build.health.describe": ["debugging-and-error-recovery"],
24
+ // No bundled default: unlike every other entry, the lens named in the
25
+ // prompt BODY already varies per turn (see `reviewQuality` in
26
+ // `./steps.ts`), which is what the preamble falls back to naming instead —
27
+ // a fixed bundled list here would repeat one entry on every turn. A
28
+ // `.gtdrc` `skills:` entry still overrides that per-turn fallback, on the
29
+ // wire and in the preamble alike (`resolveSkills` in `../replay/Replay.ts`
30
+ // is what both share).
31
+ "build.quality.reviewing": [],
32
+ "build.fix-quality": ["incremental-implementation", "code-simplification"],
33
+ "build.review.reviewing": ["code-review-and-quality"],
34
+ "build.review.answer-review-questions": ["code-review-and-quality"],
35
+ "build.review.fix-nits": ["incremental-implementation", "code-simplification"],
36
+ "build.review.collecting": ["code-review-and-quality"],
37
+ }
@@ -5,107 +5,114 @@ import * as steps from "./steps.js"
5
5
 
6
6
  const capture = (
7
7
  fn: () => Promise<void>,
8
- vars: Readonly<Record<string, string>> = {},
9
- ): Promise<StepRequest> => captureStep(fn, { vars })
8
+ skills?: Readonly<Record<string, readonly string[]>>,
9
+ ): Promise<StepRequest> => captureStep(fn, skills !== undefined ? { skills } : {})
10
10
 
11
- const agentSkills = (request: StepRequest): readonly string[] | undefined => {
11
+ const agentPrompt = (request: StepRequest): string => {
12
+ if (request.kind !== "agent") throw new Error(`expected an agent step, got ${request.kind}`)
13
+ return request.prompt
14
+ }
15
+
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 => {
12
23
  if (request.kind !== "agent") throw new Error(`expected an agent step, got ${request.kind}`)
13
24
  return request.options.skills
14
25
  }
15
26
 
16
- describe("the bundled workflow's steps declare skills", () => {
17
- it("triage declares triageSkills, split", async () => {
18
- expect(agentSkills(await capture(() => steps.triage("base")))).toEqual([
19
- "spec-driven-development",
20
- "planning-and-task-breakdown",
21
- ])
27
+ describe("the bundled workflow's steps declare skills — a bundled step's rendered prompt carries its bundled skills.ts preamble", () => {
28
+ it("triage carries design.triage's bundled skills", async () => {
29
+ const prompt = agentPrompt(await capture(() => steps.triage("base")))
30
+ expect(prompt).toContain("spec-driven-development, planning-and-task-breakdown")
31
+ })
32
+
33
+ it("author carries architecture.author's bundled skills", async () => {
34
+ const prompt = agentPrompt(await capture(() => steps.author()))
35
+ expect(prompt).toContain("api-and-interface-design, documentation-and-adrs, ponytail")
36
+ })
37
+
38
+ it("decompose carries architecture.decompose's bundled skills", async () => {
39
+ const prompt = agentPrompt(await capture(() => steps.decompose()))
40
+ expect(prompt).toContain("incremental-implementation, planning-and-task-breakdown")
22
41
  })
23
42
 
24
- it("author declares architectureSkills, split", async () => {
25
- expect(agentSkills(await capture(() => steps.author()))).toEqual([
26
- "api-and-interface-design",
27
- "documentation-and-adrs",
28
- "ponytail",
29
- ])
43
+ it("build carries packages.item.building's bundled skills", async () => {
44
+ const prompt = agentPrompt(await capture(() => steps.build("pkg")))
45
+ expect(prompt).toContain("test-driven-development, incremental-implementation")
30
46
  })
31
47
 
32
- it("decompose declares decomposeSkills, split", async () => {
33
- expect(agentSkills(await capture(() => steps.decompose()))).toEqual([
34
- "incremental-implementation",
35
- "planning-and-task-breakdown",
36
- ])
48
+ it("fixSuite carries packages.item.fix-suite's bundled skills", async () => {
49
+ const prompt = agentPrompt(await capture(() => steps.fixSuite()))
50
+ expect(prompt).toContain("debugging-and-error-recovery")
37
51
  })
38
52
 
39
- it("build declares buildSkills, split", async () => {
40
- expect(agentSkills(await capture(() => steps.build("pkg")))).toEqual([
41
- "test-driven-development",
42
- "incremental-implementation",
43
- ])
53
+ it("fixSpec carries packages.item.fix-spec's bundled skills", async () => {
54
+ const prompt = agentPrompt(await capture(() => steps.fixSpec("pkg")))
55
+ expect(prompt).toContain("incremental-implementation, code-simplification")
44
56
  })
45
57
 
46
- it("fixSuite declares fixSkills, split", async () => {
47
- expect(agentSkills(await capture(() => steps.fixSuite()))).toEqual([
48
- "debugging-and-error-recovery",
49
- ])
58
+ it("reviewPackage carries packages.item.spec.review's bundled skills", async () => {
59
+ const prompt = agentPrompt(await capture(() => steps.reviewPackage("pkg")))
60
+ expect(prompt).toContain("code-review-and-quality, spec-driven-development")
50
61
  })
51
62
 
52
- it("fixSpec declares reviewFixSkills, split", async () => {
53
- expect(agentSkills(await capture(() => steps.fixSpec("pkg")))).toEqual([
54
- "incremental-implementation",
55
- "code-simplification",
56
- ])
63
+ it("fix carries build.fix's bundled skills", async () => {
64
+ const prompt = agentPrompt(await capture(() => steps.fix()))
65
+ expect(prompt).toContain("debugging-and-error-recovery")
57
66
  })
58
67
 
59
- it("reviewPackage declares specReviewSkills, split", async () => {
60
- expect(agentSkills(await capture(() => steps.reviewPackage("pkg")))).toEqual([
61
- "code-review-and-quality",
62
- "spec-driven-development",
63
- ])
68
+ it("describeEscalation carries health.describe's bundled skills", async () => {
69
+ const prompt = agentPrompt(await capture(() => steps.describeEscalation()))
70
+ expect(prompt).toContain("debugging-and-error-recovery")
64
71
  })
65
72
 
66
- it("fix declares fixSkills, split", async () => {
67
- expect(agentSkills(await capture(() => steps.fix()))).toEqual(["debugging-and-error-recovery"])
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")
68
76
  })
69
77
 
70
- it("describeEscalation declares escalateSkills, split", async () => {
71
- expect(agentSkills(await capture(() => steps.describeEscalation()))).toEqual([
72
- "debugging-and-error-recovery",
73
- ])
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"])
74
81
  })
75
82
 
76
- it("reviewQuality declares exactly the one lens handed to it, not split further", async () => {
77
- expect(agentSkills(await capture(() => steps.reviewQuality("owasp-security")))).toEqual([
78
- "owasp-security",
79
- ])
83
+ it("reviewQuality's preamble reflects a configured build.quality.reviewing entry, every turn, regardless of lens", async () => {
84
+ const prompt = agentPrompt(
85
+ await capture(() => steps.reviewQuality("owasp-security"), {
86
+ "quality.reviewing": ["my-org-checklist"],
87
+ }),
88
+ )
89
+ expect(prompt).toContain("my-org-checklist")
90
+ // The body still names the lens — what makes N otherwise identical prompts distinguishable.
91
+ expect(prompt).toContain("owasp-security")
80
92
  })
81
93
 
82
- it("fixQuality declares reviewFixSkills, split", async () => {
83
- expect(agentSkills(await capture(() => steps.fixQuality()))).toEqual([
84
- "incremental-implementation",
85
- "code-simplification",
86
- ])
94
+ it("fixQuality carries build.fix-quality's bundled skills", async () => {
95
+ const prompt = agentPrompt(await capture(() => steps.fixQuality()))
96
+ expect(prompt).toContain("incremental-implementation, code-simplification")
87
97
  })
88
98
 
89
- it("reviewing declares reviewSkills, split", async () => {
90
- expect(agentSkills(await capture(() => steps.reviewing("base")))).toEqual([
91
- "code-review-and-quality",
92
- ])
99
+ it("reviewing carries build.review.reviewing's bundled skills", async () => {
100
+ const prompt = agentPrompt(await capture(() => steps.reviewing("base")))
101
+ expect(prompt).toContain("code-review-and-quality")
93
102
  })
94
103
 
95
- it("collecting declares reviewSkills, split", async () => {
96
- expect(agentSkills(await capture(() => steps.collecting("capture")))).toEqual([
97
- "code-review-and-quality",
98
- ])
104
+ it("collecting carries build.review.collecting's bundled skills", async () => {
105
+ const prompt = agentPrompt(await capture(() => steps.collecting("capture")))
106
+ expect(prompt).toContain("code-review-and-quality")
99
107
  })
100
108
 
101
- it("a blanked *Skills var leaves that step's skills empty, and its rendered prompt bare", async () => {
102
- const request = await capture(() => steps.fix(), { fixSkills: "" })
109
+ it("an empty configured list leaves that step's skills empty, and its rendered prompt bare", async () => {
110
+ const request = await capture(() => steps.fix(), { fix: [] })
103
111
  if (request.kind !== "agent") throw new Error("unreachable")
104
- expect(request.options.skills).toEqual([])
105
112
  expect(request.prompt).not.toContain("Load whatever's listed here")
106
113
  })
107
114
 
108
- it("answerReviewQuestions answers inline in .gtd/REVIEW.md as the reviewer, with reviewSkills", async () => {
115
+ it("answerReviewQuestions answers inline in .gtd/REVIEW.md as the reviewer, with its bundled skills", async () => {
109
116
  const request = await capture(() =>
110
117
  steps.answerReviewQuestions([{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "why?" }]),
111
118
  )
@@ -114,8 +121,8 @@ describe("the bundled workflow's steps declare skills", () => {
114
121
  expect(request.options).toMatchObject({
115
122
  file: ".gtd/REVIEW.md",
116
123
  mode: "review",
117
- skills: ["code-review-and-quality"],
118
124
  })
125
+ expect(request.prompt).toContain("code-review-and-quality")
119
126
  expect(request.prompt).toContain("note-1 — calc ./a.ts#1-1")
120
127
  expect(request.prompt).toContain("`A: ` line")
121
128
  expect(request.prompt).toContain("gtd check review .gtd/REVIEW.md")
@@ -130,7 +137,7 @@ describe("the bundled workflow's steps declare skills", () => {
130
137
  )
131
138
  if (request.kind !== "agent") throw new Error("unreachable")
132
139
  expect(request.name).toBe("review.fix-nits")
133
- expect(request.options.skills).toEqual(["incremental-implementation", "code-simplification"])
140
+ expect(request.prompt).toContain("incremental-implementation, code-simplification")
134
141
  expect(request.prompt).toContain("typo")
135
142
  expect(request.prompt).toContain("semicolon")
136
143
  expect(request.prompt).toContain("Leave `.gtd/REVIEW.md` untouched")
@@ -21,7 +21,7 @@ const coder = (): string => vars.coderModel ?? ""
21
21
 
22
22
  /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
23
23
  export const triage = (base: string): Promise<void> =>
24
- t.agentWithSkills("triage", vars.triageSkills, t.designTriagePrompt(base), {
24
+ t.agentWithSkills("triage", t.designTriagePrompt(base), {
25
25
  label: "Triaging the change",
26
26
  file: REQUIREMENTS,
27
27
  mode: "qa",
@@ -30,7 +30,7 @@ export const triage = (base: string): Promise<void> =>
30
30
  })
31
31
 
32
32
  export const author = (): Promise<void> =>
33
- t.agentWithSkills("author", vars.architectureSkills, t.architectureAuthorPrompt(), {
33
+ t.agentWithSkills("author", t.architectureAuthorPrompt(), {
34
34
  label: "Refining the technical plan",
35
35
  file: ARCHITECTURE,
36
36
  mode: "qa",
@@ -39,7 +39,7 @@ export const author = (): Promise<void> =>
39
39
  })
40
40
 
41
41
  export const decompose = (): Promise<void> =>
42
- t.agentWithSkills("decompose", vars.decomposeSkills, t.architectureDecomposePrompt(), {
42
+ t.agentWithSkills("decompose", t.architectureDecomposePrompt(), {
43
43
  label: "Decomposing into packages",
44
44
  model: planner(),
45
45
  system: t.architectSystem(),
@@ -66,14 +66,14 @@ export const answerTechnicalQuestions = (): Promise<void> =>
66
66
  // ── Packages ────────────────────────────────────────────────────────────────
67
67
 
68
68
  export const build = (pkg: string): Promise<void> =>
69
- t.agentWithSkills("building", vars.buildSkills, t.packagesItemBuildingPrompt(pkg), {
69
+ t.agentWithSkills("building", t.packagesItemBuildingPrompt(pkg), {
70
70
  label: "Building",
71
71
  model: coder(),
72
72
  system: t.builderSystem(),
73
73
  })
74
74
 
75
75
  export const fixSuite = (): Promise<void> =>
76
- t.agentWithSkills("fix-suite", vars.fixSkills, t.packagesItemFixSuitePrompt(), {
76
+ t.agentWithSkills("fix-suite", 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", vars.reviewFixSkills, t.packagesItemFixSpecPrompt(pkg), {
84
+ t.agentWithSkills("fix-spec", t.packagesItemFixSpecPrompt(pkg), {
85
85
  label: "Fixing review feedback",
86
86
  file: SPEC_FEEDBACK,
87
87
  model: coder(),
@@ -90,22 +90,17 @@ export const fixSpec = (pkg: string): Promise<void> =>
90
90
 
91
91
  /** Review `pkg` against its spec, focused on the `failing` sections the pre-judge could not clear. */
92
92
  export const reviewPackage = (pkg: string, failing: readonly string[] = []): Promise<void> =>
93
- t.agentWithSkills(
94
- "spec.review",
95
- vars.specReviewSkills,
96
- t.packagesItemSpecReviewPrompt(pkg, failing),
97
- {
98
- label: "Reviewing the package",
99
- model: planner(),
100
- system: t.specReviewerSystem(),
101
- allowEmpty: true,
102
- },
103
- )
93
+ t.agentWithSkills("spec.review", t.packagesItemSpecReviewPrompt(pkg, failing), {
94
+ label: "Reviewing the package",
95
+ model: planner(),
96
+ system: t.specReviewerSystem(),
97
+ allowEmpty: true,
98
+ })
104
99
 
105
100
  // ── Keeping the suite green ─────────────────────────────────────────────────
106
101
 
107
102
  export const fix = (): Promise<void> =>
108
- t.agentWithSkills("fix", vars.fixSkills, t.buildFixPrompt(), {
103
+ t.agentWithSkills("fix", t.buildFixPrompt(), {
109
104
  label: "Fixing the check",
110
105
  file: FEEDBACK,
111
106
  model: coder(),
@@ -113,7 +108,7 @@ export const fix = (): Promise<void> =>
113
108
  })
114
109
 
115
110
  export const describeEscalation = (): Promise<void> =>
116
- t.agentWithSkills("health.describe", vars.escalateSkills, t.healthDescribePrompt(), {
111
+ t.agentWithSkills("health.describe", t.healthDescribePrompt(), {
117
112
  label: "Describing the escalation",
118
113
  file: FEEDBACK,
119
114
  model: coder(),
@@ -139,18 +134,24 @@ export const escalationExhausted = (): Promise<void> =>
139
134
 
140
135
  // ── Quality and review ──────────────────────────────────────────────────────
141
136
 
142
- /** One quality review, through the skill `lens`. */
137
+ /**
138
+ * One quality review, through the skill `lens`. `lens` rides as this turn's
139
+ * own `skills` option — a `.gtdrc` `build.quality.reviewing` entry still
140
+ * overrides it (config beats a flow-supplied list same as any other step),
141
+ * but absent one the lens itself is what the turn loads by default.
142
+ */
143
143
  export const reviewQuality = (lens: string): Promise<void> =>
144
- t.agentWithSkills("quality.reviewing", lens, t.buildQualityReviewingPrompt(lens), {
144
+ t.agentWithSkills("quality.reviewing", t.buildQualityReviewingPrompt(lens), {
145
145
  label: "Reviewing (one quality lens)",
146
146
  file: QUALITY,
147
147
  model: planner(),
148
148
  system: t.reviewerSystem(),
149
149
  allowEmpty: true,
150
+ skills: [lens],
150
151
  })
151
152
 
152
153
  export const fixQuality = (): Promise<void> =>
153
- t.agentWithSkills("fix-quality", vars.reviewFixSkills, t.buildFixQualityPrompt(), {
154
+ t.agentWithSkills("fix-quality", t.buildFixQualityPrompt(), {
154
155
  label: "Fixing quality findings",
155
156
  file: QUALITY,
156
157
  model: coder(),
@@ -160,41 +161,31 @@ export const fixQuality = (): Promise<void> =>
160
161
 
161
162
  /** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
162
163
  export const reviewing = (base: string, carry?: string): Promise<void> =>
163
- t.agentWithSkills(
164
- "review.reviewing",
165
- vars.reviewSkills,
166
- t.buildReviewReviewingPrompt(base, carry),
167
- {
168
- label: "Reviewing",
169
- file: REVIEW,
170
- mode: "review",
171
- model: planner(),
172
- system: t.reviewerSystem(),
173
- base,
174
- },
175
- )
164
+ t.agentWithSkills("review.reviewing", t.buildReviewReviewingPrompt(base, carry), {
165
+ label: "Reviewing",
166
+ file: REVIEW,
167
+ mode: "review",
168
+ model: planner(),
169
+ system: t.reviewerSystem(),
170
+ base,
171
+ })
176
172
 
177
173
  /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
178
174
  export const answerReviewQuestions = (notes: readonly t.NoteInput[]): Promise<void> =>
179
- t.agentWithSkills(
180
- "review.answer-review-questions",
181
- vars.reviewSkills,
182
- t.buildReviewAnswerQuestionsPrompt(notes),
183
- {
184
- label: "Answering your questions",
185
- file: REVIEW,
186
- mode: "review",
187
- model: planner(),
188
- system: t.reviewerSystem(),
189
- },
190
- )
175
+ t.agentWithSkills("review.answer-review-questions", t.buildReviewAnswerQuestionsPrompt(notes), {
176
+ label: "Answering your questions",
177
+ file: REVIEW,
178
+ mode: "review",
179
+ model: planner(),
180
+ system: t.reviewerSystem(),
181
+ })
191
182
 
192
183
  /**
193
184
  * Fix every `nit` note in one turn. Its name puts it in the `build.review`
194
185
  * conversation, which has one identity — so it runs as the reviewer, not the coder.
195
186
  */
196
187
  export const fixNits = (notes: readonly t.NoteInput[]): Promise<void> =>
197
- t.agentWithSkills("review.fix-nits", vars.reviewFixSkills, t.buildReviewFixNitsPrompt(notes), {
188
+ t.agentWithSkills("review.fix-nits", t.buildReviewFixNitsPrompt(notes), {
198
189
  label: "Fixing your nits",
199
190
  file: REVIEW,
200
191
  model: planner(),
@@ -219,16 +210,11 @@ export const reviewMissing = (round: string): Promise<void> =>
219
210
 
220
211
  /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
221
212
  export const collecting = (capture: string): Promise<void> =>
222
- t.agentWithSkills(
223
- "review.collecting",
224
- vars.reviewSkills,
225
- t.buildReviewCollectingPrompt(capture),
226
- {
227
- label: "Collecting your feedback",
228
- file: REQUIREMENTS,
229
- mode: "qa",
230
- model: planner(),
231
- system: t.reviewerSystem(),
232
- allowEmpty: true,
233
- },
234
- )
213
+ t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture), {
214
+ label: "Collecting your feedback",
215
+ file: REQUIREMENTS,
216
+ mode: "qa",
217
+ model: planner(),
218
+ system: t.reviewerSystem(),
219
+ allowEmpty: true,
220
+ })