@plainconceptsplatform/agent-harness 2.2.1 → 2.3.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 +1 -1
- package/src/commands/update.js +13 -1
- package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -219
- package/src/content/.agents/skills/pc-make-engineer/template.md +80 -81
- package/src/content/.opencode/plugins/pc-subagent-tiers.js +337 -281
- package/src/content/opencode.jsonc +41 -39
- package/src/steps/copy/fullstack-engineer.js +88 -85
- package/src/steps/copy/opencode-json.js +161 -147
- package/src/steps/metadata/index.js +4 -0
- package/src/steps/optimization/detect.js +66 -0
- package/src/utils/agent-color.js +83 -0
|
@@ -1,281 +1,337 @@
|
|
|
1
|
-
// pc-subagent-tiers: on startup, reads *-engineer.md templates and creates
|
|
2
|
-
// tier variant files with the resolved model, plus the two primary agents the
|
|
3
|
-
// user actually talks to. Everything generated here is gitignored and rebuilt
|
|
4
|
-
// every startup. Model resolution: user override > team config.
|
|
5
|
-
//
|
|
6
|
-
// Agent topology:
|
|
7
|
-
// build.md / plan.md mode: primary the only agents a human selects.
|
|
8
|
-
// Both are fullstack-engineer with a
|
|
9
|
-
// tier model; plan cannot edit.
|
|
10
|
-
// fullstack-engineer.md mode: subagent the shared body of build and plan,
|
|
11
|
-
// and the fallback worker.
|
|
12
|
-
// *-engineer.md mode: subagent specialists, spawned by task().
|
|
13
|
-
// *-engineer.<tier>.md mode: subagent the same specialist pinned to a tier.
|
|
14
|
-
//
|
|
15
|
-
// Overriding opencode's built-in build and plan (rather than disabling them, as
|
|
16
|
-
// earlier versions did) means the agent picker offers exactly two entries and
|
|
17
|
-
// both carry our prompt and abilities.
|
|
18
|
-
|
|
19
|
-
import fs from "node:fs/promises"
|
|
20
|
-
import path from "node:path"
|
|
21
|
-
|
|
22
|
-
const TIERS = ["build", "fast", "plan"]
|
|
23
|
-
|
|
24
|
-
// The two primaries, and the tier each takes its model from. plan denies edit
|
|
25
|
-
// so a planning session cannot mutate the tree; bash stays allowed because the
|
|
26
|
-
// planning skills shell out to git and openspec to read state.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
description: "
|
|
36
|
-
permission:
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
await Promise.all(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
1
|
+
// pc-subagent-tiers: on startup, reads *-engineer.md templates and creates
|
|
2
|
+
// tier variant files with the resolved model, plus the two primary agents the
|
|
3
|
+
// user actually talks to. Everything generated here is gitignored and rebuilt
|
|
4
|
+
// every startup. Model resolution: user override > team config.
|
|
5
|
+
//
|
|
6
|
+
// Agent topology:
|
|
7
|
+
// build.md / plan.md mode: primary the only agents a human selects.
|
|
8
|
+
// Both are fullstack-engineer with a
|
|
9
|
+
// tier model; plan cannot edit.
|
|
10
|
+
// fullstack-engineer.md mode: subagent the shared body of build and plan,
|
|
11
|
+
// and the fallback worker.
|
|
12
|
+
// *-engineer.md mode: subagent specialists, spawned by task().
|
|
13
|
+
// *-engineer.<tier>.md mode: subagent the same specialist pinned to a tier.
|
|
14
|
+
//
|
|
15
|
+
// Overriding opencode's built-in build and plan (rather than disabling them, as
|
|
16
|
+
// earlier versions did) means the agent picker offers exactly two entries and
|
|
17
|
+
// both carry our prompt and abilities.
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs/promises"
|
|
20
|
+
import path from "node:path"
|
|
21
|
+
|
|
22
|
+
const TIERS = ["build", "fast", "plan"]
|
|
23
|
+
|
|
24
|
+
// The two primaries, and the tier each takes its model from. plan denies edit
|
|
25
|
+
// so a planning session cannot mutate the tree; bash stays allowed because the
|
|
26
|
+
// planning skills shell out to git and openspec to read state.
|
|
27
|
+
//
|
|
28
|
+
// Their colours are theme keywords rather than derived hexes, and they are
|
|
29
|
+
// fixed: these are the two agents a human picks, so they should look the same
|
|
30
|
+
// in every project regardless of the theme in use.
|
|
31
|
+
const PRIMARIES = {
|
|
32
|
+
build: {
|
|
33
|
+
tier: "build",
|
|
34
|
+
color: "primary",
|
|
35
|
+
description: "Implement changes in this repository. Full write access, spawns specialist engineers for parallel work.",
|
|
36
|
+
permission: null,
|
|
37
|
+
},
|
|
38
|
+
plan: {
|
|
39
|
+
tier: "plan",
|
|
40
|
+
color: "warning",
|
|
41
|
+
description: "Explore and plan without touching the tree. Read-only: proposes work for build to carry out.",
|
|
42
|
+
permission: { edit: "deny" },
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// opencode's theme keywords. Treated as unset so a name-derived hex wins: the
|
|
47
|
+
// old /make-engineer template asked whoever ran it to pick one of these and
|
|
48
|
+
// avoid collisions by hand, which does not survive more than a few agents.
|
|
49
|
+
const THEME_COLORS = new Set(["primary", "secondary", "accent", "success", "warning", "error", "info"])
|
|
50
|
+
|
|
51
|
+
// Mirrors src/utils/agent-color.js in the CLI, duplicated because a plugin has
|
|
52
|
+
// to stand alone in the consumer repo. agent-color.test.js asserts the two stay
|
|
53
|
+
// in step; change one and change the other.
|
|
54
|
+
//
|
|
55
|
+
// Only the hue is hashed. Fixed saturation and lightness are what keep every
|
|
56
|
+
// result usable: hashing those too would eventually produce a near-grey,
|
|
57
|
+
// near-black or near-white agent, invisible against one theme or the other.
|
|
58
|
+
function agentColor(name) {
|
|
59
|
+
let h = 0x811c9dc5
|
|
60
|
+
for (let i = 0; i < name.length; i++) {
|
|
61
|
+
h ^= name.charCodeAt(i)
|
|
62
|
+
h = Math.imul(h, 0x01000193) >>> 0
|
|
63
|
+
}
|
|
64
|
+
const hue = (h >>> 0) % 360
|
|
65
|
+
const sat = 0.68
|
|
66
|
+
const light = 0.52
|
|
67
|
+
const c = (1 - Math.abs(2 * light - 1)) * sat
|
|
68
|
+
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1))
|
|
69
|
+
const m = light - c / 2
|
|
70
|
+
const [r, g, b] =
|
|
71
|
+
hue < 60 ? [c, x, 0] :
|
|
72
|
+
hue < 120 ? [x, c, 0] :
|
|
73
|
+
hue < 180 ? [0, c, x] :
|
|
74
|
+
hue < 240 ? [0, x, c] :
|
|
75
|
+
hue < 300 ? [x, 0, c] :
|
|
76
|
+
[c, 0, x]
|
|
77
|
+
const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0")
|
|
78
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A theme keyword or a missing value yields to the derived hex; a hex stays. */
|
|
82
|
+
function shouldDeriveColor(current) {
|
|
83
|
+
if (!current) return true
|
|
84
|
+
return THEME_COLORS.has(current.trim().toLowerCase())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const FULLSTACK_NAME = "fullstack-engineer"
|
|
88
|
+
const FULLSTACK_TEMPLATE = `${FULLSTACK_NAME}.md`
|
|
89
|
+
|
|
90
|
+
export const PcSubagentTiers = async ({ directory }) => {
|
|
91
|
+
const root = directory || process.cwd()
|
|
92
|
+
const agentsDir = path.join(root, ".opencode", "agents")
|
|
93
|
+
|
|
94
|
+
async function readJson(filePath) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(await fs.readFile(filePath, "utf-8"))
|
|
97
|
+
} catch {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function resolveModels() {
|
|
103
|
+
const [user, team] = await Promise.all([
|
|
104
|
+
readJson(path.join(root, ".opencode", "harness.user.json")),
|
|
105
|
+
readJson(path.join(root, ".opencode", "harness.json")),
|
|
106
|
+
])
|
|
107
|
+
const models = {}
|
|
108
|
+
for (const tier of TIERS) {
|
|
109
|
+
models[tier] = user?.models?.[tier] ?? team?.models?.[tier] ?? null
|
|
110
|
+
}
|
|
111
|
+
return models
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function scanEngineers() {
|
|
115
|
+
try {
|
|
116
|
+
const entries = await fs.readdir(agentsDir)
|
|
117
|
+
return {
|
|
118
|
+
templates: entries.filter(f => /^[\w-]+-engineer\.md$/.test(f) && f !== FULLSTACK_TEMPLATE).map(f => f.replace(/\.md$/, "")),
|
|
119
|
+
variantFiles: entries.filter(f => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
|
|
120
|
+
hasFullstack: entries.includes(FULLSTACK_TEMPLATE),
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
return { templates: [], variantFiles: [], hasFullstack: false }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function variantFile(name, tier) {
|
|
128
|
+
return `${name}.${tier}.md`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildVariant(templateContent, model) {
|
|
132
|
+
const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
133
|
+
const modelLine = `model: ${model}`
|
|
134
|
+
if (!fmMatch) return `---\nmode: subagent\n${modelLine}\n---\n\n${templateContent}`
|
|
135
|
+
|
|
136
|
+
let fm = fmMatch[1]
|
|
137
|
+
fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: subagent') : `mode: subagent\n${fm}`
|
|
138
|
+
fm = /^model:/m.test(fm) ? fm.replace(/^model:.*$/m, modelLine) : `${modelLine}\n${fm}`
|
|
139
|
+
|
|
140
|
+
// Slice at frontmatter end: never String.replace with content-derived
|
|
141
|
+
// strings, it matches the wrong occurrence and expands $& sequences.
|
|
142
|
+
return `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Ensure template files are mode: subagent with no stale model: line. Only
|
|
146
|
+
// build and plan are primary; every engineer is reached through task(), never
|
|
147
|
+
// picked by a human, so a primary engineer would just clutter the picker.
|
|
148
|
+
// Templates must never declare a model either: models resolve per tier at
|
|
149
|
+
// startup and are injected into generated files only, so a stale model: (from
|
|
150
|
+
// a prior `stampAgentModels` run, or from when these were primary) is
|
|
151
|
+
// stripped and the agent falls back to the session model in opencode.jsonc.
|
|
152
|
+
function normalizeTemplate(templateContent, name) {
|
|
153
|
+
const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
154
|
+
if (!fmMatch) return templateContent
|
|
155
|
+
|
|
156
|
+
let fm = fmMatch[1]
|
|
157
|
+
let changed = false
|
|
158
|
+
|
|
159
|
+
if (!/^mode:\s*subagent/m.test(fm)) {
|
|
160
|
+
fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: subagent') : `mode: subagent\n${fm}`
|
|
161
|
+
changed = true
|
|
162
|
+
}
|
|
163
|
+
// Give every agent a colour derived from its name, so the same engineer
|
|
164
|
+
// reads the same in any project and two agents never collide by accident.
|
|
165
|
+
const currentColor = fm.match(/^color:\s*(.+)$/m)?.[1]
|
|
166
|
+
if (name && shouldDeriveColor(currentColor)) {
|
|
167
|
+
const derived = `color: ${agentColor(name)}`
|
|
168
|
+
fm = /^color:/m.test(fm) ? fm.replace(/^color:.*$/m, derived) : `${fm}\n${derived}`
|
|
169
|
+
changed = true
|
|
170
|
+
}
|
|
171
|
+
if (/^model:/m.test(fm)) {
|
|
172
|
+
// Remove the model: line and its trailing newline without leaving a
|
|
173
|
+
// leading blank line (model: can be the first frontmatter entry).
|
|
174
|
+
fm = fm.replace(/^model:[^\n]*\r?\n?/m, '').replace(/^\r?\n/, '').replace(/\n$/, '')
|
|
175
|
+
changed = true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return changed ? `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}` : templateContent
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function descriptionOf(templateContent) {
|
|
182
|
+
return templateContent.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Build a primary from the fullstack body: same identity and the same
|
|
186
|
+
// ## Abilities block, with our own frontmatter on top. The body is taken
|
|
187
|
+
// verbatim so /make-engineer only has to maintain fullstack-engineer.md and
|
|
188
|
+
// both primaries inherit whatever it lists.
|
|
189
|
+
function buildPrimary(name, spec, fullstackContent, model) {
|
|
190
|
+
const body = fullstackContent.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim()
|
|
191
|
+
|
|
192
|
+
const lines = [
|
|
193
|
+
'---',
|
|
194
|
+
`description: ${spec.description}`,
|
|
195
|
+
'mode: primary',
|
|
196
|
+
]
|
|
197
|
+
if (model) lines.push(`model: ${model}`)
|
|
198
|
+
lines.push(`color: ${spec.color}`)
|
|
199
|
+
lines.push('permission:')
|
|
200
|
+
// plan denies edit; everything else stays allowed so the planning skills can
|
|
201
|
+
// still read the tree, shell out to git and openspec, and spawn engineers.
|
|
202
|
+
lines.push(` edit: ${spec.permission?.edit ?? 'allow'}`)
|
|
203
|
+
for (const key of ['bash', 'read', 'glob', 'grep', 'question', 'todowrite', 'task', 'skill']) {
|
|
204
|
+
lines.push(` ${key}: ${spec.permission?.[key] ?? 'allow'}`)
|
|
205
|
+
}
|
|
206
|
+
lines.push('---')
|
|
207
|
+
|
|
208
|
+
return `${lines.join('\n')}\n\n${body}\n`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Skip writing if the file already has identical content.
|
|
212
|
+
async function writeIfChanged(filePath, content) {
|
|
213
|
+
try {
|
|
214
|
+
if ((await fs.readFile(filePath, "utf-8")) === content) return
|
|
215
|
+
} catch {
|
|
216
|
+
// file missing, write it
|
|
217
|
+
}
|
|
218
|
+
const tmpPath = `${filePath}.tmp`
|
|
219
|
+
await fs.writeFile(tmpPath, content, "utf-8")
|
|
220
|
+
await fs.rename(tmpPath, filePath)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
config: async (cfg) => {
|
|
225
|
+
try {
|
|
226
|
+
const models = await resolveModels()
|
|
227
|
+
const available = TIERS.filter(t => models[t])
|
|
228
|
+
const { templates, variantFiles, hasFullstack } = await scanEngineers()
|
|
229
|
+
|
|
230
|
+
// build.md and plan.md are regenerated from fullstack-engineer.md every
|
|
231
|
+
// startup, which is also how an edit to fullstack propagates to both.
|
|
232
|
+
// normalizeTemplate runs on it too, so a repo carrying the old
|
|
233
|
+
// mode: primary fullstack is migrated in place on first launch.
|
|
234
|
+
if (hasFullstack) {
|
|
235
|
+
const fullstackPath = path.join(agentsDir, FULLSTACK_TEMPLATE)
|
|
236
|
+
const rawFullstack = await fs.readFile(fullstackPath, "utf-8")
|
|
237
|
+
const fullstack = normalizeTemplate(rawFullstack, FULLSTACK_NAME)
|
|
238
|
+
if (fullstack !== rawFullstack) {
|
|
239
|
+
await writeIfChanged(fullstackPath, fullstack)
|
|
240
|
+
console.error(`[pc-subagent-tiers] Normalized ${FULLSTACK_TEMPLATE} (mode: subagent)`)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const [name, spec] of Object.entries(PRIMARIES)) {
|
|
244
|
+
const model = models[spec.tier]
|
|
245
|
+
await writeIfChanged(
|
|
246
|
+
path.join(agentsDir, `${name}.md`),
|
|
247
|
+
buildPrimary(name, spec, fullstack, model),
|
|
248
|
+
)
|
|
249
|
+
if (cfg?.agent) {
|
|
250
|
+
// Override opencode's built-in agent of the same name rather than
|
|
251
|
+
// disabling it, so the picker shows ours with our model.
|
|
252
|
+
cfg.agent[name] = {
|
|
253
|
+
...cfg.agent[name],
|
|
254
|
+
mode: 'primary',
|
|
255
|
+
description: spec.description,
|
|
256
|
+
color: spec.color,
|
|
257
|
+
...(model ? { model } : {}),
|
|
258
|
+
...(spec.permission ? { permission: { ...cfg.agent[name]?.permission, ...spec.permission } } : {}),
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
console.error(`[pc-subagent-tiers] Wrote primaries: build (${models.build ?? 'session model'}), plan (${models.plan ?? 'session model'})`)
|
|
263
|
+
} else {
|
|
264
|
+
console.error(`[pc-subagent-tiers] ${FULLSTACK_TEMPLATE} missing: build and plan not generated`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const templateContents = await Promise.all(
|
|
268
|
+
templates.map(async name => {
|
|
269
|
+
const rawContent = await fs.readFile(path.join(agentsDir, `${name}.md`), "utf-8")
|
|
270
|
+
const content = normalizeTemplate(rawContent, name)
|
|
271
|
+
// If the template had the wrong mode or a stale model:, persist the fix to disk
|
|
272
|
+
if (content !== rawContent) {
|
|
273
|
+
await writeIfChanged(path.join(agentsDir, `${name}.md`), content)
|
|
274
|
+
console.error(`[pc-subagent-tiers] Normalized ${name}.md (mode: subagent, model: removed)`)
|
|
275
|
+
}
|
|
276
|
+
return { name, content }
|
|
277
|
+
})
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const keepSet = new Set()
|
|
281
|
+
const variantsToWrite = []
|
|
282
|
+
|
|
283
|
+
for (const { name, content } of templateContents) {
|
|
284
|
+
for (const tier of available) {
|
|
285
|
+
const file = variantFile(name, tier)
|
|
286
|
+
variantsToWrite.push({
|
|
287
|
+
file,
|
|
288
|
+
path: path.join(agentsDir, file),
|
|
289
|
+
content: buildVariant(content, models[tier]),
|
|
290
|
+
name,
|
|
291
|
+
tier,
|
|
292
|
+
templateContent: content,
|
|
293
|
+
})
|
|
294
|
+
keepSet.add(file)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await Promise.all(variantsToWrite.map(v => writeIfChanged(v.path, v.content)))
|
|
299
|
+
|
|
300
|
+
if (cfg?.agent) {
|
|
301
|
+
// Ensure base templates are always mode: subagent in-memory. A repo
|
|
302
|
+
// upgraded from an earlier version may still have primary in config.
|
|
303
|
+
for (const { name } of templateContents) {
|
|
304
|
+
if (cfg.agent[name]) {
|
|
305
|
+
cfg.agent[name].mode = 'subagent'
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (cfg.agent[FULLSTACK_NAME]) {
|
|
309
|
+
cfg.agent[FULLSTACK_NAME].mode = 'subagent'
|
|
310
|
+
}
|
|
311
|
+
for (const { name, tier, templateContent } of variantsToWrite) {
|
|
312
|
+
const base = cfg.agent[name]
|
|
313
|
+
cfg.agent[`${name}.${tier}`] = base
|
|
314
|
+
? { ...base, mode: 'subagent', model: models[tier] }
|
|
315
|
+
: {
|
|
316
|
+
mode: "subagent",
|
|
317
|
+
description: descriptionOf(templateContent) ?? `${name} (${tier} tier)`,
|
|
318
|
+
model: models[tier],
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
await Promise.all(
|
|
324
|
+
variantFiles.filter(f => !keepSet.has(f)).map(f => fs.unlink(path.join(agentsDir, f)).catch(() => {}))
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
if (variantsToWrite.length > 0) {
|
|
328
|
+
console.error(`[pc-subagent-tiers] Created ${variantsToWrite.length} variant files (${templates.length} engineers x ${available.length} tiers)`)
|
|
329
|
+
} else if (templates.length > 0) {
|
|
330
|
+
console.error(`[pc-subagent-tiers] No variants created. Models: ${JSON.stringify(models)}`)
|
|
331
|
+
}
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.error(`[pc-subagent-tiers] Error: ${err.message}`)
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
}
|