pantheon-opencode 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1013 -0
  4. package/bin/pantheon-init.mjs +183 -0
  5. package/commands/pantheon-audit.md +25 -0
  6. package/commands/pantheon-bg.md +10 -0
  7. package/commands/pantheon-consolidate.md +11 -0
  8. package/commands/pantheon-deepwork.md +128 -0
  9. package/commands/pantheon-doc.md +10 -0
  10. package/commands/pantheon-focus.md +9 -0
  11. package/commands/pantheon-forget.md +58 -0
  12. package/commands/pantheon-hash.md +11 -0
  13. package/commands/pantheon-optimize.md +79 -0
  14. package/commands/pantheon-remember.md +44 -0
  15. package/commands/pantheon-search.md +48 -0
  16. package/commands/pantheon-status.md +71 -0
  17. package/commands/pantheon-todo.md +11 -0
  18. package/commands/pantheon.md +49 -0
  19. package/docs/AGENT-MCP.md +194 -0
  20. package/docs/ARCHITECTURE.md +384 -0
  21. package/docs/BRANCH-PROTECTION.md +142 -0
  22. package/docs/INDEX.md +81 -0
  23. package/docs/INSTALLATION.md +217 -0
  24. package/docs/MCP.md +238 -0
  25. package/docs/MEMORY.md +471 -0
  26. package/docs/MIGRATION-MEMORY-BANK.md +139 -0
  27. package/docs/PLATFORMS.md +5 -0
  28. package/docs/QUICKSTART.md +49 -0
  29. package/docs/README.md +18 -0
  30. package/docs/RELEASING.md +256 -0
  31. package/docs/SETUP.md +5 -0
  32. package/docs/UPGRADING.md +41 -0
  33. package/docs/mcp-recommendations.md +439 -0
  34. package/docs/mcp-tools.md +156 -0
  35. package/docs/mcp-user-guide.md +204 -0
  36. package/docs/persistence-mcp.md +111 -0
  37. package/package.json +72 -0
  38. package/pantheon.schema.json +158 -0
  39. package/scripts/__init__.py +0 -0
  40. package/scripts/_pantheon_paths.py +68 -0
  41. package/scripts/check-version-consistency.sh +23 -0
  42. package/scripts/code_mode_server.py +202 -0
  43. package/scripts/doctor.mjs +763 -0
  44. package/scripts/generate-prompts.sh +222 -0
  45. package/scripts/generate-routing-docs.mjs +104 -0
  46. package/scripts/hash_verify.py +192 -0
  47. package/scripts/init-pantheon-mcp.sh +118 -0
  48. package/scripts/install/agents-md.mjs +214 -0
  49. package/scripts/install/health-check.mjs +196 -0
  50. package/scripts/install/migrate.mjs +209 -0
  51. package/scripts/install/opencode.mjs +645 -0
  52. package/scripts/install/shared.mjs +655 -0
  53. package/scripts/install/venv.mjs +116 -0
  54. package/scripts/install-mcp.mjs +885 -0
  55. package/scripts/install.mjs +26 -0
  56. package/scripts/manifest.mjs +622 -0
  57. package/scripts/mcp_persistence_server.py +459 -0
  58. package/scripts/mcp_resources_server.py +206 -0
  59. package/scripts/memory_cache.py +78 -0
  60. package/scripts/memory_mcp_server.py +605 -0
  61. package/scripts/paths.py +64 -0
  62. package/scripts/prune_context.py +72 -0
  63. package/scripts/release-bundle.mjs +109 -0
  64. package/scripts/scrub-secrets.py +282 -0
  65. package/scripts/scrub_secrets.py +281 -0
  66. package/scripts/test-context-compression.sh +166 -0
  67. package/scripts/themis_heuristic_scan.py +287 -0
  68. package/scripts/todo_enforcer.py +242 -0
  69. package/scripts/uninstall.mjs +1057 -0
  70. package/scripts/validate-routing.mjs +160 -0
  71. package/scripts/validate_agent_frontmatter.py +226 -0
  72. package/scripts/versioning.mjs +254 -0
  73. package/skills-lock.json +16 -0
  74. package/src/agents/aphrodite.md +162 -0
  75. package/src/agents/apollo.md +109 -0
  76. package/src/agents/athena.md +226 -0
  77. package/src/agents/demeter.md +146 -0
  78. package/src/agents/gaia.md +82 -0
  79. package/src/agents/hephaestus.md +105 -0
  80. package/src/agents/hermes.md +302 -0
  81. package/src/agents/iris.md +99 -0
  82. package/src/agents/mnemosyne.md +226 -0
  83. package/src/agents/nyx.md +87 -0
  84. package/src/agents/prometheus.md +199 -0
  85. package/src/agents/talos.md +93 -0
  86. package/src/agents/themis.md +187 -0
  87. package/src/agents/zeus.md +209 -0
  88. package/src/instructions/agent-return-format.instructions.md +26 -0
  89. package/src/instructions/backend-standards.instructions.md +45 -0
  90. package/src/instructions/documentation-standards.instructions.md +53 -0
  91. package/src/instructions/frontend-standards.instructions.md +46 -0
  92. package/src/instructions/memory-protocol.instructions.md +67 -0
  93. package/src/instructions/yagni.instructions.md +21 -0
  94. package/src/instructions/zeus-anti-stall.instructions.md +72 -0
  95. package/src/instructions/zeus-communication-rules.instructions.md +15 -0
  96. package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
  97. package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
  98. package/src/mcp/_pantheon_paths.py +67 -0
  99. package/src/mcp/code_mode_server.py +202 -0
  100. package/src/mcp/init-pantheon-mcp.sh +118 -0
  101. package/src/mcp/install-mcp.mjs +885 -0
  102. package/src/mcp/mcp_persistence_server.py +458 -0
  103. package/src/mcp/mcp_resources_server.py +205 -0
  104. package/src/mcp/memory_mcp_server.py +604 -0
  105. package/src/mcp/requirements-mcp-core.txt +5 -0
  106. package/src/mcp/requirements-mcp.txt +5 -0
  107. package/src/plugin.ts +19 -0
  108. package/src/plugins/tui/dist/tui.js +143 -0
  109. package/src/plugins/tui/dist/tui.tsx +144 -0
  110. package/src/plugins/tui/package.json +32 -0
  111. package/src/plugins/tui/src/index.tsx +144 -0
  112. package/src/plugins/tui/tsdown.config.ts +22 -0
  113. package/src/routing.yml +499 -0
  114. package/src/skills/README.md +230 -0
  115. package/src/skills/agent-coordination/SKILL.md +95 -0
  116. package/src/skills/artifact-management/SKILL.md +118 -0
  117. package/src/skills/auto-continue/SKILL.md +280 -0
  118. package/src/skills/code-review-checklist/SKILL.md +139 -0
  119. package/src/skills/context-compression/SKILL.md +861 -0
  120. package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
  121. package/src/skills/incremental-implementation/SKILL.md +27 -0
  122. package/src/skills/memory-bank/SKILL.md +165 -0
  123. package/src/skills/orchestration-workflow/SKILL.md +311 -0
  124. package/src/skills/security-hardening/SKILL.md +36 -0
  125. package/src/skills/session-goal/SKILL.md +138 -0
  126. package/src/skills/spec-driven-development/SKILL.md +23 -0
  127. package/src/skills/tdd-with-agents/SKILL.md +170 -0
  128. package/src/skills/visual-review-pipeline/SKILL.md +200 -0
@@ -0,0 +1,645 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * opencode.mjs — OpenCode platform installer
4
+ */
5
+
6
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
7
+ import { homedir } from 'node:os'
8
+ import { join, resolve } from 'node:path'
9
+ import { healthCheck } from './health-check.mjs'
10
+ import { detectVersion, runMigrations } from './migrate.mjs'
11
+ import {
12
+ collectSkillNames,
13
+ copyFiles,
14
+ installSkills,
15
+ PLATFORM_DIR,
16
+ parseFrontmatter,
17
+ ROOT,
18
+ sourceDirValid,
19
+ summary,
20
+ syncDir,
21
+ writeIfChanged,
22
+ } from './shared.mjs'
23
+ import { setupVenv } from './venv.mjs'
24
+
25
+ function deepClone(value) {
26
+ return JSON.parse(JSON.stringify(value))
27
+ }
28
+
29
+ function mergeMissing(target, source) {
30
+ if (!source || typeof source !== 'object' || Array.isArray(source)) return target
31
+ if (!target || typeof target !== 'object' || Array.isArray(target)) return deepClone(source)
32
+
33
+ for (const [key, sourceVal] of Object.entries(source)) {
34
+ const targetVal = target[key]
35
+
36
+ if (targetVal === undefined) {
37
+ target[key] = deepClone(sourceVal)
38
+ continue
39
+ }
40
+
41
+ if (
42
+ sourceVal &&
43
+ typeof sourceVal === 'object' &&
44
+ !Array.isArray(sourceVal) &&
45
+ targetVal &&
46
+ typeof targetVal === 'object' &&
47
+ !Array.isArray(targetVal)
48
+ ) {
49
+ mergeMissing(targetVal, sourceVal)
50
+ }
51
+ }
52
+
53
+ return target
54
+ }
55
+
56
+ /**
57
+ * Detect whether `target` is the user's global OpenCode installation directory
58
+ * (~/.opencode or $XDG_CONFIG_HOME/opencode).
59
+ * Global installs use a flat layout: agents/ skills/ commands/ plugins/
60
+ * Project installs use the .opencode/ sub-directory layout.
61
+ */
62
+ function isGlobalConfigDir(target) {
63
+ // Primary: ~/.opencode (actual OpenCode installation)
64
+ const homeDir = resolve(join(homedir(), '.opencode'))
65
+ if (resolve(target) === homeDir) return true
66
+
67
+ // Fallback: $XDG_CONFIG_HOME/opencode (legacy/alternative)
68
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config')
69
+ const xdgDir = resolve(join(xdgConfig, 'opencode'))
70
+ return resolve(target) === xdgDir
71
+ }
72
+
73
+ export function installOpenCode(
74
+ target,
75
+ dryRun,
76
+ clean = false,
77
+ components = ['agents', 'skills', 'instructions', 'commands', 'plugins', 'runtime'],
78
+ ) {
79
+ const componentSet = new Set(components)
80
+ const stats = summary.opencode
81
+
82
+ // Determine layout based on install scope.
83
+ // Global config dir (~/.opencode or ~/.config/opencode) uses a flat layout:
84
+ // agents/ skills/ commands/ plugins/
85
+ // Project installs use the .opencode/ sub-directory layout:
86
+ // .opencode/agents/ .opencode/skills/ .opencode/commands/
87
+ const isGlobal = isGlobalConfigDir(target)
88
+ const _subDir = isGlobal ? '' : '.opencode'
89
+ const agentPrefix = isGlobal ? 'agents' : '.opencode/agents'
90
+
91
+ if (isGlobal) {
92
+ console.log(
93
+ ' 🌐 Global config directory detected — using flat layout (agents/, skills/, commands/)',
94
+ )
95
+ }
96
+
97
+ // -----------------------------------------------------------------------
98
+ // 1. Install agents (--components agents)
99
+ // -----------------------------------------------------------------------
100
+ if (componentSet.has('agents')) {
101
+ const srcDir = join(ROOT, 'src', 'agents')
102
+ if (!sourceDirValid(srcDir)) {
103
+ console.warn(` ⚠️ Agent source directory not found: ${srcDir}`)
104
+ stats.errors++
105
+ } else {
106
+ const dstDir = isGlobal ? join(target, 'agents') : join(target, '.opencode', 'agents')
107
+ if (!dryRun) mkdirSync(dstDir, { recursive: true })
108
+ if (clean && existsSync(dstDir) && !dryRun) {
109
+ const existing = readdirSync(dstDir)
110
+ for (const f of existing) {
111
+ rmSync(join(dstDir, f), { recursive: true, force: true })
112
+ }
113
+ }
114
+ const { created, skipped } = copyFiles(srcDir, dstDir, dryRun)
115
+ stats.created += created
116
+ stats.skipped += skipped
117
+ }
118
+ }
119
+
120
+ // -----------------------------------------------------------------------
121
+ // 2. Install skills (--components skills)
122
+ // -----------------------------------------------------------------------
123
+ if (componentSet.has('skills')) {
124
+ const skillNames = collectSkillNames()
125
+ if (skillNames.length > 0) {
126
+ console.log(` 📚 Installing ${skillNames.length} skills...`)
127
+ const dstSkillsDir = isGlobal ? join(target, 'skills') : join(target, '.opencode', 'skills')
128
+ if (clean && existsSync(dstSkillsDir) && !dryRun) {
129
+ const existing = readdirSync(dstSkillsDir)
130
+ for (const s of existing) {
131
+ rmSync(join(dstSkillsDir, s), { recursive: true, force: true })
132
+ }
133
+ }
134
+ const installSubDir = isGlobal ? '' : '.opencode'
135
+ const { created: sCreated, skipped: sSkipped } = installSkills(
136
+ skillNames,
137
+ target,
138
+ dryRun,
139
+ installSubDir,
140
+ )
141
+ stats.created += sCreated
142
+ stats.skipped += sSkipped
143
+ }
144
+ }
145
+
146
+ // -----------------------------------------------------------------------
147
+ // 2.5 Install instructions: AGENTS.md + instructions/ (--components instructions)
148
+ // -----------------------------------------------------------------------
149
+ if (componentSet.has('instructions')) {
150
+ // AGENTS.md
151
+ const srcAgentsMd = join(ROOT, 'AGENTS.md')
152
+ const dstAgentsMd = join(target, 'AGENTS.md')
153
+ if (existsSync(srcAgentsMd)) {
154
+ const content = readFileSync(srcAgentsMd, 'utf8')
155
+ const status = writeIfChanged(dstAgentsMd, content, dryRun)
156
+ if (status === 'created') stats.created++
157
+ else stats.skipped++
158
+ }
159
+ // instructions/ directory
160
+ const srcInstr = join(ROOT, 'src', 'instructions')
161
+ const dstInstr = join(target, 'instructions')
162
+ if (existsSync(srcInstr)) {
163
+ const instrResult = syncDir(srcInstr, dstInstr, dryRun, clean)
164
+ stats.created += instrResult.created
165
+ stats.skipped += instrResult.skipped
166
+ }
167
+ }
168
+
169
+ // -----------------------------------------------------------------------
170
+ // 2.6 Install prompts (--components prompts)
171
+ // -----------------------------------------------------------------------
172
+ if (componentSet.has('prompts')) {
173
+ const srcPrompts = join(ROOT, 'prompts')
174
+ const dstPrompts = join(target, 'prompts')
175
+ if (existsSync(srcPrompts)) {
176
+ const promptsResult = syncDir(srcPrompts, dstPrompts, dryRun, clean)
177
+ stats.created += promptsResult.created
178
+ stats.skipped += promptsResult.skipped
179
+ }
180
+ }
181
+
182
+ // -----------------------------------------------------------------------
183
+ // 2.7 Install commands (--components commands)
184
+ // -----------------------------------------------------------------------
185
+ if (componentSet.has('commands')) {
186
+ const srcCmds = join(ROOT, 'commands')
187
+ const dstCmds = isGlobal ? join(target, 'commands') : join(target, '.opencode', 'commands')
188
+ if (existsSync(srcCmds)) {
189
+ const cmdResult = syncDir(srcCmds, dstCmds, dryRun, clean, (f) => f.endsWith('.md'))
190
+ stats.created += cmdResult.created
191
+ stats.skipped += cmdResult.skipped
192
+ }
193
+ }
194
+
195
+ // -----------------------------------------------------------------------
196
+ // 2.8 Install TUI plugins (--components plugins)
197
+ // -----------------------------------------------------------------------
198
+ if (componentSet.has('plugins')) {
199
+ const srcPluginDir = join(PLATFORM_DIR, 'opencode', '.opencode', 'plugins', 'pantheon-tui')
200
+ const dstPluginDir = isGlobal
201
+ ? join(target, 'plugins', 'pantheon-tui')
202
+ : join(target, '.opencode', 'plugins', 'pantheon-tui')
203
+ if (!dryRun) mkdirSync(dstPluginDir, { recursive: true })
204
+ if (clean && existsSync(dstPluginDir) && !dryRun) {
205
+ rmSync(dstPluginDir, { recursive: true, force: true })
206
+ mkdirSync(dstPluginDir, { recursive: true })
207
+ }
208
+ const { created, skipped } = syncDir(srcPluginDir, dstPluginDir, dryRun, false)
209
+ stats.created += created
210
+ stats.skipped += skipped
211
+ }
212
+
213
+ // -----------------------------------------------------------------------
214
+ // 2.9 Create/update tui.json with plugin registration
215
+ // -----------------------------------------------------------------------
216
+ const targetTuiConfigPath = isGlobal
217
+ ? join(target, 'tui.json')
218
+ : join(target, '.opencode', 'tui.json')
219
+ let tuiConfig = { plugin: [] }
220
+ if (existsSync(targetTuiConfigPath)) {
221
+ try {
222
+ tuiConfig = JSON.parse(readFileSync(targetTuiConfigPath, 'utf8'))
223
+ } catch {
224
+ /* use default */
225
+ }
226
+ }
227
+ if (!Array.isArray(tuiConfig.plugin)) {
228
+ tuiConfig.plugin = []
229
+ }
230
+ // Add our plugin if not already present
231
+ const pluginRef = 'plugins/pantheon-tui/dist/tui.tsx'
232
+ if (!tuiConfig.plugin.includes(pluginRef)) {
233
+ tuiConfig.plugin.push(pluginRef)
234
+ }
235
+ const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
236
+ const tuiStatus = writeIfChanged(targetTuiConfigPath, tuiContent, dryRun)
237
+ if (tuiStatus === 'created') stats.created++
238
+ else stats.skipped++
239
+
240
+ // -----------------------------------------------------------------------
241
+ // 2.10 Install runtime infrastructure (--components runtime)
242
+ // MCP server scripts, code-mode scripts, tiers.json
243
+ // -----------------------------------------------------------------------
244
+ if (componentSet.has('runtime')) {
245
+ const runtimeTarget = isGlobal ? target : join(target, '.opencode')
246
+
247
+ // ── MCP server scripts ──
248
+ const mcpScripts = [
249
+ 'mcp_resources_server.py',
250
+ 'code_mode_server.py',
251
+ 'memory_mcp_server.py',
252
+ 'scrub-secrets.py',
253
+ '_pantheon_paths.py',
254
+ 'mcp_persistence_server.py',
255
+ ]
256
+ const srcScriptsDir = join(ROOT, 'scripts')
257
+ const dstScriptsDir = join(runtimeTarget, 'scripts')
258
+ if (!dryRun) mkdirSync(dstScriptsDir, { recursive: true })
259
+ for (const script of mcpScripts) {
260
+ const src = join(srcScriptsDir, script)
261
+ if (!existsSync(src)) {
262
+ console.warn(` ⚠️ Script not found: ${src}`)
263
+ stats.errors++
264
+ continue
265
+ }
266
+ const content = readFileSync(src, 'utf8')
267
+ const dst = join(dstScriptsDir, script)
268
+ const status = writeIfChanged(dst, content, dryRun)
269
+ if (status === 'created') {
270
+ stats.created++
271
+ // Make executable
272
+ if (!dryRun) {
273
+ try {
274
+ chmodSync(dst, 0o755)
275
+ } catch {
276
+ /* non-critical */
277
+ }
278
+ }
279
+ } else stats.skipped++
280
+ }
281
+
282
+ // ── Code-mode scripts ──
283
+ const srcCodeModeDir = join(ROOT, '.pantheon', 'code-mode')
284
+ const dstCodeModeDir = join(runtimeTarget, '.pantheon', 'code-mode')
285
+ if (existsSync(srcCodeModeDir)) {
286
+ const cmResult = syncDir(srcCodeModeDir, dstCodeModeDir, dryRun, clean)
287
+ stats.created += cmResult.created
288
+ stats.skipped += cmResult.skipped
289
+ }
290
+
291
+ // ── tiers.json ──
292
+ const srcTiers = join(ROOT, '.pantheon', 'tiers.json')
293
+ const dstTiers = join(runtimeTarget, '.pantheon', 'tiers.json')
294
+ if (existsSync(srcTiers)) {
295
+ const content = readFileSync(srcTiers, 'utf8')
296
+ const status = writeIfChanged(dstTiers, content, dryRun)
297
+ if (status === 'created') stats.created++
298
+ else stats.skipped++
299
+ }
300
+ }
301
+
302
+ // -----------------------------------------------------------------------
303
+ // 3. Create/update opencode.json (always runs)
304
+ // Reads TARGET's existing config first, then merges Pantheon settings
305
+ // on top. Preserves user's MCP, provider, plugin, compaction, theme.
306
+ // -----------------------------------------------------------------------
307
+ const pantheonConfigPath = join(ROOT, 'opencode.json')
308
+ const targetConfigPath = join(target, 'opencode.json')
309
+
310
+ let config = {}
311
+ if (existsSync(targetConfigPath)) {
312
+ try {
313
+ config = JSON.parse(readFileSync(targetConfigPath, 'utf8'))
314
+ } catch {
315
+ config = {}
316
+ }
317
+ }
318
+
319
+ let pantheonConfig = {}
320
+ if (existsSync(pantheonConfigPath)) {
321
+ try {
322
+ pantheonConfig = JSON.parse(readFileSync(pantheonConfigPath, 'utf8'))
323
+ } catch {
324
+ pantheonConfig = {}
325
+ }
326
+ }
327
+
328
+ // --------------------------------------------------------------------
329
+ // A. Parse canonical agent config from agents/*.agent.md frontmatter
330
+ // and merge into opencode.json config.
331
+ // --------------------------------------------------------------------
332
+ function getAgentSources(agentPrefix) {
333
+ const agentsDir = join(ROOT, 'src', 'agents')
334
+ if (!existsSync(agentsDir)) return {}
335
+ const sources = {}
336
+ const files = readdirSync(agentsDir).filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
337
+ for (const f of files) {
338
+ const name = f.replace(/\.(agent\.)?md$/, '')
339
+ sources[name] = `${agentPrefix}/${name}.md`
340
+ }
341
+ return sources
342
+ }
343
+
344
+ function readAgentConfigFromCanonical() {
345
+ const agentsDir = join(ROOT, 'src', 'agents')
346
+ if (!existsSync(agentsDir)) return {}
347
+ const config = {}
348
+ const files = readdirSync(agentsDir).filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
349
+ for (const f of files) {
350
+ const name = f.replace(/\.(agent\.)?md$/, '')
351
+ const content = readFileSync(join(agentsDir, f), 'utf8')
352
+ const parsed = parseFrontmatter(content)
353
+ if (!parsed) continue
354
+ const fm = parsed.fm
355
+ const agent = {}
356
+
357
+ // Extract fields from frontmatter
358
+ if (fm.color) agent.color = fm.color
359
+ if (fm.description) agent.description = fm.description
360
+ if (fm.mode) agent.mode = fm.mode
361
+ if (fm.hidden) agent.hidden = fm.hidden
362
+ if (fm.temperature !== undefined) agent.temperature = fm.temperature
363
+ if (fm.steps !== undefined) agent.steps = fm.steps
364
+ // Support both hyphen (YAML) and underscore (JSON) key variants
365
+ if (fm['disable-model-invocation'] !== undefined)
366
+ agent.disable_model_invocation = fm['disable-model-invocation']
367
+ else if (fm.disable_model_invocation !== undefined)
368
+ agent.disable_model_invocation = fm.disable_model_invocation
369
+
370
+ // Build permission from frontmatter
371
+ if (fm.permission) {
372
+ agent.permission = JSON.parse(JSON.stringify(fm.permission))
373
+ }
374
+
375
+ config[name] = agent
376
+ }
377
+ return config
378
+ }
379
+
380
+ const canonicalAgentConfig = readAgentConfigFromCanonical()
381
+ const agentSources = getAgentSources(agentPrefix)
382
+ const MANAGED_FIELDS = [
383
+ 'steps',
384
+ 'temperature',
385
+ 'color',
386
+ 'permission',
387
+ 'mode',
388
+ 'hidden',
389
+ 'disable_model_invocation',
390
+ ]
391
+
392
+ if (!config.agent) config.agent = {}
393
+
394
+ for (const [agentName, agentCfg] of Object.entries(canonicalAgentConfig)) {
395
+ if (!agentCfg || typeof agentCfg !== 'object') continue
396
+
397
+ if (config.agent[agentName]) {
398
+ // ── Agent exists in target config ──
399
+ // Update framework-managed fields from canonical source
400
+ // Preserve user-customized fields (model, provider, mcp, etc.)
401
+ const existing = config.agent[agentName]
402
+ if (agentSources[agentName]) existing.source = agentSources[agentName]
403
+ for (const field of MANAGED_FIELDS) {
404
+ if (field in agentCfg) {
405
+ existing[field] = JSON.parse(JSON.stringify(agentCfg[field]))
406
+ }
407
+ }
408
+ // Remove stale fields that are no longer in canonical config
409
+ delete existing.model
410
+ delete existing.small_model
411
+ } else {
412
+ // ── New agent ──
413
+ const newAgent = {}
414
+ if (agentSources[agentName]) newAgent.source = agentSources[agentName]
415
+ if (agentCfg.description) newAgent.description = agentCfg.description
416
+
417
+ // Copy all framework-managed fields from canonical config
418
+ for (const field of MANAGED_FIELDS) {
419
+ if (field in agentCfg) {
420
+ newAgent[field] = JSON.parse(JSON.stringify(agentCfg[field]))
421
+ }
422
+ }
423
+
424
+ // Ensure bash permission is set from canonical (default to deny if missing)
425
+ if (!newAgent.permission) {
426
+ newAgent.permission = {}
427
+ }
428
+ if (!newAgent.permission.bash && agentCfg.permission?.bash) {
429
+ newAgent.permission.bash = JSON.parse(JSON.stringify(agentCfg.permission.bash))
430
+ }
431
+
432
+ config.agent[agentName] = newAgent
433
+ }
434
+ }
435
+
436
+ // Remove stale agents (exist in target config but not in canonical source)
437
+ // Only removes agents whose source is managed (starts with our prefix)
438
+ // so user-defined agents with different source paths are preserved.
439
+ const canonicalNames = new Set(Object.keys(canonicalAgentConfig))
440
+ for (const [agentName, agentCfg] of Object.entries(config.agent)) {
441
+ const source = agentCfg?.source || ''
442
+ if (source.startsWith(agentPrefix) && !canonicalNames.has(agentName)) {
443
+ delete config.agent[agentName]
444
+ }
445
+ }
446
+ if (Object.keys(config.agent).length === 0) delete config.agent
447
+
448
+ // --------------------------------------------------------------------
449
+ // B. Commands from .md frontmatter (commands.json removed)
450
+ // --------------------------------------------------------------------
451
+ // Commands are now sourced from .md frontmatter in commands/.
452
+ // The .md frontmatter is the canonical source — no json merge needed.
453
+
454
+ // --------------------------------------------------------------------
455
+ // B.5 Ensure critical top-level OpenCode config sections
456
+ // --------------------------------------------------------------------
457
+ if (!config.default_agent && pantheonConfig.default_agent) {
458
+ config.default_agent = pantheonConfig.default_agent
459
+ }
460
+
461
+ if (!Array.isArray(config.plugin)) {
462
+ config.plugin = []
463
+ }
464
+ if (Array.isArray(pantheonConfig.plugin)) {
465
+ for (const plugin of pantheonConfig.plugin) {
466
+ if (!config.plugin.includes(plugin)) {
467
+ config.plugin.push(plugin)
468
+ }
469
+ }
470
+ }
471
+
472
+ if (!config.provider && pantheonConfig.provider) {
473
+ config.provider = deepClone(pantheonConfig.provider)
474
+ } else if (config.provider && pantheonConfig.provider) {
475
+ mergeMissing(config.provider, pantheonConfig.provider)
476
+ }
477
+
478
+ if (!config.compaction && pantheonConfig.compaction) {
479
+ config.compaction = deepClone(pantheonConfig.compaction)
480
+ } else if (config.compaction && pantheonConfig.compaction) {
481
+ mergeMissing(config.compaction, pantheonConfig.compaction)
482
+ }
483
+
484
+ // --------------------------------------------------------------------
485
+ // C. Merge permissions
486
+ // --------------------------------------------------------------------
487
+ if (!config.permission) config.permission = {}
488
+ if (pantheonConfig.permission) {
489
+ mergeMissing(config.permission, pantheonConfig.permission)
490
+ }
491
+ if (componentSet.has('skills')) {
492
+ config.permission.skill = { '*': 'allow' }
493
+ }
494
+ if (!config.permission.bash) {
495
+ config.permission.bash = {
496
+ 'git *': 'allow',
497
+ 'npm *': 'allow',
498
+ 'npx *': 'allow',
499
+ 'pytest *': 'allow',
500
+ 'ruff *': 'allow',
501
+ 'black *': 'allow',
502
+ 'pip *': 'allow',
503
+ 'docker *': 'allow',
504
+ 'curl *': 'allow',
505
+ 'gh *': 'allow',
506
+ 'make *': 'allow',
507
+ }
508
+ }
509
+
510
+ // --------------------------------------------------------------------
511
+ // D. Merge instructions paths
512
+ // --------------------------------------------------------------------
513
+ const pantheonInstructions = ['AGENTS.md', 'instructions/*.instructions.md']
514
+ if (!config.instructions) {
515
+ config.instructions = [...pantheonInstructions]
516
+ } else {
517
+ for (const instr of pantheonInstructions) {
518
+ if (!config.instructions.includes(instr)) {
519
+ config.instructions.push(instr)
520
+ }
521
+ }
522
+ }
523
+
524
+ // --------------------------------------------------------------------
525
+ // E. Ensure $schema
526
+ // --------------------------------------------------------------------
527
+ if (!config.$schema) {
528
+ config.$schema = 'https://opencode.ai/config.json'
529
+ }
530
+
531
+ // --------------------------------------------------------------------
532
+ // F. Compatibility cleanup (OpenCode >= v1.15.7)
533
+ // --------------------------------------------------------------------
534
+ // `todoContinuation` is rejected by newer OpenCode versions.
535
+ // Remove it from merged user config to avoid startup/config failures.
536
+ if (Object.hasOwn(config, 'todoContinuation')) {
537
+ delete config.todoContinuation
538
+ }
539
+
540
+ // --------------------------------------------------------------------
541
+ // F.5 MCP server entries (for runtime-deployed scripts)
542
+ // --------------------------------------------------------------------
543
+ if (componentSet.has('runtime')) {
544
+ config.mcp = config.mcp || {}
545
+ const runtimeTarget = isGlobal ? target : join(target, '.opencode')
546
+ const pythonPath = process.platform === 'win32' ? 'python' : 'python3'
547
+ const venvPython = join(runtimeTarget, '.venv', 'bin', 'python3')
548
+ const memoryPython = existsSync(venvPython) ? venvPython : pythonPath
549
+
550
+ // Only add if not already configured by user
551
+ if (!config.mcp['pantheon-resources']) {
552
+ config.mcp['pantheon-resources'] = {
553
+ type: 'local',
554
+ cwd: runtimeTarget,
555
+ command: [memoryPython, 'scripts/mcp_resources_server.py'],
556
+ enabled: true,
557
+ }
558
+ }
559
+ if (!config.mcp['pantheon-code-mode']) {
560
+ config.mcp['pantheon-code-mode'] = {
561
+ type: 'local',
562
+ cwd: runtimeTarget,
563
+ command: [memoryPython, 'scripts/code_mode_server.py'],
564
+ enabled: true,
565
+ }
566
+ }
567
+ if (!config.mcp['pantheon-memory']) {
568
+ config.mcp['pantheon-memory'] = {
569
+ type: 'local',
570
+ cwd: runtimeTarget,
571
+ command: [memoryPython, 'scripts/memory_mcp_server.py'],
572
+ enabled: true,
573
+ }
574
+ }
575
+ if (!config.mcp['pantheon-persistence']) {
576
+ config.mcp['pantheon-persistence'] = {
577
+ type: 'local',
578
+ cwd: runtimeTarget,
579
+ command: [memoryPython, 'scripts/mcp_persistence_server.py'],
580
+ enabled: true,
581
+ }
582
+ }
583
+
584
+ // Default MCP permissions
585
+ config.permission = config.permission || {}
586
+ config.permission.mcp = config.permission.mcp || {}
587
+ if (!config.permission.mcp['pantheon-resources']) {
588
+ config.permission.mcp['pantheon-resources'] = 'allow'
589
+ }
590
+ if (!config.permission.mcp['pantheon-code-mode']) {
591
+ config.permission.mcp['pantheon-code-mode'] = 'ask'
592
+ }
593
+ if (!config.permission.mcp['pantheon-memory']) {
594
+ config.permission.mcp['pantheon-memory'] = 'allow'
595
+ }
596
+ if (!config.permission.mcp['pantheon-persistence']) {
597
+ config.permission.mcp['pantheon-persistence'] = 'allow'
598
+ }
599
+ }
600
+
601
+ const configContent = `${JSON.stringify(config, null, 2)}\n`
602
+ const status = writeIfChanged(targetConfigPath, configContent, dryRun)
603
+ if (status === 'created') stats.created++
604
+ else stats.skipped++
605
+
606
+ // -----------------------------------------------------------------------
607
+ // 3.5 Run migrations (--components runtime)
608
+ // -----------------------------------------------------------------------
609
+ if (componentSet.has('runtime')) {
610
+ const currentVersion = detectVersion(target)
611
+ if (currentVersion) {
612
+ const migration = runMigrations(target, currentVersion, { dryRun })
613
+ if (migration.applied > 0) {
614
+ console.log(` ✅ Applied ${migration.applied} migration(s)`)
615
+ for (const msg of migration.messages) {
616
+ console.log(` • ${msg}`)
617
+ }
618
+ }
619
+ }
620
+ }
621
+
622
+ // -----------------------------------------------------------------------
623
+ // 4. Setup virtual environment + health check (--components runtime)
624
+ // -----------------------------------------------------------------------
625
+ if (componentSet.has('runtime')) {
626
+ try {
627
+ setupVenv(target, { dryRun })
628
+ const health = healthCheck(target, { dryRun })
629
+
630
+ // Print health summary
631
+ console.log('\n 📊 Health Check:')
632
+ for (const p of health.passed) console.log(` ✅ ${p.check}: ${p.detail}`)
633
+ for (const w of health.warnings) console.log(` ⚠️ ${w.check}: ${w.detail}`)
634
+ for (const f of health.failed) console.log(` ❌ ${f.check}: ${f.detail}`)
635
+
636
+ if (health.failed.length > 0) {
637
+ console.log(` ⚠️ ${health.failed.length} check(s) failed — review above`)
638
+ stats.errors += health.failed.length
639
+ }
640
+ } catch (err) {
641
+ console.error(` ❌ Setup failed: ${err.message}`)
642
+ stats.errors++
643
+ }
644
+ }
645
+ }