@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.
@@ -1,4 +1,4 @@
1
- import { human, vars } from "../flows/index.js"
1
+ import { env, human } from "../flows/index.js"
2
2
  import * as t from "./text.js"
3
3
 
4
4
  // The bundled workflow's single steps. A step's name is relative to the
@@ -14,14 +14,14 @@ export const REVIEW = ".gtd/REVIEW.md"
14
14
  export const QUALITY = ".gtd/QUALITY.md"
15
15
  export const SPEC_FEEDBACK = ".gtd/SPEC_FEEDBACK.md"
16
16
 
17
- const planner = (): string => vars.plannerModel ?? ""
18
- const coder = (): string => vars.coderModel ?? ""
17
+ const planner = (): string => env.plannerModel ?? ""
18
+ const coder = (): string => env.coderModel ?? ""
19
19
 
20
20
  // ── Planning ────────────────────────────────────────────────────────────────
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
+ })
@@ -4,20 +4,56 @@ import {
4
4
  type FlowContext,
5
5
  type StepRequest,
6
6
  } from "../flows/index.js"
7
- import { defaults } from "./vars.js"
7
+ import { skills as bundledSkills } from "./skills.js"
8
+ import { defaults, envDefaults } from "./vars.js"
8
9
 
9
10
  export interface TextContext {
10
11
  readonly vars?: Readonly<Record<string, string>>
12
+ readonly env?: Readonly<Record<string, string>>
11
13
  readonly head?: string
12
14
  readonly start?: string
13
15
  readonly codeThreads?: readonly CodeThreadInfo[]
14
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
+ */
26
+ readonly skills?: Readonly<Record<string, readonly string[]>>
15
27
  }
16
28
 
17
29
  const unavailable = (): never => {
18
30
  throw new Error("not available while rendering a text outside a replay")
19
31
  }
20
32
 
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
+ */
49
+ const skillsForOf =
50
+ (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)
55
+ }
56
+
21
57
  /** The fixed replay context texts render against; steps, refusals and scopes are unavailable unless overridden. */
22
58
  export const fixtureContext = (
23
59
  context: TextContext = {},
@@ -38,8 +74,10 @@ export const fixtureContext = (
38
74
  threads: () => [],
39
75
  codeThreads: () => context.codeThreads ?? [],
40
76
  vars: { ...defaults, ...context.vars },
77
+ env: { ...envDefaults, ...context.env },
41
78
  head: () => context.head ?? "",
42
79
  start: () => context.start ?? "",
80
+ skillsFor: skillsForOf(context),
43
81
  ...overrides,
44
82
  })
45
83
 
@@ -8,7 +8,6 @@ import {
8
8
  buildReviewCollectingPrompt,
9
9
  designGateAnswerMessage,
10
10
  designTriagePrompt,
11
- splitSkills,
12
11
  summaryPrompt,
13
12
  withSkills,
14
13
  } from "./text.js"
@@ -30,42 +29,36 @@ describe("withSkills", () => {
30
29
  })
31
30
  })
32
31
 
33
- describe("splitSkills", () => {
34
- it("splits on comma, trims each name, drops empty entries", () => {
35
- expect(splitSkills("a, b")).toEqual(["a", "b"])
36
- expect(splitSkills(" a ,, b ")).toEqual(["a", "b"])
37
- })
38
-
39
- it("returns [] for undefined, empty, or blank input", () => {
40
- expect(splitSkills(undefined)).toEqual([])
41
- expect(splitSkills("")).toEqual([])
42
- expect(splitSkills(" ")).toEqual([])
43
- })
44
- })
45
-
46
32
  describe("agentWithSkills", () => {
47
- const capture = (fn: () => Promise<void>) => captureStep(fn)
33
+ const capture = (fn: () => Promise<void>, skills?: Readonly<Record<string, readonly string[]>>) =>
34
+ captureStep(fn, skills !== undefined ? { skills } : {})
48
35
 
49
- it("produces a byte-identical preamble to withSkills for a comma-free name list", async () => {
50
- const skills = "code-review, testing"
51
- const viaHelper = await capture(() => agentWithSkills("name", skills, "do-the-work"))
52
- if (viaHelper.kind !== "agent") throw new Error("unreachable")
53
- expect(viaHelper.prompt).toBe(withSkills(skills, "do-the-work"))
36
+ it("puts skillsFor(name)'s list, joined, in the preamble", async () => {
37
+ const request = await capture(() => agentWithSkills("name", "do-the-work"), {
38
+ name: ["code-review", "testing"],
39
+ })
40
+ if (request.kind !== "agent") throw new Error("unreachable")
41
+ expect(request.prompt).toBe(withSkills("code-review, testing", "do-the-work"))
54
42
  })
55
43
 
56
- it("passes the skills list, split, as the option, alongside the preamble", async () => {
57
- const request = await capture(() =>
58
- agentWithSkills("name", "code-review, testing", "do-the-work"),
59
- )
44
+ it("passes no skills option itself — the replay resolver fills the wire field", async () => {
45
+ const request = await capture(() => agentWithSkills("name", "do-the-work"))
60
46
  if (request.kind !== "agent") throw new Error("unreachable")
61
- expect(request.options.skills).toEqual(["code-review", "testing"])
47
+ expect(request.options.skills).toBeUndefined()
62
48
  })
63
49
 
64
- it("leaves the prompt bare and the option an empty array when skills is undefined", async () => {
65
- const request = await capture(() => agentWithSkills("name", undefined, "do-the-work"))
50
+ it("leaves the prompt bare when the name resolves to no configured skills", async () => {
51
+ const request = await capture(() => agentWithSkills("name", "do-the-work"), { name: [] })
66
52
  if (request.kind !== "agent") throw new Error("unreachable")
67
53
  expect(request.prompt).toBe("do-the-work")
68
- expect(request.options.skills).toEqual([])
54
+ })
55
+
56
+ it("still accepts an explicit options.skills of its own, untouched", async () => {
57
+ const request = await capture(() =>
58
+ agentWithSkills("name", "do-the-work", { skills: ["own-skill"] }),
59
+ )
60
+ if (request.kind !== "agent") throw new Error("unreachable")
61
+ expect(request.options.skills).toEqual(["own-skill"])
69
62
  })
70
63
  })
71
64
 
@@ -136,6 +129,7 @@ describe("summaryPrompt", () => {
136
129
  { model: "base", cost: 7 },
137
130
  ],
138
131
  vars: {},
132
+ env: {},
139
133
  })
140
134
  expect(prompt).toContain("Token cost: 12\n- smart: 5\n- base: 7\n\nPrint the closing message")
141
135
  })
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  agent,
3
3
  codeThreads,
4
+ env,
4
5
  head,
6
+ skillsFor,
5
7
  start,
6
- vars,
7
8
  type AgentOptions,
8
9
  type SummaryContext,
9
10
  } from "../flows/index.js"
@@ -36,31 +37,23 @@ export const withSkills = (skills: string | undefined, prompt: string): string =
36
37
  return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`
37
38
  }
38
39
 
39
- /** `skills:` split on `,`, each name trimmed, empty entries dropped — `[]` for `undefined`, `""`, or blank. */
40
- export const splitSkills = (raw: string | undefined): readonly string[] =>
41
- (raw ?? "")
42
- .split(",")
43
- .map((name) => name.trim())
44
- .filter((name) => name.length > 0)
45
-
46
40
  /**
47
- * An `agent()` step declaring `skills`: the raw, comma-separated var feeds
48
- * both the `skills` option (the wire's `skills` key, split) and, re-joined
49
- * with `", "`, `withSkills`'s preamble — one declaration, so the two can
50
- * never drift apart.
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.
51
50
  */
52
51
  export const agentWithSkills = (
53
52
  name: string,
54
- skills: string | undefined,
55
53
  prompt: string,
56
54
  options: AgentOptions = {},
57
- ): Promise<void> => {
58
- const list = splitSkills(skills)
59
- return agent(name, withSkills(list.join(", "), prompt), {
60
- ...options,
61
- skills: list,
62
- })
63
- }
55
+ ): Promise<void> =>
56
+ agent(name, withSkills(skillsFor(name, options.skills).join(", "), prompt), options)
64
57
 
65
58
  export const unwindFailure = (commit: string): string =>
66
59
  `gtd could not unwind ${commit} out of your working tree.`
@@ -202,7 +195,7 @@ ${questionBarReturn}
202
195
  never re-open one. A genuinely open PRODUCT point may still
203
196
  raise a fresh \`## Open Questions\` entry: the product gate sits
204
197
  on this path exactly as on the first lap
205
- - Before grouping anything, run \`${vars.testCommand}\`
198
+ - Before grouping anything, run \`${env.testCommand}\`
206
199
  yourself — a loop-back runs no green-baseline gate, so the
207
200
  tree may already be red (reverting the edit can undo a fix it
208
201
  made). If red, make that breakage the first concern, ahead of
@@ -814,10 +807,13 @@ every commit is machine-authored.
814
807
  high-level architectural changes — never which files changed; \`git
815
808
  diff --stat\` is for that, not this message
816
809
  - If the range removes or renames a documented config key, CLI flag, or
817
- workflow state — a public surface, not an internal one — add a literal
818
- \`BREAKING CHANGE:\` footer naming what disappears. A \`!\` on the type
819
- prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's own
820
- commit-analyzer has missed that marker before, cutting no release
810
+ workflow state, OR makes a previously-accepted config value now fail to
811
+ load (a stricter validation, an escape a value must now spell differently)
812
+ — a public surface, not an internal one, either way — add a literal
813
+ \`BREAKING CHANGE:\` footer naming what breaks and, for the second case,
814
+ the exact escape or rewrite a committed \`.gtdrc\` needs. A \`!\` on the
815
+ type prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's
816
+ own commit-analyzer has missed that marker before, cutting no release
821
817
  until a follow-up commit carried the literal footer instead
822
818
 
823
819
  The process's entry commit is \`${it.entryCommit}\`. ${human}Inspect the range: \`git log ${it.processBase}..${it.processTip}\`
@@ -31,7 +31,8 @@ import * as t from "./text.js"
31
31
  // re-exported below.
32
32
 
33
33
  export { threads, type ThreadInfo } from "../flows/index.js"
34
- export { defaults } from "./vars.js"
34
+ export { defaults, envDefaults } from "./vars.js"
35
+ export { skills } from "./skills.js"
35
36
  export { agentWithSkills } from "./text.js"
36
37
  export * from "./steps.js"
37
38
  export * from "./health.js"
@@ -1,23 +1,27 @@
1
- // The bundled workflow's variable defaults. `.gtdrc` `vars:`, `--var` and
2
- // `GTD_<NAME>` override any of them.
1
+ // The bundled workflow's settings, in two kinds. A process setting changes
2
+ // which step comes next, so it is pinned for the whole process at its start;
3
+ // an environment setting only changes how a step runs on this machine, so it
4
+ // is read live on every invocation. `.gtdrc` `vars:`/`env:`, `--var` (process
5
+ // settings only) and `GTD_<NAME>` override either.
6
+
7
+ /** Process settings. */
3
8
  export const defaults: Readonly<Record<string, string>> = {
4
- testCommand: "npm test",
5
- plannerModel: "smart",
6
- coderModel: "base",
7
9
  reviewBase: "",
8
10
  judgeBudgetBytes: "32768",
9
11
  judgeIdenticalMinP: "0.7",
10
12
  specPreJudge: "0.9",
11
13
  reviewNoteActionable: "0.7",
12
14
  architectureSkipMinP: "0.85",
13
- triageSkills: "spec-driven-development, planning-and-task-breakdown",
14
- architectureSkills: "api-and-interface-design, documentation-and-adrs, ponytail",
15
- decomposeSkills: "incremental-implementation, planning-and-task-breakdown",
16
- buildSkills: "test-driven-development, incremental-implementation",
17
- fixSkills: "debugging-and-error-recovery",
18
- reviewFixSkills: "incremental-implementation, code-simplification",
19
- reviewSkills: "code-review-and-quality",
20
- specReviewSkills: "code-review-and-quality, spec-driven-development",
21
- escalateSkills: "debugging-and-error-recovery",
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.
22
19
  qualityReviews: "owasp-security, ponytail-review, test-audit",
23
20
  }
21
+
22
+ /** Environment settings. */
23
+ export const envDefaults: Readonly<Record<string, string>> = {
24
+ testCommand: "npm test",
25
+ plannerModel: "smart",
26
+ coderModel: "base",
27
+ }