@pmelab/gtd 15.10.0 → 17.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.10.0",
3
+ "version": "17.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
@@ -5,7 +5,7 @@
5
5
  "properties": {
6
6
  "vars": {
7
7
  "type": "object",
8
- "description": "Flat name -> scalar map merged into the workflow's vars. Scalars are coerced to strings.",
8
+ "description": "Flat name -> scalar map merged into the workflow's process settings (defaults), pinned for the whole process at its start. Scalars are coerced to strings.",
9
9
  "additionalProperties": {
10
10
  "type": [
11
11
  "string",
@@ -14,6 +14,38 @@
14
14
  ]
15
15
  }
16
16
  },
17
+ "env": {
18
+ "type": "object",
19
+ "description": "Flat name -> scalar map merged into the workflow's environment settings (envDefaults) — values that change how a step runs on this machine, like the test command or a model hint. Read fresh on every gtd call, never pinned to a process. GTD_<NAME> environment variables override these entries. Scalars are coerced to strings.",
20
+ "additionalProperties": {
21
+ "type": [
22
+ "string",
23
+ "number",
24
+ "boolean"
25
+ ]
26
+ }
27
+ },
28
+ "judge": {
29
+ "type": "object",
30
+ "required": [],
31
+ "properties": {
32
+ "provider": {
33
+ "type": "string",
34
+ "enum": [
35
+ "fixed",
36
+ "jev",
37
+ "llm"
38
+ ],
39
+ "description": "Judge provider `gtd judge run` uses when no --provider flag and no GTD_JUDGE_PROVIDER is set. Absent means auto: jev when TYPESAFE_API_KEY is set, otherwise llm."
40
+ },
41
+ "model": {
42
+ "type": "string",
43
+ "description": "Model for provider llm, used when no --model flag and no GTD_JUDGE_MODEL is set. Pairing it with another provider is an error."
44
+ }
45
+ },
46
+ "additionalProperties": false,
47
+ "description": "Which judge `gtd judge run` uses, when no flag or GTD_JUDGE_* variable says."
48
+ },
17
49
  "modes": {
18
50
  "type": "object",
19
51
  "description": "Steering-file modes a workflow step's mode: may name. Each entry declares at least one of format/validate: shell commands gtd runs via bash with $GTD_FILE set to the steering file's path. format rewrites the file in place; validate exits 0 when valid, non-zero with findings on stdout/stderr otherwise. The halves layer independently, so naming a built-in mode (qa/review) and declaring only format: adds formatting while keeping gtd's own validation. gtd ships no formatter — bring your own (prettier, dprint, a script).",
@@ -63,6 +95,16 @@
63
95
  "additionalProperties": false,
64
96
  "description": "Settings for `gtd ui`: where it listens, and optional TLS."
65
97
  },
98
+ "skills": {
99
+ "type": "object",
100
+ "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.",
101
+ "additionalProperties": {
102
+ "type": "array",
103
+ "items": {
104
+ "type": "string"
105
+ }
106
+ }
107
+ },
66
108
  "$schema": {
67
109
  "type": "string",
68
110
  "description": "Editor-only pointer at this schema. Stripped by gtd before validation."
@@ -136,8 +136,18 @@ export interface FlowContext {
136
136
  readonly threads: (text: string) => readonly ThreadInfo[]
137
137
  readonly codeThreads: () => readonly CodeThreadInfo[]
138
138
  readonly vars: Readonly<Record<string, string>>
139
+ readonly env: Readonly<Record<string, string>>
139
140
  readonly head: () => string
140
141
  readonly start: () => string
142
+ /**
143
+ * The skill list `localName` (scoped from here, same as `agent()`) resolves
144
+ * to — for a prompt preamble. `ownSkills`, when given, is the same
145
+ * precedence tier as a call's own `skills` option: it stands in for the
146
+ * bundled default when there isn't one, but a `.gtdrc` entry still beats
147
+ * it. Shares its resolution with `agent()`'s own wire resolver (both read
148
+ * `Workflow.ts`'s `configuredSkills`/`skills`), so the two can never drift.
149
+ */
150
+ readonly skillsFor: (localName: string, ownSkills?: readonly string[]) => readonly string[]
141
151
  }
142
152
 
143
153
  const CONTEXT_KEY = Symbol.for("@pmelab/gtd/flow-context")
@@ -243,19 +253,35 @@ export const head = (): string => ctx().head()
243
253
  /** The process's diff base: the commit before it began, or the base `gtd --entry` fixed. */
244
254
  export const start = (): string => ctx().start()
245
255
 
246
- /** The merged workflow variables. */
247
- export const vars: Readonly<Record<string, string>> = new Proxy(
248
- {},
249
- {
250
- get: (_target, key) => (typeof key === "string" ? ctx().vars[key] : undefined),
251
- has: (_target, key) => typeof key === "string" && key in ctx().vars,
252
- ownKeys: () => Object.keys(ctx().vars),
253
- getOwnPropertyDescriptor: (_target, key) =>
254
- typeof key === "string" && key in ctx().vars
255
- ? { enumerable: true, configurable: true, value: ctx().vars[key] }
256
- : undefined,
257
- },
258
- )
256
+ /** The skill list `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. See `FlowContext.skillsFor` for `ownSkills`. */
257
+ export const skillsFor = (localName: string, ownSkills?: readonly string[]): readonly string[] =>
258
+ ctx().skillsFor(localName, ownSkills)
259
+
260
+ const settingsProxy = (
261
+ read: () => Readonly<Record<string, string>>,
262
+ ): Readonly<Record<string, string>> =>
263
+ new Proxy(
264
+ {},
265
+ {
266
+ get: (_target, key) => (typeof key === "string" ? read()[key] : undefined),
267
+ has: (_target, key) => typeof key === "string" && key in read(),
268
+ ownKeys: () => Object.keys(read()),
269
+ getOwnPropertyDescriptor: (_target, key) =>
270
+ typeof key === "string" && key in read()
271
+ ? { enumerable: true, configurable: true, value: read()[key] }
272
+ : undefined,
273
+ },
274
+ )
275
+
276
+ /** The process settings: pinned at process start, so flow code may branch on them. */
277
+ export const vars: Readonly<Record<string, string>> = settingsProxy(() => ctx().vars)
278
+
279
+ /**
280
+ * The environment settings: resolved live on every invocation. Flow code must
281
+ * read them only where they cannot change the next step (step options, script
282
+ * bodies) — replay cannot enforce this.
283
+ */
284
+ export const env: Readonly<Record<string, string>> = settingsProxy(() => ctx().env)
259
285
 
260
286
  // ── Text utilities ──────────────────────────────────────────────────────────
261
287
 
@@ -330,6 +356,7 @@ export interface SummaryContext {
330
356
  readonly processCost: number
331
357
  readonly processCostByModel: readonly { readonly model: string; readonly cost: number }[]
332
358
  readonly vars: Readonly<Record<string, string>>
359
+ readonly env: Readonly<Record<string, string>>
333
360
  }
334
361
 
335
362
  /** `gtd summary`'s prompt — a workflow module's optional `summary` export. */
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  answered,
3
3
  check,
4
+ env,
4
5
  human,
5
6
  judge,
6
7
  numeric,
@@ -17,7 +18,7 @@ export const FIX_CAP = 3
17
18
 
18
19
  /** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
19
20
  export const baseline = (name: string, label = "Checking the baseline"): Promise<boolean> =>
20
- check(name, vars.testCommand ?? "", { report: FEEDBACK, label })
21
+ check(name, env.testCommand ?? "", { report: FEEDBACK, label })
21
22
 
22
23
  /** How many escalation rounds a run of red checks has spent — reset once the suite goes green. */
23
24
  export interface EscalationCount {
@@ -95,7 +96,7 @@ export const healthy = async (
95
96
  let fixes = options.fixesSoFar ?? 0
96
97
  let previous: string | undefined
97
98
  for (;;) {
98
- const green = await check("health.check", vars.testCommand ?? "", {
99
+ const green = await check("health.check", env.testCommand ?? "", {
99
100
  report: FEEDBACK,
100
101
  label: "Running checks",
101
102
  // Swept only on green: an unresolved analysis survives every retry.
@@ -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")