@plainconceptsplatform/agent-harness 2.9.0 → 3.0.1
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/README.md +5 -5
- package/cli/presets/quota.json +6 -2
- package/cli/steps/copy/opencode-json.js +260 -135
- package/cli/steps/optimization/quota.js +36 -20
- package/cli/utils/copy.js +0 -1
- package/harness/.opencode/_gitignore +9 -9
- package/harness/.opencode/package.json +1 -5
- package/harness/.opencode/plugins/pc-subagent-monitor.js +144 -179
- package/harness/.opencode/plugins/pc-subagent-tiers.js +216 -248
- package/harness/.opencode/plugins/pc-system-reminders.js +386 -434
- package/harness/opencode.jsonc +21 -32
- package/package.json +1 -1
- package/skills/pc-guardrails-generic/SKILL.md +15 -10
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/template.md +36 -42
- package/skills/pc-make-guardrails/category-reference.md +2 -2
- package/harness/.opencode/tui/pc-subagents.tsx +0 -98
- package/harness/.opencode/tui.json +0 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
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
|
|
3
|
-
// user actually talks to. Everything generated here is gitignored and
|
|
4
|
-
// every startup. Model resolution: user override > team config.
|
|
2
|
+
// tier variant agent files with the resolved model, plus the two primary agents
|
|
3
|
+
// the user actually talks to. Everything generated here is gitignored and
|
|
4
|
+
// rebuilt every startup. Model resolution: user override > team config.
|
|
5
5
|
//
|
|
6
6
|
// Agent topology:
|
|
7
7
|
// build.md / plan.md mode: primary the only agents a human selects.
|
|
@@ -10,39 +10,41 @@
|
|
|
10
10
|
// nor spawn.
|
|
11
11
|
// fullstack-engineer.md mode: subagent the shared body of build and plan,
|
|
12
12
|
// and the fallback worker.
|
|
13
|
-
// *-engineer.md mode: subagent specialists, spawned by
|
|
13
|
+
// *-engineer.md mode: subagent specialists, spawned by subagent().
|
|
14
14
|
// *-engineer.<tier>.md mode: subagent the same specialist pinned to a tier.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// OpenCode 2 discovers agents from .opencode/agents/*.md and overrides a built-in
|
|
17
|
+
// by ID, so the generated files replace the built-in build/plan. The plugin API's
|
|
18
|
+
// agent editor can update or remove an existing agent but cannot add one, so file
|
|
19
|
+
// generation is the mechanism: writing the files in setup, before agents resolve,
|
|
20
|
+
// is what makes them visible.
|
|
19
21
|
|
|
22
|
+
import { Plugin } from "@opencode/plugin"
|
|
20
23
|
import fs from "node:fs/promises"
|
|
21
24
|
import path from "node:path"
|
|
22
25
|
|
|
23
26
|
const TIERS = ["build", "fast", "plan"]
|
|
24
27
|
|
|
25
28
|
// The two primaries, and the tier each takes its model from. plan denies edit
|
|
26
|
-
// and
|
|
27
|
-
// spawn a worker that would do it on its behalf.
|
|
29
|
+
// and subagent: a planning session cannot mutate the tree itself, and it cannot
|
|
30
|
+
// spawn a worker that would do it on its behalf. shell stays allowed because the
|
|
28
31
|
// planning skills shell out to git and openspec to read state, and
|
|
29
32
|
// pc-system-reminders holds it to inspection commands.
|
|
30
|
-
//
|
|
31
|
-
// Their colours are theme keywords rather than derived hexes, and they are
|
|
32
|
-
// fixed: these are the two agents a human picks, so they should look the same
|
|
33
|
-
// in every project regardless of the theme in use.
|
|
34
33
|
const PRIMARIES = {
|
|
35
34
|
build: {
|
|
36
35
|
tier: "build",
|
|
37
36
|
color: "primary",
|
|
38
37
|
description: "Implement changes in this repository. Full write access, spawns specialist engineers for parallel work.",
|
|
39
|
-
|
|
38
|
+
permissions: null,
|
|
40
39
|
},
|
|
41
40
|
plan: {
|
|
42
41
|
tier: "plan",
|
|
43
42
|
color: "warning",
|
|
44
43
|
description: "Explore and plan without touching the tree. Read-only: proposes work for build to carry out.",
|
|
45
|
-
|
|
44
|
+
permissions: [
|
|
45
|
+
{ action: "edit", resource: "*", effect: "deny" },
|
|
46
|
+
{ action: "subagent", resource: "*", effect: "deny" },
|
|
47
|
+
],
|
|
46
48
|
},
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -51,6 +53,20 @@ const PRIMARIES = {
|
|
|
51
53
|
// avoid collisions by hand, which does not survive more than a few agents.
|
|
52
54
|
const THEME_COLORS = new Set(["primary", "secondary", "accent", "success", "warning", "error", "info"])
|
|
53
55
|
|
|
56
|
+
// Fields V1 agent frontmatter used that V2 forbids. Stripped on sight so a repo
|
|
57
|
+
// upgraded in place does not carry a definition V2 refuses to load.
|
|
58
|
+
const LEGACY_FIELDS = [
|
|
59
|
+
"model",
|
|
60
|
+
"permission",
|
|
61
|
+
"prompt",
|
|
62
|
+
"disable",
|
|
63
|
+
"disabled",
|
|
64
|
+
"temperature",
|
|
65
|
+
"top_p",
|
|
66
|
+
"maxSteps",
|
|
67
|
+
"tools",
|
|
68
|
+
]
|
|
69
|
+
|
|
54
70
|
// Mirrors cli/utils/agent-color.js in the CLI, duplicated because a plugin has
|
|
55
71
|
// to stand alone in the consumer repo. agent-color.test.js asserts the two stay
|
|
56
72
|
// in step; change one and change the other.
|
|
@@ -102,12 +118,49 @@ function unquoteColor(raw) {
|
|
|
102
118
|
return (raw ?? "").trim().replace(/^["']/, "").replace(/["']$/, "")
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
function splitFrontmatter(content) {
|
|
122
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
123
|
+
if (!match) return { frontmatter: null, body: content }
|
|
124
|
+
return { frontmatter: match[1], body: content.slice(match[0].length) }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function stripLegacyFields(frontmatter) {
|
|
128
|
+
const lines = frontmatter.split(/\r?\n/)
|
|
129
|
+
const kept = []
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const key = lines[i].match(/^([A-Za-z0-9_-]+):/)?.[1]
|
|
132
|
+
if (key && LEGACY_FIELDS.includes(key)) {
|
|
133
|
+
// Drop the key and any indented continuation lines that belong to it.
|
|
134
|
+
i++
|
|
135
|
+
while (i < lines.length && /^\s+\S/.test(lines[i])) i++
|
|
136
|
+
i--
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
kept.push(lines[i])
|
|
140
|
+
}
|
|
141
|
+
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setScalar(frontmatter, key, value) {
|
|
145
|
+
const line = `${key}: ${value}`
|
|
146
|
+
if (new RegExp(`^${key}:`, "m").test(frontmatter)) {
|
|
147
|
+
return frontmatter.replace(new RegExp(`^${key}:.*$`, "m"), () => line)
|
|
148
|
+
}
|
|
149
|
+
return frontmatter ? `${line}\n${frontmatter}` : line
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function permissionsYaml(permissions) {
|
|
153
|
+
return [
|
|
154
|
+
"permissions:",
|
|
155
|
+
...permissions.flatMap(p => [
|
|
156
|
+
` - action: ${p.action}`,
|
|
157
|
+
` resource: ${p.resource}`,
|
|
158
|
+
` effect: ${p.effect}`,
|
|
159
|
+
]),
|
|
160
|
+
].join("\n")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The colour line an agent should have, or null when the current one is right. */
|
|
111
164
|
function colorLineFor(name, rawCurrent) {
|
|
112
165
|
const current = unquoteColor(rawCurrent)
|
|
113
166
|
const target = shouldDeriveColor(current) ? agentColor(name) : current
|
|
@@ -118,254 +171,169 @@ function colorLineFor(name, rawCurrent) {
|
|
|
118
171
|
const FULLSTACK_NAME = "fullstack-engineer"
|
|
119
172
|
const FULLSTACK_TEMPLATE = `${FULLSTACK_NAME}.md`
|
|
120
173
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
async function readJson(filePath) {
|
|
126
|
-
try {
|
|
127
|
-
return JSON.parse(await fs.readFile(filePath, "utf-8"))
|
|
128
|
-
} catch {
|
|
129
|
-
return null
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function resolveModels() {
|
|
134
|
-
const [user, team] = await Promise.all([
|
|
135
|
-
readJson(path.join(root, ".opencode", "harness.user.json")),
|
|
136
|
-
readJson(path.join(root, ".opencode", "harness.json")),
|
|
137
|
-
])
|
|
138
|
-
const models = {}
|
|
139
|
-
for (const tier of TIERS) {
|
|
140
|
-
models[tier] = user?.models?.[tier] ?? team?.models?.[tier] ?? null
|
|
141
|
-
}
|
|
142
|
-
return models
|
|
143
|
-
}
|
|
174
|
+
/** Rewrite a template's frontmatter to a clean V2 subagent definition. */
|
|
175
|
+
function normalizeTemplate(content, name) {
|
|
176
|
+
const { frontmatter, body } = splitFrontmatter(content)
|
|
177
|
+
if (frontmatter === null) return `---\nmode: subagent\ncolor: ${yamlColor(agentColor(name))}\n---\n\n${content}`
|
|
144
178
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
hasFullstack: entries.includes(FULLSTACK_TEMPLATE),
|
|
152
|
-
}
|
|
153
|
-
} catch {
|
|
154
|
-
return { templates: [], variantFiles: [], hasFullstack: false }
|
|
155
|
-
}
|
|
156
|
-
}
|
|
179
|
+
let fm = stripLegacyFields(frontmatter)
|
|
180
|
+
fm = setScalar(fm, "mode", "subagent")
|
|
181
|
+
const colorLine = colorLineFor(name, fm.match(/^color:\s*(.+)$/m)?.[1])
|
|
182
|
+
if (colorLine) fm = setScalar(fm, "color", colorLine.slice("color: ".length))
|
|
183
|
+
return `---\n${fm}\n---\n${body}`
|
|
184
|
+
}
|
|
157
185
|
|
|
158
|
-
|
|
159
|
-
|
|
186
|
+
/** A tier-pinned copy of a specialist: same body, resolved model. */
|
|
187
|
+
function buildVariant(templateContent, model) {
|
|
188
|
+
const { frontmatter, body } = splitFrontmatter(templateContent)
|
|
189
|
+
let fm = stripLegacyFields(frontmatter ?? "")
|
|
190
|
+
fm = setScalar(fm, "mode", "subagent")
|
|
191
|
+
fm = setScalar(fm, "model", model)
|
|
192
|
+
if (!/^color:/m.test(fm)) {
|
|
193
|
+
const name = ""
|
|
194
|
+
fm = setScalar(fm, "color", name ? yamlColor(agentColor(name)) : yamlColor(agentColor("engineer")))
|
|
160
195
|
}
|
|
196
|
+
return `---\n${fm}\n---\n${body}`
|
|
197
|
+
}
|
|
161
198
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Build a primary from the fullstack body: same identity and the same
|
|
201
|
+
* ## Abilities block, with our own frontmatter on top. The body is taken
|
|
202
|
+
* verbatim so /make-engineer only has to maintain fullstack-engineer.md and
|
|
203
|
+
* both primaries inherit whatever it lists.
|
|
204
|
+
*/
|
|
205
|
+
function buildPrimary(spec, fullstackContent, model) {
|
|
206
|
+
const { body } = splitFrontmatter(fullstackContent)
|
|
207
|
+
const lines = [
|
|
208
|
+
"---",
|
|
209
|
+
`description: ${spec.description}`,
|
|
210
|
+
"mode: primary",
|
|
211
|
+
]
|
|
212
|
+
if (model) lines.push(`model: ${model}`)
|
|
213
|
+
lines.push(`color: ${yamlColor(spec.color)}`)
|
|
214
|
+
if (spec.permissions) lines.push(permissionsYaml(spec.permissions))
|
|
215
|
+
lines.push("---")
|
|
216
|
+
return `${lines.join("\n")}\n\n${body.trim()}\n`
|
|
217
|
+
}
|
|
170
218
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return
|
|
219
|
+
async function readJson(filePath) {
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(await fs.readFile(filePath, "utf-8"))
|
|
222
|
+
} catch {
|
|
223
|
+
return null
|
|
174
224
|
}
|
|
225
|
+
}
|
|
175
226
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (!fmMatch) return templateContent
|
|
186
|
-
|
|
187
|
-
let fm = fmMatch[1]
|
|
188
|
-
let changed = false
|
|
189
|
-
|
|
190
|
-
if (!/^mode:\s*subagent/m.test(fm)) {
|
|
191
|
-
fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: subagent') : `mode: subagent\n${fm}`
|
|
192
|
-
changed = true
|
|
227
|
+
async function scanEngineers(agentsDir) {
|
|
228
|
+
try {
|
|
229
|
+
const entries = await fs.readdir(agentsDir)
|
|
230
|
+
return {
|
|
231
|
+
templates: entries
|
|
232
|
+
.filter(f => /^[\w-]+-engineer\.md$/.test(f) && f !== FULLSTACK_TEMPLATE)
|
|
233
|
+
.map(f => f.replace(/\.md$/, "")),
|
|
234
|
+
variantFiles: entries.filter(f => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
|
|
235
|
+
hasFullstack: entries.includes(FULLSTACK_TEMPLATE),
|
|
193
236
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// colorLineFor also repairs a bare #hex written by an earlier version,
|
|
197
|
-
// which YAML read as a comment and opencode rejected outright.
|
|
198
|
-
if (name) {
|
|
199
|
-
const colorLine = colorLineFor(name, fm.match(/^color:\s*(.+)$/m)?.[1])
|
|
200
|
-
if (colorLine) {
|
|
201
|
-
fm = /^color:/m.test(fm) ? fm.replace(/^color:.*$/m, () => colorLine) : `${fm}\n${colorLine}`
|
|
202
|
-
changed = true
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (/^model:/m.test(fm)) {
|
|
206
|
-
// Remove the model: line and its trailing newline without leaving a
|
|
207
|
-
// leading blank line (model: can be the first frontmatter entry).
|
|
208
|
-
fm = fm.replace(/^model:[^\n]*\r?\n?/m, '').replace(/^\r?\n/, '').replace(/\n$/, '')
|
|
209
|
-
changed = true
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return changed ? `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}` : templateContent
|
|
237
|
+
} catch {
|
|
238
|
+
return { templates: [], variantFiles: [], hasFullstack: false }
|
|
213
239
|
}
|
|
240
|
+
}
|
|
214
241
|
|
|
215
|
-
|
|
216
|
-
|
|
242
|
+
async function writeIfChanged(filePath, content) {
|
|
243
|
+
try {
|
|
244
|
+
if ((await fs.readFile(filePath, "utf-8")) === content) return
|
|
245
|
+
} catch {
|
|
246
|
+
// file missing, write it
|
|
217
247
|
}
|
|
248
|
+
const tmpPath = `${filePath}.tmp`
|
|
249
|
+
await fs.writeFile(tmpPath, content, "utf-8")
|
|
250
|
+
await fs.rename(tmpPath, filePath)
|
|
251
|
+
}
|
|
218
252
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const body = fullstackContent.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim()
|
|
225
|
-
|
|
226
|
-
const lines = [
|
|
227
|
-
'---',
|
|
228
|
-
`description: ${spec.description}`,
|
|
229
|
-
'mode: primary',
|
|
230
|
-
]
|
|
231
|
-
if (model) lines.push(`model: ${model}`)
|
|
232
|
-
lines.push(`color: ${yamlColor(spec.color)}`)
|
|
233
|
-
lines.push('permission:')
|
|
234
|
-
// plan denies edit and task; the rest stays allowed so the planning skills
|
|
235
|
-
// can still read the tree and shell out to git and openspec.
|
|
236
|
-
lines.push(` edit: ${spec.permission?.edit ?? 'allow'}`)
|
|
237
|
-
for (const key of ['bash', 'read', 'glob', 'grep', 'question', 'todowrite', 'task', 'skill']) {
|
|
238
|
-
lines.push(` ${key}: ${spec.permission?.[key] ?? 'allow'}`)
|
|
239
|
-
}
|
|
240
|
-
lines.push('---')
|
|
241
|
-
|
|
242
|
-
return `${lines.join('\n')}\n\n${body}\n`
|
|
243
|
-
}
|
|
253
|
+
export default Plugin.define({
|
|
254
|
+
id: "pc.subagent-tiers",
|
|
255
|
+
async setup(ctx) {
|
|
256
|
+
const root = ctx.location.directory
|
|
257
|
+
const agentsDir = path.join(root, ".opencode", "agents")
|
|
244
258
|
|
|
245
|
-
// Skip writing if the file already has identical content.
|
|
246
|
-
async function writeIfChanged(filePath, content) {
|
|
247
259
|
try {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
config: async (cfg) => {
|
|
259
|
-
try {
|
|
260
|
-
const models = await resolveModels()
|
|
261
|
-
const available = TIERS.filter(t => models[t])
|
|
262
|
-
const { templates, variantFiles, hasFullstack } = await scanEngineers()
|
|
260
|
+
const [user, team] = await Promise.all([
|
|
261
|
+
readJson(path.join(root, ".opencode", "harness.user.json")),
|
|
262
|
+
readJson(path.join(root, ".opencode", "harness.json")),
|
|
263
|
+
])
|
|
264
|
+
const models = {}
|
|
265
|
+
for (const tier of TIERS) {
|
|
266
|
+
models[tier] = user?.models?.[tier] ?? team?.models?.[tier] ?? null
|
|
267
|
+
}
|
|
268
|
+
const available = TIERS.filter(t => models[t])
|
|
263
269
|
|
|
264
|
-
|
|
265
|
-
// startup, which is also how an edit to fullstack propagates to both.
|
|
266
|
-
// normalizeTemplate runs on it too, so a repo carrying the old
|
|
267
|
-
// mode: primary fullstack is migrated in place on first launch.
|
|
268
|
-
if (hasFullstack) {
|
|
269
|
-
const fullstackPath = path.join(agentsDir, FULLSTACK_TEMPLATE)
|
|
270
|
-
const rawFullstack = await fs.readFile(fullstackPath, "utf-8")
|
|
271
|
-
const fullstack = normalizeTemplate(rawFullstack, FULLSTACK_NAME)
|
|
272
|
-
if (fullstack !== rawFullstack) {
|
|
273
|
-
await writeIfChanged(fullstackPath, fullstack)
|
|
274
|
-
console.error(`[pc-subagent-tiers] Normalized ${FULLSTACK_TEMPLATE} (mode: subagent)`)
|
|
275
|
-
}
|
|
270
|
+
const { templates, variantFiles, hasFullstack } = await scanEngineers(agentsDir)
|
|
276
271
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Override opencode's built-in agent of the same name rather than
|
|
285
|
-
// disabling it, so the picker shows ours with our model.
|
|
286
|
-
cfg.agent[name] = {
|
|
287
|
-
...cfg.agent[name],
|
|
288
|
-
mode: 'primary',
|
|
289
|
-
description: spec.description,
|
|
290
|
-
color: spec.color,
|
|
291
|
-
...(model ? { model } : {}),
|
|
292
|
-
...(spec.permission ? { permission: { ...cfg.agent[name]?.permission, ...spec.permission } } : {}),
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
console.error(`[pc-subagent-tiers] Wrote primaries: build (${models.build ?? 'session model'}), plan (${models.plan ?? 'session model'})`)
|
|
297
|
-
} else {
|
|
298
|
-
console.error(`[pc-subagent-tiers] ${FULLSTACK_TEMPLATE} missing: build and plan not generated`)
|
|
272
|
+
if (hasFullstack) {
|
|
273
|
+
const fullstackPath = path.join(agentsDir, FULLSTACK_TEMPLATE)
|
|
274
|
+
const rawFullstack = await fs.readFile(fullstackPath, "utf-8")
|
|
275
|
+
const fullstack = normalizeTemplate(rawFullstack, FULLSTACK_NAME)
|
|
276
|
+
if (fullstack !== rawFullstack) {
|
|
277
|
+
await writeIfChanged(fullstackPath, fullstack)
|
|
278
|
+
console.error("[pc-subagent-tiers] Normalized fullstack-engineer.md (V2 subagent)")
|
|
299
279
|
}
|
|
300
280
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (content !== rawContent) {
|
|
307
|
-
await writeIfChanged(path.join(agentsDir, `${name}.md`), content)
|
|
308
|
-
console.error(`[pc-subagent-tiers] Normalized ${name}.md (mode: subagent, model: removed)`)
|
|
309
|
-
}
|
|
310
|
-
return { name, content }
|
|
311
|
-
})
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
const keepSet = new Set()
|
|
315
|
-
const variantsToWrite = []
|
|
316
|
-
|
|
317
|
-
for (const { name, content } of templateContents) {
|
|
318
|
-
for (const tier of available) {
|
|
319
|
-
const file = variantFile(name, tier)
|
|
320
|
-
variantsToWrite.push({
|
|
321
|
-
file,
|
|
322
|
-
path: path.join(agentsDir, file),
|
|
323
|
-
content: buildVariant(content, models[tier]),
|
|
324
|
-
name,
|
|
325
|
-
tier,
|
|
326
|
-
templateContent: content,
|
|
327
|
-
})
|
|
328
|
-
keepSet.add(file)
|
|
329
|
-
}
|
|
281
|
+
for (const [name, spec] of Object.entries(PRIMARIES)) {
|
|
282
|
+
await writeIfChanged(
|
|
283
|
+
path.join(agentsDir, `${name}.md`),
|
|
284
|
+
buildPrimary(spec, fullstack, models[spec.tier]),
|
|
285
|
+
)
|
|
330
286
|
}
|
|
287
|
+
console.error(`[pc-subagent-tiers] Wrote primaries: build (${models.build ?? "session model"}), plan (${models.plan ?? "session model"})`)
|
|
288
|
+
} else {
|
|
289
|
+
console.error(`[pc-subagent-tiers] ${FULLSTACK_TEMPLATE} missing: build and plan not generated`)
|
|
290
|
+
}
|
|
331
291
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
for (const { name, tier, templateContent } of variantsToWrite) {
|
|
346
|
-
const base = cfg.agent[name]
|
|
347
|
-
cfg.agent[`${name}.${tier}`] = base
|
|
348
|
-
? { ...base, mode: 'subagent', model: models[tier] }
|
|
349
|
-
: {
|
|
350
|
-
mode: "subagent",
|
|
351
|
-
description: descriptionOf(templateContent) ?? `${name} (${tier} tier)`,
|
|
352
|
-
model: models[tier],
|
|
353
|
-
}
|
|
354
|
-
}
|
|
292
|
+
const keepSet = new Set()
|
|
293
|
+
const variantsToWrite = []
|
|
294
|
+
for (const name of templates) {
|
|
295
|
+
const rawContent = await fs.readFile(path.join(agentsDir, `${name}.md`), "utf-8")
|
|
296
|
+
const content = normalizeTemplate(rawContent, name)
|
|
297
|
+
if (content !== rawContent) {
|
|
298
|
+
await writeIfChanged(path.join(agentsDir, `${name}.md`), content)
|
|
299
|
+
console.error(`[pc-subagent-tiers] Normalized ${name}.md (V2 subagent)`)
|
|
300
|
+
}
|
|
301
|
+
for (const tier of available) {
|
|
302
|
+
const file = `${name}.${tier}.md`
|
|
303
|
+
variantsToWrite.push({ file, path: path.join(agentsDir, file), content: buildVariant(content, models[tier]) })
|
|
304
|
+
keepSet.add(file)
|
|
355
305
|
}
|
|
306
|
+
}
|
|
356
307
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
)
|
|
308
|
+
await Promise.all(variantsToWrite.map(v => writeIfChanged(v.path, v.content)))
|
|
309
|
+
await Promise.all(
|
|
310
|
+
variantFiles.filter(f => !keepSet.has(f)).map(f => fs.unlink(path.join(agentsDir, f)).catch(() => {})),
|
|
311
|
+
)
|
|
360
312
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
} catch (err) {
|
|
367
|
-
console.error(`[pc-subagent-tiers] Error: ${err.message}`)
|
|
313
|
+
if (variantsToWrite.length > 0) {
|
|
314
|
+
console.error(`[pc-subagent-tiers] Created ${variantsToWrite.length} variant files (${templates.length} engineers x ${available.length} tiers)`)
|
|
315
|
+
} else if (templates.length > 0) {
|
|
316
|
+
console.error(`[pc-subagent-tiers] No variants created. Models: ${JSON.stringify(models)}`)
|
|
368
317
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
318
|
+
|
|
319
|
+
// Best effort: if V2 already discovered these agents (same startup), pin
|
|
320
|
+
// the primaries' model and plan's read-only permissions in memory too.
|
|
321
|
+
if (typeof ctx.agent?.transform === "function") {
|
|
322
|
+
await ctx.agent.transform(editor => {
|
|
323
|
+
for (const [id, spec] of Object.entries(PRIMARIES)) {
|
|
324
|
+
const existing = editor.get(id)
|
|
325
|
+
if (!existing) continue
|
|
326
|
+
const model = models[spec.tier]
|
|
327
|
+
if (model) existing.model = model
|
|
328
|
+
existing.mode = "primary"
|
|
329
|
+
existing.description = spec.description
|
|
330
|
+
if (spec.permissions) existing.permissions = spec.permissions
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
await ctx.agent.reload?.()
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error(`[pc-subagent-tiers] Error: ${error?.message}`)
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
})
|