@plainconceptsplatform/agent-harness 2.0.0 → 2.1.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.
Files changed (117) hide show
  1. package/README.md +426 -419
  2. package/package.json +3 -3
  3. package/src/commands/join.js +244 -244
  4. package/src/commands/shared.js +27 -27
  5. package/src/commands/single.js +79 -79
  6. package/src/commands/update.js +109 -109
  7. package/src/commands/wizard.js +134 -134
  8. package/src/content/.agents/skills/browser-automation/SKILL.md +66 -66
  9. package/src/content/.agents/skills/pc-guardrails-generic/SKILL.md +68 -68
  10. package/src/content/.agents/skills/pc-guardrails-project/SKILL.md +8 -8
  11. package/src/content/.agents/skills/pc-make-architecture/SKILL.md +51 -51
  12. package/src/content/.agents/skills/pc-make-architecture/structure-template.md +38 -38
  13. package/src/content/.agents/skills/pc-make-design/SKILL.md +68 -68
  14. package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -219
  15. package/src/content/.agents/skills/pc-make-engineer/signal-mapping.md +68 -68
  16. package/src/content/.agents/skills/pc-make-engineer/template.md +81 -81
  17. package/src/content/.agents/skills/pc-make-evidence-scaffold/SKILL.md +18 -18
  18. package/src/content/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +29 -29
  19. package/src/content/.agents/skills/pc-make-guardrails/SKILL.md +74 -74
  20. package/src/content/.agents/skills/pc-make-guardrails/category-reference.md +68 -68
  21. package/src/content/.agents/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  22. package/src/content/.agents/skills/pc-make-merge-risk-assess/category-reference.md +98 -98
  23. package/src/content/.agents/skills/pc-make-user-model/SKILL.md +66 -66
  24. package/src/content/.agents/skills/pc-ops-evidence/SKILL.md +127 -127
  25. package/src/content/.agents/skills/pc-ops-ship/SKILL.md +18 -18
  26. package/src/content/.agents/skills/pc-plan-apply/SKILL.md +83 -83
  27. package/src/content/.agents/skills/pc-plan-apply/simple-mode.md +21 -21
  28. package/src/content/.agents/skills/pc-plan-archive/SKILL.md +63 -63
  29. package/src/content/.agents/skills/pc-plan-explore/SKILL.md +9 -9
  30. package/src/content/.agents/skills/pc-plan-goal/SKILL.md +94 -94
  31. package/src/content/.agents/skills/pc-plan-goal/branching.md +30 -30
  32. package/src/content/.agents/skills/pc-plan-goal/failure-policy.md +30 -30
  33. package/src/content/.agents/skills/pc-plan-goal/output-mode.md +9 -9
  34. package/src/content/.agents/skills/pc-plan-goal/output.md +68 -68
  35. package/src/content/.agents/skills/pc-plan-propose/SKILL.md +125 -125
  36. package/src/content/.agents/skills/pc-plan-propose/task-annotation.md +39 -39
  37. package/src/content/.agents/skills/pc-plan-quick/SKILL.md +62 -62
  38. package/src/content/.agents/skills/pc-plan-story/SKILL.md +146 -146
  39. package/src/content/.agents/skills/pc-repo-audit/SKILL.md +44 -44
  40. package/src/content/.agents/skills/pc-repo-help/SKILL.md +91 -91
  41. package/src/content/.agents/skills/pc-repo-initialize/SKILL.md +130 -130
  42. package/src/content/.agents/skills/pc-repo-onboard/SKILL.md +87 -87
  43. package/src/content/.agents/skills/pc-repo-verify/SKILL.md +34 -34
  44. package/src/content/.agents/skills/pc-userstory-az/SKILL.md +157 -157
  45. package/src/content/.agents/skills/pc-userstory-browser/SKILL.md +132 -132
  46. package/src/content/.agents/skills/pc-userstory-gh/SKILL.md +120 -120
  47. package/src/content/.agents/skills/pc-userstory-jira/SKILL.md +131 -131
  48. package/src/content/.opencode/_gitignore +9 -7
  49. package/src/content/.opencode/commands/init.md +5 -5
  50. package/src/content/.opencode/commands/make-architecture.md +5 -5
  51. package/src/content/.opencode/commands/make-design.md +5 -5
  52. package/src/content/.opencode/commands/make-engineer.md +5 -5
  53. package/src/content/.opencode/commands/make-evidence-scaffold.md +5 -5
  54. package/src/content/.opencode/commands/make-guardrails.md +5 -5
  55. package/src/content/.opencode/commands/make-user-model.md +5 -5
  56. package/src/content/.opencode/commands/ops-backlog.md +10 -10
  57. package/src/content/.opencode/commands/ops-evidence.md +9 -9
  58. package/src/content/.opencode/commands/ops-review.md +8 -8
  59. package/src/content/.opencode/commands/ops-ship.md +9 -9
  60. package/src/content/.opencode/commands/plan-apply.md +9 -9
  61. package/src/content/.opencode/commands/plan-archive.md +5 -5
  62. package/src/content/.opencode/commands/plan-explore.md +9 -9
  63. package/src/content/.opencode/commands/plan-goal.md +5 -5
  64. package/src/content/.opencode/commands/plan-propose.md +9 -9
  65. package/src/content/.opencode/commands/plan-quick.md +5 -5
  66. package/src/content/.opencode/commands/plan-story.md +9 -9
  67. package/src/content/.opencode/commands/repo-audit.md +5 -5
  68. package/src/content/.opencode/commands/repo-help.md +5 -5
  69. package/src/content/.opencode/commands/repo-initialize.md +5 -5
  70. package/src/content/.opencode/commands/repo-onboard.md +5 -5
  71. package/src/content/.opencode/commands/repo-verify.md +5 -5
  72. package/src/content/.opencode/plugins/pc-subagent-monitor.js +139 -139
  73. package/src/content/.opencode/plugins/pc-subagent-tiers.js +281 -179
  74. package/src/content/.opencode/plugins/pc-system-reminders.js +96 -96
  75. package/src/content/.opencode/tui/pc-subagents.tsx +98 -98
  76. package/src/content/.opencode/tui.json +6 -6
  77. package/src/content/AGENTS.md +71 -71
  78. package/src/content/opencode.jsonc +39 -31
  79. package/src/fragments/archive/az.md +95 -95
  80. package/src/fragments/archive/gh.md +94 -94
  81. package/src/fragments/archive/gl.md +94 -94
  82. package/src/fragments/archive/none.md +73 -73
  83. package/src/fragments/guardrails/codegraph.md +7 -7
  84. package/src/fragments/guardrails/humanizer.md +4 -4
  85. package/src/fragments/guardrails/memory.md +4 -4
  86. package/src/fragments/guardrails/rtk.md +3 -3
  87. package/src/fragments/guardrails/simple-english.md +4 -4
  88. package/src/fragments/ops-backlog/az.md +28 -28
  89. package/src/fragments/ops-backlog/gh.md +29 -29
  90. package/src/fragments/ops-backlog/jira.md +28 -28
  91. package/src/fragments/ops-evidence/az.md +41 -41
  92. package/src/fragments/ops-evidence/gh.md +53 -53
  93. package/src/fragments/ops-evidence/jira.md +38 -38
  94. package/src/fragments/ops-review/az.md +62 -62
  95. package/src/fragments/ops-review/gh.md +52 -52
  96. package/src/fragments/ops-review/gl.md +56 -56
  97. package/src/fragments/ops-ship/az.md +80 -80
  98. package/src/fragments/ops-ship/gh.md +68 -68
  99. package/src/fragments/ops-ship/gl.md +85 -85
  100. package/src/index.js +107 -107
  101. package/src/presets/agents-content.json +53 -53
  102. package/src/presets/models.json +68 -68
  103. package/src/steps/copy/agents.js +118 -118
  104. package/src/steps/copy/commands.js +91 -91
  105. package/src/steps/copy/fullstack-engineer.js +85 -83
  106. package/src/steps/copy/index.js +88 -88
  107. package/src/steps/copy/opencode-json.js +147 -129
  108. package/src/steps/copy/skills.js +196 -196
  109. package/src/steps/metadata/index.js +108 -108
  110. package/src/steps/models/write.js +34 -34
  111. package/src/steps/optimization/patch-guardrails.js +108 -108
  112. package/src/utils/copy.js +108 -108
  113. package/src/utils/legacy-check.js +30 -30
  114. package/src/utils/models-cache.js +58 -58
  115. package/src/utils/paths.js +67 -64
  116. package/src/utils/update-manifest.js +49 -49
  117. package/src/content/.opencode/plugins/pc-system-reminders.test.js +0 -35
@@ -1,179 +1,281 @@
1
- // pc-subagent-tiers: on startup, reads *-engineer.md templates and creates
2
- // tier variant files with the resolved model. Variants are gitignored and
3
- // regenerated every startup. Model resolution: user override > team config.
4
-
5
- import fs from "node:fs/promises"
6
- import path from "node:path"
7
-
8
- const TIERS = ["build", "fast", "plan"]
9
-
10
- export const PcSubagentTiers = async ({ directory }) => {
11
- const root = directory || process.cwd()
12
- const agentsDir = path.join(root, ".opencode", "agents")
13
-
14
- async function readJson(filePath) {
15
- try {
16
- return JSON.parse(await fs.readFile(filePath, "utf-8"))
17
- } catch {
18
- return null
19
- }
20
- }
21
-
22
- async function resolveModels() {
23
- const [user, team] = await Promise.all([
24
- readJson(path.join(root, ".opencode", "harness.user.json")),
25
- readJson(path.join(root, ".opencode", "harness.json")),
26
- ])
27
- const models = {}
28
- for (const tier of TIERS) {
29
- models[tier] = user?.models?.[tier] ?? team?.models?.[tier] ?? null
30
- }
31
- return models
32
- }
33
-
34
- async function scanEngineers() {
35
- try {
36
- const entries = await fs.readdir(agentsDir)
37
- return {
38
- templates: entries.filter(f => /^[\w-]+-engineer\.md$/.test(f) && f !== 'fullstack-engineer.md').map(f => f.replace(/\.md$/, "")),
39
- variantFiles: entries.filter(f => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
40
- }
41
- } catch {
42
- return { templates: [], variantFiles: [] }
43
- }
44
- }
45
-
46
- function variantFile(name, tier) {
47
- return `${name}.${tier}.md`
48
- }
49
-
50
- function buildVariant(templateContent, model) {
51
- const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
52
- const modelLine = `model: ${model}`
53
- if (!fmMatch) return `---\nmode: subagent\n${modelLine}\n---\n\n${templateContent}`
54
-
55
- let fm = fmMatch[1]
56
- fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: subagent') : `mode: subagent\n${fm}`
57
- fm = /^model:/m.test(fm) ? fm.replace(/^model:.*$/m, modelLine) : `${modelLine}\n${fm}`
58
-
59
- // Slice at frontmatter end: never String.replace with content-derived
60
- // strings, it matches the wrong occurrence and expands $& sequences.
61
- return `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}`
62
- }
63
-
64
- // Ensure template files have mode: primary and no stale model: line. Base
65
- // engineer templates must never declare a model: models are resolved at
66
- // startup per-tier and injected into variant files only. A stale model:
67
- // (e.g. from a prior `stampAgentModels` run) is stripped here so the base
68
- // agent falls back to the session-level model in opencode.jsonc.
69
- function normalizeTemplate(templateContent) {
70
- const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
71
- if (!fmMatch) return templateContent
72
-
73
- let fm = fmMatch[1]
74
- let changed = false
75
-
76
- if (!/^mode:\s*primary/m.test(fm)) {
77
- fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: primary') : `mode: primary\n${fm}`
78
- changed = true
79
- }
80
- if (/^model:/m.test(fm)) {
81
- // Remove the model: line and its trailing newline without leaving a
82
- // leading blank line (model: can be the first frontmatter entry).
83
- fm = fm.replace(/^model:[^\n]*\r?\n?/m, '').replace(/^\r?\n/, '').replace(/\n$/, '')
84
- changed = true
85
- }
86
-
87
- return changed ? `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}` : templateContent
88
- }
89
-
90
- function descriptionOf(templateContent) {
91
- return templateContent.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? null
92
- }
93
-
94
- // Skip writing if the file already has identical content.
95
- async function writeIfChanged(filePath, content) {
96
- try {
97
- if ((await fs.readFile(filePath, "utf-8")) === content) return
98
- } catch {
99
- // file missing, write it
100
- }
101
- const tmpPath = `${filePath}.tmp`
102
- await fs.writeFile(tmpPath, content, "utf-8")
103
- await fs.rename(tmpPath, filePath)
104
- }
105
-
106
- return {
107
- config: async (cfg) => {
108
- try {
109
- const models = await resolveModels()
110
- const available = TIERS.filter(t => models[t])
111
- const { templates, variantFiles } = await scanEngineers()
112
-
113
- const templateContents = await Promise.all(
114
- templates.map(async name => {
115
- const rawContent = await fs.readFile(path.join(agentsDir, `${name}.md`), "utf-8")
116
- const content = normalizeTemplate(rawContent)
117
- // If the template had the wrong mode or a stale model:, persist the fix to disk
118
- if (content !== rawContent) {
119
- await writeIfChanged(path.join(agentsDir, `${name}.md`), content)
120
- console.error(`[pc-subagent-tiers] Normalized ${name}.md (mode: primary, model: removed)`)
121
- }
122
- return { name, content }
123
- })
124
- )
125
-
126
- const keepSet = new Set()
127
- const variantsToWrite = []
128
-
129
- for (const { name, content } of templateContents) {
130
- for (const tier of available) {
131
- const file = variantFile(name, tier)
132
- variantsToWrite.push({
133
- file,
134
- path: path.join(agentsDir, file),
135
- content: buildVariant(content, models[tier]),
136
- name,
137
- tier,
138
- templateContent: content,
139
- })
140
- keepSet.add(file)
141
- }
142
- }
143
-
144
- await Promise.all(variantsToWrite.map(v => writeIfChanged(v.path, v.content)))
145
-
146
- if (cfg?.agent) {
147
- // Ensure base templates are always mode: primary in-memory
148
- for (const { name } of templateContents) {
149
- if (cfg.agent[name]) {
150
- cfg.agent[name].mode = 'primary'
151
- }
152
- }
153
- for (const { name, tier, templateContent } of variantsToWrite) {
154
- const base = cfg.agent[name]
155
- cfg.agent[`${name}.${tier}`] = base
156
- ? { ...base, mode: 'subagent', model: models[tier] }
157
- : {
158
- mode: "subagent",
159
- description: descriptionOf(templateContent) ?? `${name} (${tier} tier)`,
160
- model: models[tier],
161
- }
162
- }
163
- }
164
-
165
- await Promise.all(
166
- variantFiles.filter(f => !keepSet.has(f)).map(f => fs.unlink(path.join(agentsDir, f)).catch(() => {}))
167
- )
168
-
169
- if (variantsToWrite.length > 0) {
170
- console.error(`[pc-subagent-tiers] Created ${variantsToWrite.length} variant files (${templates.length} engineers x ${available.length} tiers)`)
171
- } else if (templates.length > 0) {
172
- console.error(`[pc-subagent-tiers] No variants created. Models: ${JSON.stringify(models)}`)
173
- }
174
- } catch (err) {
175
- console.error(`[pc-subagent-tiers] Error: ${err.message}`)
176
- }
177
- },
178
- }
179
- }
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
+ const PRIMARIES = {
28
+ build: {
29
+ tier: "build",
30
+ description: "Implement changes in this repository. Full write access, spawns specialist engineers for parallel work.",
31
+ permission: null,
32
+ },
33
+ plan: {
34
+ tier: "plan",
35
+ description: "Explore and plan without touching the tree. Read-only: proposes work for build to carry out.",
36
+ permission: { edit: "deny" },
37
+ },
38
+ }
39
+
40
+ const FULLSTACK_NAME = "fullstack-engineer"
41
+ const FULLSTACK_TEMPLATE = `${FULLSTACK_NAME}.md`
42
+
43
+ export const PcSubagentTiers = async ({ directory }) => {
44
+ const root = directory || process.cwd()
45
+ const agentsDir = path.join(root, ".opencode", "agents")
46
+
47
+ async function readJson(filePath) {
48
+ try {
49
+ return JSON.parse(await fs.readFile(filePath, "utf-8"))
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+
55
+ async function resolveModels() {
56
+ const [user, team] = await Promise.all([
57
+ readJson(path.join(root, ".opencode", "harness.user.json")),
58
+ readJson(path.join(root, ".opencode", "harness.json")),
59
+ ])
60
+ const models = {}
61
+ for (const tier of TIERS) {
62
+ models[tier] = user?.models?.[tier] ?? team?.models?.[tier] ?? null
63
+ }
64
+ return models
65
+ }
66
+
67
+ async function scanEngineers() {
68
+ try {
69
+ const entries = await fs.readdir(agentsDir)
70
+ return {
71
+ templates: entries.filter(f => /^[\w-]+-engineer\.md$/.test(f) && f !== FULLSTACK_TEMPLATE).map(f => f.replace(/\.md$/, "")),
72
+ variantFiles: entries.filter(f => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
73
+ hasFullstack: entries.includes(FULLSTACK_TEMPLATE),
74
+ }
75
+ } catch {
76
+ return { templates: [], variantFiles: [], hasFullstack: false }
77
+ }
78
+ }
79
+
80
+ function variantFile(name, tier) {
81
+ return `${name}.${tier}.md`
82
+ }
83
+
84
+ function buildVariant(templateContent, model) {
85
+ const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
86
+ const modelLine = `model: ${model}`
87
+ if (!fmMatch) return `---\nmode: subagent\n${modelLine}\n---\n\n${templateContent}`
88
+
89
+ let fm = fmMatch[1]
90
+ fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: subagent') : `mode: subagent\n${fm}`
91
+ fm = /^model:/m.test(fm) ? fm.replace(/^model:.*$/m, modelLine) : `${modelLine}\n${fm}`
92
+
93
+ // Slice at frontmatter end: never String.replace with content-derived
94
+ // strings, it matches the wrong occurrence and expands $& sequences.
95
+ return `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}`
96
+ }
97
+
98
+ // Ensure template files are mode: subagent with no stale model: line. Only
99
+ // build and plan are primary; every engineer is reached through task(), never
100
+ // picked by a human, so a primary engineer would just clutter the picker.
101
+ // Templates must never declare a model either: models resolve per tier at
102
+ // startup and are injected into generated files only, so a stale model: (from
103
+ // a prior `stampAgentModels` run, or from when these were primary) is
104
+ // stripped and the agent falls back to the session model in opencode.jsonc.
105
+ function normalizeTemplate(templateContent) {
106
+ const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
107
+ if (!fmMatch) return templateContent
108
+
109
+ let fm = fmMatch[1]
110
+ let changed = false
111
+
112
+ if (!/^mode:\s*subagent/m.test(fm)) {
113
+ fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: subagent') : `mode: subagent\n${fm}`
114
+ changed = true
115
+ }
116
+ if (/^model:/m.test(fm)) {
117
+ // Remove the model: line and its trailing newline without leaving a
118
+ // leading blank line (model: can be the first frontmatter entry).
119
+ fm = fm.replace(/^model:[^\n]*\r?\n?/m, '').replace(/^\r?\n/, '').replace(/\n$/, '')
120
+ changed = true
121
+ }
122
+
123
+ return changed ? `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}` : templateContent
124
+ }
125
+
126
+ function descriptionOf(templateContent) {
127
+ return templateContent.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? null
128
+ }
129
+
130
+ // Build a primary from the fullstack body: same identity and the same
131
+ // ## Abilities block, with our own frontmatter on top. The body is taken
132
+ // verbatim so /make-engineer only has to maintain fullstack-engineer.md and
133
+ // both primaries inherit whatever it lists.
134
+ function buildPrimary(name, spec, fullstackContent, model) {
135
+ const body = fullstackContent.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim()
136
+
137
+ const lines = [
138
+ '---',
139
+ `description: ${spec.description}`,
140
+ 'mode: primary',
141
+ ]
142
+ if (model) lines.push(`model: ${model}`)
143
+ lines.push('color: warning')
144
+ lines.push('permission:')
145
+ // plan denies edit; everything else stays allowed so the planning skills can
146
+ // still read the tree, shell out to git and openspec, and spawn engineers.
147
+ lines.push(` edit: ${spec.permission?.edit ?? 'allow'}`)
148
+ for (const key of ['bash', 'read', 'glob', 'grep', 'question', 'todowrite', 'task', 'skill']) {
149
+ lines.push(` ${key}: ${spec.permission?.[key] ?? 'allow'}`)
150
+ }
151
+ lines.push('---')
152
+
153
+ return `${lines.join('\n')}\n\n${body}\n`
154
+ }
155
+
156
+ // Skip writing if the file already has identical content.
157
+ async function writeIfChanged(filePath, content) {
158
+ try {
159
+ if ((await fs.readFile(filePath, "utf-8")) === content) return
160
+ } catch {
161
+ // file missing, write it
162
+ }
163
+ const tmpPath = `${filePath}.tmp`
164
+ await fs.writeFile(tmpPath, content, "utf-8")
165
+ await fs.rename(tmpPath, filePath)
166
+ }
167
+
168
+ return {
169
+ config: async (cfg) => {
170
+ try {
171
+ const models = await resolveModels()
172
+ const available = TIERS.filter(t => models[t])
173
+ const { templates, variantFiles, hasFullstack } = await scanEngineers()
174
+
175
+ // build.md and plan.md are regenerated from fullstack-engineer.md every
176
+ // startup, which is also how an edit to fullstack propagates to both.
177
+ // normalizeTemplate runs on it too, so a repo carrying the old
178
+ // mode: primary fullstack is migrated in place on first launch.
179
+ if (hasFullstack) {
180
+ const fullstackPath = path.join(agentsDir, FULLSTACK_TEMPLATE)
181
+ const rawFullstack = await fs.readFile(fullstackPath, "utf-8")
182
+ const fullstack = normalizeTemplate(rawFullstack)
183
+ if (fullstack !== rawFullstack) {
184
+ await writeIfChanged(fullstackPath, fullstack)
185
+ console.error(`[pc-subagent-tiers] Normalized ${FULLSTACK_TEMPLATE} (mode: subagent)`)
186
+ }
187
+
188
+ for (const [name, spec] of Object.entries(PRIMARIES)) {
189
+ const model = models[spec.tier]
190
+ await writeIfChanged(
191
+ path.join(agentsDir, `${name}.md`),
192
+ buildPrimary(name, spec, fullstack, model),
193
+ )
194
+ if (cfg?.agent) {
195
+ // Override opencode's built-in agent of the same name rather than
196
+ // disabling it, so the picker shows ours with our model.
197
+ cfg.agent[name] = {
198
+ ...cfg.agent[name],
199
+ mode: 'primary',
200
+ description: spec.description,
201
+ ...(model ? { model } : {}),
202
+ ...(spec.permission ? { permission: { ...cfg.agent[name]?.permission, ...spec.permission } } : {}),
203
+ }
204
+ }
205
+ }
206
+ console.error(`[pc-subagent-tiers] Wrote primaries: build (${models.build ?? 'session model'}), plan (${models.plan ?? 'session model'})`)
207
+ } else {
208
+ console.error(`[pc-subagent-tiers] ${FULLSTACK_TEMPLATE} missing: build and plan not generated`)
209
+ }
210
+
211
+ const templateContents = await Promise.all(
212
+ templates.map(async name => {
213
+ const rawContent = await fs.readFile(path.join(agentsDir, `${name}.md`), "utf-8")
214
+ const content = normalizeTemplate(rawContent)
215
+ // If the template had the wrong mode or a stale model:, persist the fix to disk
216
+ if (content !== rawContent) {
217
+ await writeIfChanged(path.join(agentsDir, `${name}.md`), content)
218
+ console.error(`[pc-subagent-tiers] Normalized ${name}.md (mode: subagent, model: removed)`)
219
+ }
220
+ return { name, content }
221
+ })
222
+ )
223
+
224
+ const keepSet = new Set()
225
+ const variantsToWrite = []
226
+
227
+ for (const { name, content } of templateContents) {
228
+ for (const tier of available) {
229
+ const file = variantFile(name, tier)
230
+ variantsToWrite.push({
231
+ file,
232
+ path: path.join(agentsDir, file),
233
+ content: buildVariant(content, models[tier]),
234
+ name,
235
+ tier,
236
+ templateContent: content,
237
+ })
238
+ keepSet.add(file)
239
+ }
240
+ }
241
+
242
+ await Promise.all(variantsToWrite.map(v => writeIfChanged(v.path, v.content)))
243
+
244
+ if (cfg?.agent) {
245
+ // Ensure base templates are always mode: subagent in-memory. A repo
246
+ // upgraded from an earlier version may still have primary in config.
247
+ for (const { name } of templateContents) {
248
+ if (cfg.agent[name]) {
249
+ cfg.agent[name].mode = 'subagent'
250
+ }
251
+ }
252
+ if (cfg.agent[FULLSTACK_NAME]) {
253
+ cfg.agent[FULLSTACK_NAME].mode = 'subagent'
254
+ }
255
+ for (const { name, tier, templateContent } of variantsToWrite) {
256
+ const base = cfg.agent[name]
257
+ cfg.agent[`${name}.${tier}`] = base
258
+ ? { ...base, mode: 'subagent', model: models[tier] }
259
+ : {
260
+ mode: "subagent",
261
+ description: descriptionOf(templateContent) ?? `${name} (${tier} tier)`,
262
+ model: models[tier],
263
+ }
264
+ }
265
+ }
266
+
267
+ await Promise.all(
268
+ variantFiles.filter(f => !keepSet.has(f)).map(f => fs.unlink(path.join(agentsDir, f)).catch(() => {}))
269
+ )
270
+
271
+ if (variantsToWrite.length > 0) {
272
+ console.error(`[pc-subagent-tiers] Created ${variantsToWrite.length} variant files (${templates.length} engineers x ${available.length} tiers)`)
273
+ } else if (templates.length > 0) {
274
+ console.error(`[pc-subagent-tiers] No variants created. Models: ${JSON.stringify(models)}`)
275
+ }
276
+ } catch (err) {
277
+ console.error(`[pc-subagent-tiers] Error: ${err.message}`)
278
+ }
279
+ },
280
+ }
281
+ }