@plainconceptsplatform/agent-harness 3.8.3 → 3.9.0-alpha.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/LICENSE +21 -21
- package/README.md +551 -551
- package/cli/commands/join.js +259 -259
- package/cli/commands/migrate.js +310 -310
- package/cli/commands/shared.js +27 -27
- package/cli/commands/single.js +79 -79
- package/cli/commands/update.js +121 -121
- package/cli/commands/wizard.js +134 -134
- package/cli/fragments/archive/az.md +97 -97
- package/cli/fragments/archive/gh.md +96 -96
- package/cli/fragments/archive/gl.md +96 -96
- package/cli/fragments/archive/none.md +75 -75
- package/cli/fragments/guardrails/codegraph.md +5 -5
- package/cli/fragments/guardrails/humanizer.md +4 -4
- package/cli/fragments/guardrails/memory.md +4 -4
- package/cli/fragments/guardrails/rtk.md +3 -3
- package/cli/fragments/guardrails/simple-english.md +4 -4
- package/cli/fragments/ops-backlog/az.md +28 -28
- package/cli/fragments/ops-backlog/gh.md +28 -28
- package/cli/fragments/ops-backlog/jira.md +28 -28
- package/cli/fragments/ops-review/az.md +62 -62
- package/cli/fragments/ops-review/gh.md +51 -51
- package/cli/fragments/ops-review/gl.md +55 -55
- package/cli/fragments/ops-ship/az.md +81 -81
- package/cli/fragments/ops-ship/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- package/cli/presets/agents-content.json +34 -34
- package/cli/presets/browser.json +10 -10
- package/cli/presets/clean.json +21 -21
- package/cli/presets/models.json +68 -68
- package/cli/presets/optimization.json +32 -32
- package/cli/presets/platforms.json +76 -76
- package/cli/presets/source.json +23 -23
- package/cli/steps/browser/index.js +52 -48
- package/cli/steps/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +176 -176
- package/cli/steps/copy/commands.js +79 -79
- package/cli/steps/copy/fullstack-engineer.js +81 -89
- package/cli/steps/copy/index.js +88 -88
- package/cli/steps/copy/opencode-json.js +324 -324
- package/cli/steps/copy/skills.js +316 -316
- package/cli/steps/metadata/index.js +115 -115
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- package/cli/steps/models/write.js +38 -38
- package/cli/steps/openspec/index.js +136 -136
- package/cli/steps/optimization/codegraph.js +141 -141
- package/cli/steps/optimization/detect.js +66 -66
- package/cli/steps/optimization/humanizer.js +17 -17
- package/cli/steps/optimization/index.js +158 -158
- package/cli/steps/optimization/memory.js +91 -91
- package/cli/steps/optimization/patch-guardrails.js +111 -111
- package/cli/steps/optimization/simple-english.js +17 -17
- package/cli/steps/optimization/skills-lock.js +30 -30
- package/cli/steps/platform/index.js +109 -109
- package/cli/steps/source/index.js +123 -123
- package/cli/utils/agent-color.js +111 -111
- package/cli/utils/copy.js +134 -134
- package/cli/utils/exec-spinner.js +47 -47
- package/cli/utils/exec.js +134 -134
- package/cli/utils/legacy-check.js +30 -30
- package/cli/utils/models-cache.js +58 -58
- package/cli/utils/models-pricing.js +42 -42
- package/cli/utils/paths.js +82 -82
- package/cli/utils/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/_gitignore +12 -10
- package/harness/.opencode/commands/init.md +5 -5
- package/harness/.opencode/commands/make-architecture.md +5 -5
- package/harness/.opencode/commands/make-design.md +5 -5
- package/harness/.opencode/commands/make-engineer.md +5 -5
- package/harness/.opencode/commands/make-guardrails.md +5 -5
- package/harness/.opencode/commands/make-user-model.md +5 -5
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +10 -10
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-apply.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +7 -7
- package/harness/.opencode/commands/plan-explore.md +11 -11
- package/harness/.opencode/commands/plan-goal.md +5 -5
- package/harness/.opencode/commands/plan-propose.md +11 -11
- package/harness/.opencode/commands/plan-quick.md +7 -7
- package/harness/.opencode/commands/plan-story.md +11 -11
- package/harness/.opencode/commands/repo-audit.md +7 -7
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/commands/repo-initialize.md +5 -5
- package/harness/.opencode/commands/repo-onboard.md +5 -5
- package/harness/.opencode/commands/repo-verify.md +5 -5
- package/harness/.opencode/package.json +6 -5
- package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -41
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-subagent-tiers-core.js +337 -0
- package/harness/.opencode/plugins/pc-subagent-tiers.js +56 -367
- package/harness/.opencode/plugins/pc-system-reminders.js +424 -424
- package/harness/.opencode/scripts/check-diff.mjs +305 -305
- package/harness/.opencode/scripts/code-rules.mjs +288 -288
- package/harness/.opencode/scripts/harness-doctor.mjs +52 -0
- package/harness/.opencode/scripts/harness-doctor.test.mjs +58 -0
- package/harness/.opencode/scripts/platform-contract.mjs +366 -366
- package/harness/.opencode/scripts/sync-agents.mjs +29 -0
- package/harness/AGENTS.md +55 -55
- package/harness/ARCHITECTURE.md +13 -13
- package/harness/DESIGN.md +13 -13
- package/harness/opencode.jsonc +36 -36
- package/harness/openspec/config.yaml +20 -20
- package/harness/skills-lock.json +17 -17
- package/package.json +14 -12
- package/skills/README.md +18 -18
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/browser-automation/SKILL.md +100 -72
- package/skills/pc-guardrails-generic/SKILL.md +53 -53
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/SKILL.md +33 -33
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-design/SKILL.md +45 -45
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/signal-mapping.md +53 -53
- package/skills/pc-make-engineer/template.md +36 -36
- package/skills/pc-make-guardrails/SKILL.md +44 -44
- package/skills/pc-make-guardrails/category-reference.md +112 -112
- package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
- package/skills/pc-make-user-model/SKILL.md +56 -56
- package/skills/pc-ops-ship/SKILL.md +22 -22
- package/skills/pc-plan-apply/SKILL.md +90 -90
- package/skills/pc-plan-apply/simple-mode.md +21 -21
- package/skills/pc-plan-archive/SKILL.md +66 -66
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/branching.md +39 -39
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-goal/output.md +71 -71
- package/skills/pc-plan-propose/SKILL.md +127 -127
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +46 -46
- package/skills/pc-plan-story/SKILL.md +93 -93
- package/skills/pc-repo-audit/SKILL.md +44 -44
- package/skills/pc-repo-help/SKILL.md +87 -87
- package/skills/pc-repo-initialize/SKILL.md +119 -119
- package/skills/pc-repo-onboard/SKILL.md +32 -32
- package/skills/pc-repo-verify/SKILL.md +150 -150
- package/skills/pc-userstory-az/SKILL.md +71 -71
- package/skills/pc-userstory-browser/SKILL.md +73 -64
- package/skills/pc-userstory-gh/SKILL.md +63 -63
- package/skills/pc-userstory-jira/SKILL.md +74 -74
|
@@ -1,367 +1,56 @@
|
|
|
1
|
-
// pc-subagent-tiers: on startup,
|
|
2
|
-
//
|
|
3
|
-
// the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
mode: "all",
|
|
58
|
-
color: "info",
|
|
59
|
-
description: "Write plans, proposals, stories, reviews and archives on the plan model. Edits the tree, never spawns workers.",
|
|
60
|
-
permissions: [{ action: "subagent", resource: "*", effect: "deny" }],
|
|
61
|
-
},
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// opencode's theme keywords. Treated as unset so a name-derived hex wins: the
|
|
65
|
-
// old /make-engineer template asked whoever ran it to pick one of these and
|
|
66
|
-
// avoid collisions by hand, which does not survive more than a few agents.
|
|
67
|
-
const THEME_COLORS = new Set(["primary", "secondary", "accent", "success", "warning", "error", "info"])
|
|
68
|
-
|
|
69
|
-
// Fields V1 agent frontmatter used that V2 forbids. Stripped on sight so a repo
|
|
70
|
-
// upgraded in place does not carry a definition V2 refuses to load.
|
|
71
|
-
const LEGACY_FIELDS = [
|
|
72
|
-
"model",
|
|
73
|
-
"permission",
|
|
74
|
-
"prompt",
|
|
75
|
-
"disable",
|
|
76
|
-
"disabled",
|
|
77
|
-
"temperature",
|
|
78
|
-
"top_p",
|
|
79
|
-
"maxSteps",
|
|
80
|
-
"tools",
|
|
81
|
-
]
|
|
82
|
-
|
|
83
|
-
// Mirrors cli/utils/agent-color.js in the CLI, duplicated because a plugin has
|
|
84
|
-
// to stand alone in the consumer repo. agent-color.test.js asserts the two stay
|
|
85
|
-
// in step; change one and change the other.
|
|
86
|
-
//
|
|
87
|
-
// Only the hue is hashed. Fixed saturation and lightness are what keep every
|
|
88
|
-
// result usable: hashing those too would eventually produce a near-grey,
|
|
89
|
-
// near-black or near-white agent, invisible against one theme or the other.
|
|
90
|
-
function agentColor(name) {
|
|
91
|
-
let h = 0x811c9dc5
|
|
92
|
-
for (let i = 0; i < name.length; i++) {
|
|
93
|
-
h ^= name.charCodeAt(i)
|
|
94
|
-
h = Math.imul(h, 0x01000193) >>> 0
|
|
95
|
-
}
|
|
96
|
-
const hue = (h >>> 0) % 360
|
|
97
|
-
const sat = 0.68
|
|
98
|
-
const light = 0.52
|
|
99
|
-
const c = (1 - Math.abs(2 * light - 1)) * sat
|
|
100
|
-
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1))
|
|
101
|
-
const m = light - c / 2
|
|
102
|
-
const [r, g, b] =
|
|
103
|
-
hue < 60 ? [c, x, 0] :
|
|
104
|
-
hue < 120 ? [x, c, 0] :
|
|
105
|
-
hue < 180 ? [0, c, x] :
|
|
106
|
-
hue < 240 ? [0, x, c] :
|
|
107
|
-
hue < 300 ? [x, 0, c] :
|
|
108
|
-
[c, 0, x]
|
|
109
|
-
const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0")
|
|
110
|
-
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** A theme keyword or a missing value yields to the derived hex; a hex stays. */
|
|
114
|
-
function shouldDeriveColor(current) {
|
|
115
|
-
if (!current) return true
|
|
116
|
-
return THEME_COLORS.has(current.trim().toLowerCase())
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Render a colour for YAML frontmatter.
|
|
121
|
-
*
|
|
122
|
-
* A hex has to be quoted. `#` opens a comment in YAML, so `color: #D83155`
|
|
123
|
-
* parses as null and opencode refuses the agent with "Invalid input color".
|
|
124
|
-
* Theme keywords need no quoting and stay bare.
|
|
125
|
-
*/
|
|
126
|
-
function yamlColor(value) {
|
|
127
|
-
return value.startsWith("#") ? `"${value}"` : value
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function unquoteColor(raw) {
|
|
131
|
-
return (raw ?? "").trim().replace(/^["']/, "").replace(/["']$/, "")
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function splitFrontmatter(content) {
|
|
135
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
136
|
-
if (!match) return { frontmatter: null, body: content }
|
|
137
|
-
return { frontmatter: match[1], body: content.slice(match[0].length) }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function stripLegacyFields(frontmatter) {
|
|
141
|
-
const lines = frontmatter.split(/\r?\n/)
|
|
142
|
-
const kept = []
|
|
143
|
-
for (let i = 0; i < lines.length; i++) {
|
|
144
|
-
const key = lines[i].match(/^([A-Za-z0-9_-]+):/)?.[1]
|
|
145
|
-
if (key && LEGACY_FIELDS.includes(key)) {
|
|
146
|
-
// Drop the key and any indented continuation lines that belong to it.
|
|
147
|
-
i++
|
|
148
|
-
while (i < lines.length && /^\s+\S/.test(lines[i])) i++
|
|
149
|
-
i--
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
kept.push(lines[i])
|
|
153
|
-
}
|
|
154
|
-
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim()
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function setScalar(frontmatter, key, value) {
|
|
158
|
-
const line = `${key}: ${value}`
|
|
159
|
-
// String operations rather than a built RegExp: `key` is one of this module's
|
|
160
|
-
// own constants, so the pattern would be safe, but a dynamic regexp here is a
|
|
161
|
-
// ReDoS-shaped pattern that static analysis rightly refuses to clear.
|
|
162
|
-
const lines = frontmatter.split(/\r?\n/)
|
|
163
|
-
const index = lines.findIndex(current => current.startsWith(`${key}:`))
|
|
164
|
-
if (index === -1) {
|
|
165
|
-
return frontmatter ? `${line}\n${frontmatter}` : line
|
|
166
|
-
}
|
|
167
|
-
lines[index] = line
|
|
168
|
-
return lines.join("\n")
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function permissionsYaml(permissions) {
|
|
172
|
-
return [
|
|
173
|
-
"permissions:",
|
|
174
|
-
...permissions.flatMap(p => [
|
|
175
|
-
` - action: ${p.action}`,
|
|
176
|
-
` resource: ${p.resource}`,
|
|
177
|
-
` effect: ${p.effect}`,
|
|
178
|
-
]),
|
|
179
|
-
].join("\n")
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** The colour line an agent should have, or null when the current one is right. */
|
|
183
|
-
function colorLineFor(name, rawCurrent) {
|
|
184
|
-
const current = unquoteColor(rawCurrent)
|
|
185
|
-
const target = shouldDeriveColor(current) ? agentColor(name) : current
|
|
186
|
-
const rendered = yamlColor(target)
|
|
187
|
-
return (rawCurrent ?? "").trim() === rendered ? null : `color: ${rendered}`
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const FULLSTACK_NAME = "fullstack-engineer"
|
|
191
|
-
const FULLSTACK_TEMPLATE = `${FULLSTACK_NAME}.md`
|
|
192
|
-
|
|
193
|
-
/** Rewrite a template's frontmatter to a clean V2 subagent definition. */
|
|
194
|
-
function normalizeTemplate(content, name) {
|
|
195
|
-
const { frontmatter, body } = splitFrontmatter(content)
|
|
196
|
-
if (frontmatter === null) return `---\nmode: subagent\ncolor: ${yamlColor(agentColor(name))}\n---\n\n${content}`
|
|
197
|
-
|
|
198
|
-
let fm = stripLegacyFields(frontmatter)
|
|
199
|
-
fm = setScalar(fm, "mode", "subagent")
|
|
200
|
-
const colorLine = colorLineFor(name, fm.match(/^color:\s*(.+)$/m)?.[1])
|
|
201
|
-
if (colorLine) fm = setScalar(fm, "color", colorLine.slice("color: ".length))
|
|
202
|
-
return `---\n${fm}\n---\n${body}`
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** A tier-pinned copy of a specialist: same body, resolved model. */
|
|
206
|
-
function buildVariant(templateContent, model) {
|
|
207
|
-
const { frontmatter, body } = splitFrontmatter(templateContent)
|
|
208
|
-
let fm = stripLegacyFields(frontmatter ?? "")
|
|
209
|
-
fm = setScalar(fm, "mode", "subagent")
|
|
210
|
-
fm = setScalar(fm, "model", model)
|
|
211
|
-
// Hidden by default: a tier is chosen by whoever writes the task, not picked
|
|
212
|
-
// from the @ menu. Without this the menu lists every specialist once per tier.
|
|
213
|
-
fm = setScalar(fm, "hidden", "true")
|
|
214
|
-
if (!/^color:/m.test(fm)) {
|
|
215
|
-
const name = ""
|
|
216
|
-
fm = setScalar(fm, "color", name ? yamlColor(agentColor(name)) : yamlColor(agentColor("engineer")))
|
|
217
|
-
}
|
|
218
|
-
return `---\n${fm}\n---\n${body}`
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Build a primary from the fullstack body: same identity and the same
|
|
223
|
-
* ## Abilities block, with our own frontmatter on top. The body is taken
|
|
224
|
-
* verbatim so /make-engineer only has to maintain fullstack-engineer.md and
|
|
225
|
-
* both primaries inherit whatever it lists.
|
|
226
|
-
*/
|
|
227
|
-
function buildPrimary(spec, fullstackContent, model) {
|
|
228
|
-
const { body } = splitFrontmatter(fullstackContent)
|
|
229
|
-
const lines = [
|
|
230
|
-
"---",
|
|
231
|
-
`description: ${spec.description}`,
|
|
232
|
-
`mode: ${spec.mode ?? "primary"}`,
|
|
233
|
-
]
|
|
234
|
-
if (model) lines.push(`model: ${model}`)
|
|
235
|
-
lines.push(`color: ${yamlColor(spec.color)}`)
|
|
236
|
-
if (spec.permissions) lines.push(permissionsYaml(spec.permissions))
|
|
237
|
-
lines.push("---")
|
|
238
|
-
return `${lines.join("\n")}\n\n${body.trim()}\n`
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function readJson(filePath) {
|
|
242
|
-
try {
|
|
243
|
-
return JSON.parse(await fs.readFile(filePath, "utf-8"))
|
|
244
|
-
} catch {
|
|
245
|
-
return null
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function scanEngineers(agentsDir) {
|
|
250
|
-
try {
|
|
251
|
-
const entries = await fs.readdir(agentsDir)
|
|
252
|
-
return {
|
|
253
|
-
templates: entries
|
|
254
|
-
.filter(f => /^[\w-]+-engineer\.md$/.test(f) && f !== FULLSTACK_TEMPLATE)
|
|
255
|
-
.map(f => f.replace(/\.md$/, "")),
|
|
256
|
-
variantFiles: entries.filter(f => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
|
|
257
|
-
hasFullstack: entries.includes(FULLSTACK_TEMPLATE),
|
|
258
|
-
}
|
|
259
|
-
} catch {
|
|
260
|
-
return { templates: [], variantFiles: [], hasFullstack: false }
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function writeIfChanged(filePath, content) {
|
|
265
|
-
try {
|
|
266
|
-
if ((await fs.readFile(filePath, "utf-8")) === content) return
|
|
267
|
-
} catch {
|
|
268
|
-
// file missing, write it
|
|
269
|
-
}
|
|
270
|
-
const tmpPath = `${filePath}.tmp`
|
|
271
|
-
await fs.writeFile(tmpPath, content, "utf-8")
|
|
272
|
-
await fs.rename(tmpPath, filePath)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export default Plugin.define({
|
|
276
|
-
id: "pc.subagent-tiers",
|
|
277
|
-
async setup(ctx) {
|
|
278
|
-
const root = ctx.location.directory
|
|
279
|
-
const agentsDir = path.join(root, ".opencode", "agents")
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
const [user, team] = await Promise.all([
|
|
283
|
-
readJson(path.join(root, ".opencode", "harness.user.json")),
|
|
284
|
-
readJson(path.join(root, ".opencode", "harness.json")),
|
|
285
|
-
])
|
|
286
|
-
const models = {}
|
|
287
|
-
for (const tier of TIERS) {
|
|
288
|
-
models[tier] = user?.models?.[tier] ?? team?.models?.[tier] ?? null
|
|
289
|
-
}
|
|
290
|
-
const available = TIERS.filter(t => models[t])
|
|
291
|
-
|
|
292
|
-
const { templates, variantFiles, hasFullstack } = await scanEngineers(agentsDir)
|
|
293
|
-
|
|
294
|
-
if (hasFullstack) {
|
|
295
|
-
const fullstackPath = path.join(agentsDir, FULLSTACK_TEMPLATE)
|
|
296
|
-
const rawFullstack = await fs.readFile(fullstackPath, "utf-8")
|
|
297
|
-
const fullstack = normalizeTemplate(rawFullstack, FULLSTACK_NAME)
|
|
298
|
-
if (fullstack !== rawFullstack) {
|
|
299
|
-
await writeIfChanged(fullstackPath, fullstack)
|
|
300
|
-
console.error("[pc-subagent-tiers] Normalized fullstack-engineer.md (V2 subagent)")
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
for (const [name, spec] of Object.entries(PRIMARIES)) {
|
|
304
|
-
await writeIfChanged(
|
|
305
|
-
path.join(agentsDir, `${name}.md`),
|
|
306
|
-
buildPrimary(spec, fullstack, models[spec.tier]),
|
|
307
|
-
)
|
|
308
|
-
}
|
|
309
|
-
console.error(`[pc-subagent-tiers] Wrote primaries: build (${models.build ?? "session model"}), plan and planner (${models.plan ?? "session model"})`)
|
|
310
|
-
} else {
|
|
311
|
-
console.error(`[pc-subagent-tiers] ${FULLSTACK_TEMPLATE} missing: build and plan not generated`)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const keepSet = new Set()
|
|
315
|
-
const variantsToWrite = []
|
|
316
|
-
for (const name of templates) {
|
|
317
|
-
const rawContent = await fs.readFile(path.join(agentsDir, `${name}.md`), "utf-8")
|
|
318
|
-
const content = normalizeTemplate(rawContent, name)
|
|
319
|
-
if (content !== rawContent) {
|
|
320
|
-
await writeIfChanged(path.join(agentsDir, `${name}.md`), content)
|
|
321
|
-
console.error(`[pc-subagent-tiers] Normalized ${name}.md (V2 subagent)`)
|
|
322
|
-
}
|
|
323
|
-
for (const tier of available) {
|
|
324
|
-
const file = `${name}.${tier}.md`
|
|
325
|
-
variantsToWrite.push({ file, path: path.join(agentsDir, file), content: buildVariant(content, models[tier]) })
|
|
326
|
-
keepSet.add(file)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
await Promise.all(variantsToWrite.map(v => writeIfChanged(v.path, v.content)))
|
|
331
|
-
await Promise.all(
|
|
332
|
-
variantFiles.filter(f => !keepSet.has(f)).map(f => fs.unlink(path.join(agentsDir, f)).catch(() => {})),
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
if (variantsToWrite.length > 0) {
|
|
336
|
-
console.error(`[pc-subagent-tiers] Created ${variantsToWrite.length} variant files (${templates.length} engineers x ${available.length} tiers)`)
|
|
337
|
-
} else if (templates.length > 0) {
|
|
338
|
-
console.error(`[pc-subagent-tiers] No variants created. Models: ${JSON.stringify(models)}`)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Best effort: if V2 already discovered these agents (same startup), pin
|
|
342
|
-
// the primaries' model and plan's read-only permissions in memory too.
|
|
343
|
-
if (typeof ctx.agent?.transform === "function") {
|
|
344
|
-
await ctx.agent.transform(editor => {
|
|
345
|
-
for (const [id, spec] of Object.entries(PRIMARIES)) {
|
|
346
|
-
const existing = editor.get(id)
|
|
347
|
-
if (!existing) continue
|
|
348
|
-
const model = models[spec.tier]
|
|
349
|
-
if (model) {
|
|
350
|
-
try {
|
|
351
|
-
existing.model = Model.Ref.parse(model)
|
|
352
|
-
} catch {
|
|
353
|
-
// invalid model ref; leave undefined so session model applies
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
existing.mode = spec.mode ?? "primary"
|
|
357
|
-
existing.description = spec.description
|
|
358
|
-
if (spec.permissions) existing.permissions = spec.permissions
|
|
359
|
-
}
|
|
360
|
-
})
|
|
361
|
-
await ctx.agent.reload?.()
|
|
362
|
-
}
|
|
363
|
-
} catch (error) {
|
|
364
|
-
console.error(`[pc-subagent-tiers] Error: ${error?.message}`)
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
})
|
|
1
|
+
// pc-subagent-tiers: on startup, generates the primary agents, the spawnable
|
|
2
|
+
// planner alias and the tier variants from *-engineer.md templates, then pins
|
|
3
|
+
// the primaries in memory. The generation itself lives in
|
|
4
|
+
// pc-subagent-tiers-core.js so a CI pre-start step can write the same files
|
|
5
|
+
// before OpenCode builds its task enum.
|
|
6
|
+
|
|
7
|
+
import { Plugin, Model } from "@opencode/plugin";
|
|
8
|
+
import { PRIMARIES, generateAgents } from "./pc-subagent-tiers-core.js";
|
|
9
|
+
|
|
10
|
+
export default Plugin.define({
|
|
11
|
+
id: "pc.subagent-tiers",
|
|
12
|
+
async setup(ctx) {
|
|
13
|
+
const root = ctx.location.directory;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = await generateAgents({ root });
|
|
17
|
+
|
|
18
|
+
console.error(
|
|
19
|
+
`[pc-subagent-tiers] Wrote primaries: build (${result.models.build ?? "session model"}), plan and planner (${result.models.plan ?? "session model"})`,
|
|
20
|
+
);
|
|
21
|
+
if (result.variants.length > 0) {
|
|
22
|
+
console.error(
|
|
23
|
+
`[pc-subagent-tiers] Created ${result.variants.length} variant files (${result.templates.length} engineers x ${Object.values(result.models).filter(Boolean).length} tiers)`,
|
|
24
|
+
);
|
|
25
|
+
} else if (result.templates.length > 0) {
|
|
26
|
+
console.error(`[pc-subagent-tiers] No variants created. Models: ${JSON.stringify(result.models)}`);
|
|
27
|
+
}
|
|
28
|
+
for (const warning of result.warnings) {
|
|
29
|
+
console.error(`[pc-subagent-tiers] Warning: ${warning}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof ctx.agent?.transform === "function") {
|
|
33
|
+
await ctx.agent.transform((editor) => {
|
|
34
|
+
for (const [id, spec] of Object.entries(PRIMARIES)) {
|
|
35
|
+
const existing = editor.get(id);
|
|
36
|
+
if (!existing) continue;
|
|
37
|
+
const model = result.models[spec.tier];
|
|
38
|
+
if (model) {
|
|
39
|
+
try {
|
|
40
|
+
existing.model = Model.Ref.parse(model);
|
|
41
|
+
} catch {
|
|
42
|
+
// invalid model ref; leave undefined so the session model applies
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
existing.mode = spec.mode ?? "primary";
|
|
46
|
+
existing.description = spec.description;
|
|
47
|
+
if (spec.permissions) existing.permissions = spec.permissions;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
await ctx.agent.reload?.();
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(`[pc-subagent-tiers] Error: ${error?.message}`);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|