pantheon-opencode 1.2.0 → 1.2.2

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 (63) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +112 -15
  3. package/bin/pantheon-init.mjs +93 -0
  4. package/commands/pantheon-vision.md +67 -0
  5. package/opencode.json +14 -10
  6. package/package.json +20 -4
  7. package/scripts/_pantheon_paths.py +0 -2
  8. package/scripts/changelog-extract.mjs +112 -0
  9. package/scripts/doctor.mjs +307 -15
  10. package/scripts/hooks/audit-imports.sh +43 -0
  11. package/scripts/hooks/format-multi-language.sh +96 -0
  12. package/scripts/hooks/log-session-start.sh +24 -0
  13. package/scripts/hooks/on-subagent-delegation-start.sh +91 -0
  14. package/scripts/hooks/on-subagent-delegation-stop.sh +37 -0
  15. package/scripts/hooks/run-type-check.sh +21 -0
  16. package/scripts/hooks/scan-secrets.sh +55 -0
  17. package/scripts/hooks/validate-post-conditions.sh +21 -0
  18. package/scripts/hooks/validate-talos-scope.sh +43 -0
  19. package/scripts/hooks/validate-tool-safety.sh +46 -0
  20. package/scripts/install/health-check.mjs +101 -1
  21. package/scripts/install/model-picker.mjs +118 -0
  22. package/scripts/install/opencode.mjs +155 -43
  23. package/scripts/install/plugin.mjs +4 -2
  24. package/scripts/install/venv.mjs +25 -2
  25. package/scripts/install-mcp.mjs +170 -37
  26. package/scripts/mcp_persistence_server.py +2 -1
  27. package/scripts/prune.mjs +359 -0
  28. package/scripts/secret-scan.mjs +88 -0
  29. package/scripts/uninstall.mjs +3 -3
  30. package/scripts/validate-routing.mjs +15 -0
  31. package/scripts/version-check.mjs +146 -0
  32. package/scripts/versioning.mjs +13 -14
  33. package/src/agents/hermes.md +2 -2
  34. package/src/agents/mnemosyne.md +1 -1
  35. package/src/agents/zeus.md +1 -1
  36. package/src/mcp/_pantheon_paths.py +0 -2
  37. package/src/mcp/install-mcp.mjs +6 -913
  38. package/src/mcp/mcp_persistence_server.py +2 -1
  39. package/src/mcp/pantheon_vision_server.py +495 -0
  40. package/src/mcp/requirements-mcp.txt +8 -5
  41. package/src/mcp/requirements-vision.txt +3 -0
  42. package/src/pantheon/auto-wake.ts +91 -0
  43. package/src/pantheon/background-job-board.ts +568 -0
  44. package/src/pantheon/file-persistence.ts +95 -0
  45. package/src/pantheon/presets.d.mts +108 -0
  46. package/src/pantheon/presets.mjs +554 -0
  47. package/src/pantheon/vision.ts +1437 -0
  48. package/src/plugin.ts +69 -25
  49. package/src/plugins/hook-runner.ts +186 -0
  50. package/src/plugins/pantheon-hooks.ts +153 -0
  51. package/src/plugins/tui/dist/tui.d.ts +1 -1
  52. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  53. package/src/plugins/tui/dist/tui.js +812 -223
  54. package/src/plugins/tui/dist/tui.js.map +1 -1
  55. package/src/plugins/tui/dist/tui.tsx +881 -121
  56. package/src/plugins/tui/package.json +3 -3
  57. package/src/plugins/tui/src/index.tsx +881 -121
  58. package/src/routing.yml +148 -0
  59. package/src/skills/context-compression/SKILL.md +1 -1
  60. package/src/skills/loop-engineering/SKILL.md +1 -1
  61. package/src/skills/orchestration-workflow/SKILL.md +11 -11
  62. package/scripts/scrub_secrets.py +0 -281
  63. package/src/mcp/requirements-mcp-core.txt +0 -5
@@ -3,12 +3,33 @@
3
3
  * opencode.mjs — OpenCode platform installer
4
4
  */
5
5
 
6
- import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
6
+ import {
7
+ chmodSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ readdirSync,
11
+ readFileSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from 'node:fs'
7
15
  import { homedir } from 'node:os'
8
- import { join, resolve } from 'node:path'
16
+ import { basename, join, resolve } from 'node:path'
17
+ import {
18
+ bullet,
19
+ colors,
20
+ configure,
21
+ error,
22
+ info,
23
+ printSummary,
24
+ section,
25
+ spinner,
26
+ step,
27
+ success,
28
+ warning,
29
+ } from './cli-ui.mjs'
9
30
  import { healthCheck } from './health-check.mjs'
10
31
  import { detectVersion, runMigrations } from './migrate.mjs'
11
- import { colors, configure, section, step, success, warning, error, info, bullet, spinner, printSummary } from './cli-ui.mjs'
32
+ import { installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
12
33
  import {
13
34
  collectSkillNames,
14
35
  copyFiles,
@@ -20,11 +41,17 @@ import {
20
41
  syncDir,
21
42
  writeIfChanged,
22
43
  } from './shared.mjs'
23
- import { setupVenv } from './venv.mjs'
24
- import { installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
25
- import { resolveComponents } from './component.mjs'
26
-
27
- const COMPONENT_NAMES = ['agents', 'skills', 'instructions', 'prompts', 'commands', 'plugins', 'runtime']
44
+ import { setupVenv, venvPythonPath } from './venv.mjs'
45
+
46
+ const COMPONENT_NAMES = [
47
+ 'agents',
48
+ 'skills',
49
+ 'instructions',
50
+ 'prompts',
51
+ 'commands',
52
+ 'plugins',
53
+ 'runtime',
54
+ ]
28
55
 
29
56
  function deepClone(value) {
30
57
  return JSON.parse(JSON.stringify(value))
@@ -63,7 +90,7 @@ function mergeMissing(target, source) {
63
90
  * Global installs use a flat layout: agents/ skills/ commands/ plugins/
64
91
  * Project installs use the .opencode/ sub-directory layout.
65
92
  */
66
- function isGlobalConfigDir(target) {
93
+ export function isGlobalConfigDir(target) {
67
94
  // Primary: ~/.opencode (actual OpenCode installation)
68
95
  const homeDir = resolve(join(homedir(), '.opencode'))
69
96
  if (resolve(target) === homeDir) return true
@@ -166,8 +193,9 @@ export async function installOpenCode(
166
193
  const srcRouting = join(ROOT, 'src', 'routing.yml')
167
194
  const dstRouting = join(target, 'routing.yml')
168
195
  if (existsSync(srcRouting)) {
169
- writeIfChanged(dstRouting, readFileSync(srcRouting, 'utf8'), dryRun)
170
- stats.created++
196
+ const routingStatus = writeIfChanged(dstRouting, readFileSync(srcRouting, 'utf8'), dryRun)
197
+ if (routingStatus === 'created') stats.created++
198
+ else stats.skipped++
171
199
  }
172
200
  }
173
201
 
@@ -253,24 +281,12 @@ export async function installOpenCode(
253
281
 
254
282
  // -----------------------------------------------------------------------
255
283
  // -----------------------------------------------------------------------
256
- // 2.7 Ensure subagent_depth in opencode.json
284
+ // 2.7 subagent_depth is merged into opencode.json in the config section
285
+ // below (always runs) — see "Ensure critical top-level config sections".
286
+ // Previously this was a separate block gated on the config file already
287
+ // existing, so a FRESH install only gained subagent_depth on the SECOND
288
+ // init run (idempotence bug, issue #19).
257
289
  // -----------------------------------------------------------------------
258
- if (componentSet.has('agents')) {
259
- const cfgPath = join(target, 'opencode.json')
260
- if (existsSync(cfgPath)) {
261
- try {
262
- const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'))
263
- let changed = false
264
- if (cfg.subagent_depth === undefined) { cfg.subagent_depth = 2; changed = true }
265
- if (changed && !dryRun) {
266
- writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
267
- success('subagent_depth: 2 added to opencode.json')
268
- }
269
- } catch (e) {
270
- warning(`Could not update opencode.json: ${e.message}`)
271
- }
272
- }
273
- }
274
290
 
275
291
  // 2.8 Install TUI plugins (--components plugins)
276
292
  // -----------------------------------------------------------------------
@@ -292,9 +308,16 @@ export async function installOpenCode(
292
308
  const targetTuiConfigPath = isGlobal
293
309
  ? join(target, 'tui.json')
294
310
  : join(target, '.opencode', 'tui.json')
295
- const pluginRef = 'plugins/pantheon-tui'
296
- unregisterPlugin(targetTuiConfigPath, pluginRef, { dryRun })
297
- const tuiStatus = registerPlugin(targetTuiConfigPath, pluginRef, { dryRun })
311
+ // Hermetic TUI plugin registration: opencode's tui loader treats relative
312
+ // entries ("plugins/pantheon-tui") as npm/github package specs and fails
313
+ // with NpmInstallFailedError ("Repository not found"). Register the plugin
314
+ // by the ABSOLUTE path of the built dist inside the installed package
315
+ // (derived from ROOT — never hardcoded), mirroring the loader's
316
+ // absolute-path support (same pattern as the hooks plugin fix).
317
+ const tuiPluginRef = join(ROOT, 'src', 'plugins', 'tui', 'dist', 'tui.tsx')
318
+ // Clean up the old broken relative ref (and stale dist refs) on upgrade.
319
+ unregisterPlugin(targetTuiConfigPath, 'plugins/pantheon-tui', { dryRun })
320
+ const tuiStatus = registerPlugin(targetTuiConfigPath, tuiPluginRef, { dryRun })
298
321
  if (tuiStatus === 'created') stats.created++
299
322
  else stats.skipped++
300
323
 
@@ -314,12 +337,26 @@ export async function installOpenCode(
314
337
  'scrub-secrets.py',
315
338
  '_pantheon_paths.py',
316
339
  'mcp_persistence_server.py',
340
+ 'pantheon_vision_server.py',
317
341
  ]
318
342
  const srcScriptsDir = join(ROOT, 'scripts')
343
+ // Canonical MCP server sources live in src/mcp/. Map them explicitly so the
344
+ // sync always copies from the canonical source, never from the stale root
345
+ // scripts/ copies (which previously propagated bugs like the missing
346
+ // `import uuid` in mcp_persistence_server.py). scrub-secrets.py is not in
347
+ // src/mcp/ and intentionally falls back to join(ROOT, 'scripts').
348
+ const canonicalMcpScripts = {
349
+ 'mcp_resources_server.py': join(ROOT, 'src', 'mcp', 'mcp_resources_server.py'),
350
+ 'code_mode_server.py': join(ROOT, 'src', 'mcp', 'code_mode_server.py'),
351
+ 'memory_mcp_server.py': join(ROOT, 'src', 'mcp', 'memory_mcp_server.py'),
352
+ '_pantheon_paths.py': join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
353
+ 'mcp_persistence_server.py': join(ROOT, 'src', 'mcp', 'mcp_persistence_server.py'),
354
+ 'pantheon_vision_server.py': join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py'),
355
+ }
319
356
  const dstScriptsDir = join(runtimeTarget, 'scripts')
320
357
  if (!dryRun) mkdirSync(dstScriptsDir, { recursive: true })
321
358
  for (const script of mcpScripts) {
322
- const src = join(srcScriptsDir, script)
359
+ const src = canonicalMcpScripts[script] || join(srcScriptsDir, script)
323
360
  if (!existsSync(src)) {
324
361
  warning(`Script not found: ${src}`)
325
362
  stats.errors++
@@ -341,6 +378,18 @@ export async function installOpenCode(
341
378
  } else stats.skipped++
342
379
  }
343
380
 
381
+ // Keep Vision's dependency contract available to the installed runtime.
382
+ const visionRequirements = join(ROOT, 'src', 'mcp', 'requirements-vision.txt')
383
+ if (existsSync(visionRequirements)) {
384
+ const destination = join(runtimeTarget, 'requirements-vision.txt')
385
+ const status = writeIfChanged(destination, readFileSync(visionRequirements, 'utf8'), dryRun)
386
+ if (status === 'created') stats.created++
387
+ else stats.skipped++
388
+ } else {
389
+ warning(`Requirements file not found: ${visionRequirements}`)
390
+ stats.errors++
391
+ }
392
+
344
393
  // ── Code-mode scripts ──
345
394
  const srcCodeModeDir = join(ROOT, '.pantheon', 'code-mode')
346
395
  const dstCodeModeDir = join(runtimeTarget, '.pantheon', 'code-mode')
@@ -519,15 +568,41 @@ export async function installOpenCode(
519
568
  if (!config.default_agent && pantheonConfig.default_agent) {
520
569
  config.default_agent = pantheonConfig.default_agent
521
570
  }
571
+ // Merge subagent_depth HERE (the config merge always runs, including on a
572
+ // fresh install) instead of a file-existence-gated block, so run #1 already
573
+ // produces the final config — run #2 must be a byte-identical no-op (#19).
574
+ if (config.subagent_depth === undefined) {
575
+ config.subagent_depth = 2
576
+ }
577
+
578
+ // --------------------------------------------------------------------
579
+ // B.5.1 Hermetic plugin resolution (packaging fix)
580
+ // --------------------------------------------------------------------
581
+ // The packaged opencode.json may reference the pantheon-hooks plugin via a
582
+ // developer-machine absolute path (e.g. /home/<dev>/pantheon/src/plugins/...).
583
+ // Copying that verbatim into the user's config would break every global
584
+ // install on any other machine. Option A: rewrite the entry to the plugin
585
+ // file inside the INSTALLED package (derived from ROOT — never hardcoded),
586
+ // so both dev and global installs resolve to a path that actually exists.
587
+ // The source opencode.json keeps its dev path for local development; the
588
+ // transform happens at install/sync time only.
589
+ function resolveInstalledPlugin(plugin) {
590
+ const file = basename(plugin)
591
+ const packaged = join(ROOT, 'src', 'plugins', file)
592
+ return existsSync(packaged) ? packaged : plugin
593
+ }
522
594
 
523
595
  if (!Array.isArray(config.plugin)) {
524
596
  config.plugin = []
525
597
  }
526
598
  if (Array.isArray(pantheonConfig.plugin)) {
527
599
  for (const plugin of pantheonConfig.plugin) {
528
- if (!config.plugin.includes(plugin)) {
529
- config.plugin.push(plugin)
530
- }
600
+ const resolved = resolveInstalledPlugin(plugin)
601
+ const file = basename(resolved)
602
+ // Replace any pre-existing entry for the same plugin file (e.g. a stale
603
+ // dev-machine path from an earlier install) so upgrades stay hermetic.
604
+ config.plugin = config.plugin.filter((p) => basename(p) !== file)
605
+ config.plugin.push(resolved)
531
606
  }
532
607
  }
533
608
 
@@ -604,10 +679,16 @@ export async function installOpenCode(
604
679
  // --------------------------------------------------------------------
605
680
  if (componentSet.has('runtime')) {
606
681
  config.mcp = config.mcp || {}
682
+ // P1-3: derive the MCP python from the SAME venv layout setupVenv
683
+ // creates (<target>/.venv via venvPythonPath). For project installs the
684
+ // runtime payload lives under <target>/.opencode (runtimeTarget), but the
685
+ // venv is created at <target>/.venv — pointing MCP commands at
686
+ // runtimeTarget/.venv would reference an executable that never exists.
607
687
  const runtimeTarget = isGlobal ? target : join(target, '.opencode')
608
- const pythonPath = process.platform === 'win32' ? 'python' : 'python3'
609
- const venvPython = join(runtimeTarget, '.venv', 'bin', 'python3')
610
- const memoryPython = existsSync(venvPython) ? venvPython : pythonPath
688
+ const venvPython = venvPythonPath(target)
689
+ // Point the config at the venv even on a first install; setupVenv runs
690
+ // later in this function and creates this path before OpenCode starts.
691
+ const memoryPython = venvPython
611
692
 
612
693
  // Only add if not already configured by user
613
694
  if (!config.mcp['pantheon-resources']) {
@@ -642,6 +723,14 @@ export async function installOpenCode(
642
723
  enabled: true,
643
724
  }
644
725
  }
726
+ if (!config.mcp['pantheon-vision']) {
727
+ config.mcp['pantheon-vision'] = {
728
+ type: 'local',
729
+ cwd: runtimeTarget,
730
+ command: [memoryPython, 'scripts/pantheon_vision_server.py'],
731
+ enabled: true,
732
+ }
733
+ }
645
734
 
646
735
  // Default MCP permissions
647
736
  config.permission = config.permission || {}
@@ -658,6 +747,9 @@ export async function installOpenCode(
658
747
  if (!config.permission.mcp['pantheon-persistence']) {
659
748
  config.permission.mcp['pantheon-persistence'] = 'allow'
660
749
  }
750
+ if (!config.permission.mcp['pantheon-vision']) {
751
+ config.permission.mcp['pantheon-vision'] = 'ask'
752
+ }
661
753
  }
662
754
 
663
755
  const configContent = `${JSON.stringify(config, null, 2)}\n`
@@ -707,7 +799,7 @@ export async function installOpenCode(
707
799
  }
708
800
  } catch (err) {
709
801
  error(`Setup failed: ${err.message}`)
710
- throw err // Fatal — abort installation
802
+ throw err // Fatal — abort installation
711
803
  }
712
804
  }
713
805
 
@@ -718,18 +810,38 @@ export async function installOpenCode(
718
810
 
719
811
  process.stdout.write('\n')
720
812
  process.stdout.write(` ${colors.bold(colors.green('Installation complete'))}\n`)
721
- process.stdout.write(` ${colors.dim('\u2500'.repeat(Math.min(process.stdout.columns || 60, 60)))}\n`)
813
+ process.stdout.write(
814
+ ` ${colors.dim('\u2500'.repeat(Math.min(process.stdout.columns || 60, 60)))}\n`,
815
+ )
722
816
  process.stdout.write(` ${colors.green('\u2713')} ${created} component(s) installed\n`)
723
- if (skipped > 0) process.stdout.write(` ${colors.dim('\u2014')} ${skipped} already up-to-date\n`)
724
- if (errors > 0) process.stdout.write(` ${colors.yellow('\u26a0')} ${errors} error(s) encountered\n`)
817
+ if (skipped > 0)
818
+ process.stdout.write(` ${colors.dim('\u2014')} ${skipped} already up-to-date\n`)
819
+ if (errors > 0)
820
+ process.stdout.write(` ${colors.yellow('\u26a0')} ${errors} error(s) encountered\n`)
725
821
  process.stdout.write('\n')
726
822
  process.stdout.write(` ${colors.bold('Next steps:')}\n`)
727
823
  process.stdout.write(` ${colors.dim('\u2022')} Configure agents in opencode.json\n`)
728
824
  process.stdout.write(` ${colors.dim('\u2022')} Add MCP servers in mcp.json\n`)
729
825
  process.stdout.write(` ${colors.dim('\u2022')} Run 'opencode doctor' to verify\n`)
730
- process.stdout.write(` ${colors.dim('\u2022')} Run 'opencode' to start using Pantheon agents\n`)
826
+ process.stdout.write(
827
+ ` ${colors.dim('\u2022')} Run 'opencode' to start using Pantheon agents\n`,
828
+ )
731
829
  process.stdout.write('\n')
732
830
  } else {
733
831
  printSummary(target, ['opencode'], stats)
734
832
  }
833
+
834
+ // -----------------------------------------------------------------------
835
+ // 5. Model preset selection (--components agents). Interactive picker runs
836
+ // unless autoYes (use defaults) or an explicit --preset was given.
837
+ // -----------------------------------------------------------------------
838
+ if (interactive && !dryRun && !autoYes && !opts.preset) {
839
+ const { runModelPicker } = await import('./model-picker.mjs')
840
+ await runModelPicker({ presetDir: target, logger: console })
841
+ }
842
+
843
+ if (opts.preset && !dryRun) {
844
+ const { writeActivePreset } = await import('./model-picker.mjs')
845
+ writeActivePreset(target, opts.preset, { source: 'cli' })
846
+ }
735
847
  }
@@ -128,8 +128,10 @@ export function unregisterPlugin(tuiJsonPath, pluginId, { dryRun = false } = {})
128
128
  if (!Array.isArray(tuiConfig.plugin)) {
129
129
  tuiConfig.plugin = []
130
130
  }
131
- // Remove stale plugin refs (old dist/tui.tsx path)
132
- const staleRefs = [`${pluginId}/dist/tui.tsx`, `${pluginId}/dist/tui.js`]
131
+ // Remove stale refs: the bare relative id (openCode < 1.19 wrote
132
+ // "plugins/pantheon-tui", which its tui loader misreads as an npm/github
133
+ // spec → NpmInstallFailedError) and the old dist paths.
134
+ const staleRefs = [pluginId, `${pluginId}/dist/tui.tsx`, `${pluginId}/dist/tui.js`]
133
135
  for (const stale of staleRefs) {
134
136
  const idx = tuiConfig.plugin.indexOf(stale)
135
137
  if (idx !== -1) tuiConfig.plugin.splice(idx, 1)
@@ -17,6 +17,25 @@ import { fileURLToPath } from 'node:url'
17
17
  const __dirname = dirname(fileURLToPath(import.meta.url))
18
18
  const ROOT = join(__dirname, '..', '..')
19
19
 
20
+ /**
21
+ * Resolve the python executable of the venv that setupVenv creates for a
22
+ * target. Single source of truth for the venv layout: the venv always lives
23
+ * at <target>/.venv — even for PROJECT installs, where the runtime payload
24
+ * goes to <target>/.opencode. MCP commands and health checks MUST derive
25
+ * their python from here (never from a runtime sub-directory), or a project
26
+ * install would point at <target>/.opencode/.venv/bin/python3, which
27
+ * setupVenv never creates (P1-3).
28
+ *
29
+ * @param {string} target - target installation directory
30
+ * @returns {string} absolute path to the venv python executable
31
+ */
32
+ export function venvPythonPath(target) {
33
+ const venvPath = join(target, '.venv')
34
+ return process.platform === 'win32'
35
+ ? join(venvPath, 'Scripts', 'python.exe')
36
+ : join(venvPath, 'bin', 'python3')
37
+ }
38
+
20
39
  /**
21
40
  * Set up a Python virtual environment and install MCP dependencies.
22
41
  *
@@ -26,7 +45,7 @@ const ROOT = join(__dirname, '..', '..')
26
45
  */
27
46
  export function setupVenv(target, { dryRun = false, skipInstall = false, force = false } = {}) {
28
47
  const venvPath = join(target, '.venv')
29
- const pythonBin = join(venvPath, 'bin', 'python3')
48
+ const pythonBin = venvPythonPath(target)
30
49
 
31
50
  // Step 1: Force recreate venv if --force
32
51
  if (force && existsSync(venvPath)) {
@@ -94,7 +113,11 @@ export function setupVenv(target, { dryRun = false, skipInstall = false, force =
94
113
  if (r2.status !== 0) {
95
114
  throw new Error(
96
115
  'Failed to install MCP dependencies. ' +
97
- 'Try: PIP_USER=0 ' + pip + ' install -r ' + reqFile + ' --break-system-packages'
116
+ 'Try: PIP_USER=0 ' +
117
+ pip +
118
+ ' install -r ' +
119
+ reqFile +
120
+ ' --break-system-packages',
98
121
  )
99
122
  }
100
123
  }