pantheon-opencode 1.3.5-beta.60.efa533f → 1.3.6

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 (75) hide show
  1. package/README.md +38 -6
  2. package/package.json +4 -3
  3. package/scripts/_pantheon_paths.py +0 -1
  4. package/scripts/build-agents-md.mjs +4 -2
  5. package/scripts/code_mode_server.py +307 -21
  6. package/scripts/doctor.mjs +58 -15
  7. package/scripts/hooks/format-multi-language.sh +6 -3
  8. package/scripts/hooks/log-session-start.sh +6 -0
  9. package/scripts/hooks/on-subagent-delegation-start.sh +22 -6
  10. package/scripts/hooks/validate-post-conditions.sh +1 -1
  11. package/scripts/hooks/validate-tool-safety.sh +5 -2
  12. package/scripts/install/agents-md.mjs +3 -1
  13. package/scripts/install/cli-ui.mjs +24 -4
  14. package/scripts/install/interactive.mjs +18 -7
  15. package/scripts/install/migrate-config.mjs +13 -19
  16. package/scripts/install/migrate.mjs +8 -5
  17. package/scripts/install/model-picker.mjs +4 -1
  18. package/scripts/install/opencode.mjs +8 -2
  19. package/scripts/install/shared.mjs +8 -4
  20. package/scripts/install/state.mjs +9 -6
  21. package/scripts/mcp_persistence_server.py +23 -22
  22. package/scripts/mcp_resources_server.py +60 -1
  23. package/scripts/memory_mcp_server.py +80 -4
  24. package/scripts/postinstall.mjs +27 -25
  25. package/scripts/prune.mjs +1 -1
  26. package/scripts/redaction-gate.mjs +3 -1
  27. package/scripts/uninstall.mjs +0 -2
  28. package/scripts/validation-policy.mjs +6 -1
  29. package/scripts/version-check.mjs +9 -4
  30. package/src/agents/aphrodite.md +1 -1
  31. package/src/agents/apollo.md +3 -0
  32. package/src/agents/demeter.md +1 -1
  33. package/src/agents/gaia.md +3 -0
  34. package/src/agents/hephaestus.md +1 -1
  35. package/src/agents/hermes.md +1 -1
  36. package/src/agents/themis.md +4 -0
  37. package/src/agents/zeus.md +5 -1
  38. package/src/mcp/code_mode_server.py +304 -18
  39. package/src/mcp/eval_store.py +233 -0
  40. package/src/mcp/mcp_persistence_server.py +23 -22
  41. package/src/mcp/mcp_resources_server.py +58 -0
  42. package/src/mcp/memory_mcp_server.py +80 -3
  43. package/src/pantheon/auto-wake.ts +2 -4
  44. package/src/pantheon/background-job-board.ts +17 -4
  45. package/src/pantheon/comment-checker.ts +48 -0
  46. package/src/pantheon/delegation-classifier.ts +307 -0
  47. package/src/pantheon/delegation-enforce.ts +67 -3
  48. package/src/pantheon/delegation-finalize.ts +44 -4
  49. package/src/pantheon/delegation.ts +413 -30
  50. package/src/pantheon/dispatch-guard.ts +2 -0
  51. package/src/pantheon/file-persistence.ts +7 -3
  52. package/src/pantheon/goal-loop.ts +13 -2
  53. package/src/pantheon/permission-eval.ts +284 -0
  54. package/src/pantheon/permission-globs.ts +86 -0
  55. package/src/pantheon/presets.d.mts +41 -0
  56. package/src/pantheon/presets.mjs +135 -0
  57. package/src/pantheon/retry-policy.ts +212 -0
  58. package/src/pantheon/session-guard.ts +48 -0
  59. package/src/pantheon/step-cap.ts +110 -0
  60. package/src/pantheon/todo-enforcer.ts +31 -5
  61. package/src/pantheon/todo-preserve.ts +7 -1
  62. package/src/pantheon/vision.ts +7 -5
  63. package/src/pantheon/zeus-delegate-with-retry.ts +401 -0
  64. package/src/plugin.ts +72 -13
  65. package/src/plugins/hook-runner.ts +1 -1
  66. package/src/plugins/pantheon-hooks.ts +1 -1
  67. package/src/plugins/tui/dist/tui.d.ts +1 -1
  68. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  69. package/src/plugins/tui/dist/tui.js +36 -14
  70. package/src/plugins/tui/dist/tui.tsx +71 -16
  71. package/src/plugins/tui/package.json +1 -1
  72. package/src/plugins/tui/src/index.tsx +71 -16
  73. package/src/plugins/tui/tsdown.config.ts +1 -1
  74. package/src/routing.yml +47 -0
  75. package/src/agents/README.md +0 -26
@@ -187,6 +187,26 @@ function showHelp() {
187
187
  // Helper utilities
188
188
  // ---------------------------------------------------------------------------
189
189
 
190
+ /** @type {Set<string>} agent-defining frontmatter fields */
191
+ const AGENT_FRONTMATTER_FIELDS = new Set(['name', 'description', 'mode'])
192
+
193
+ /**
194
+ * Return true if the file content has YAML frontmatter containing at least
195
+ * one agent-defining field (name, description, or mode). Non-agent .md files
196
+ * such as README.md are excluded.
197
+ * @param {string} content
198
+ * @returns {boolean}
199
+ */
200
+ export function isValidAgentFile(content) {
201
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
202
+ if (!match) return false
203
+ const fm = match[1]
204
+ for (const field of AGENT_FRONTMATTER_FIELDS) {
205
+ if (new RegExp(`(?:^|\\r?\\n)${field}\\s*:`).test(fm)) return true
206
+ }
207
+ return false
208
+ }
209
+
190
210
  /**
191
211
  * Collect canonical agent names from agents/*.md
192
212
  * @returns {string[]} sorted list of agent names (without extension)
@@ -195,7 +215,9 @@ function getCanonicalAgentNames() {
195
215
  if (!existsSync(AGENTS_DIR)) return []
196
216
  return readdirSync(AGENTS_DIR)
197
217
  .filter((f) => f.endsWith('.md'))
198
- .map((f) => f.replace(/\.agent\.md$/, ''))
218
+ .map((f) => ({ name: f, path: join(AGENTS_DIR, f) }))
219
+ .filter(({ path: p }) => isValidAgentFile(readFileSync(p, 'utf8')))
220
+ .map(({ name }) => name.replace(/\.agent\.md$/, ''))
199
221
  .sort()
200
222
  }
201
223
 
@@ -329,30 +351,41 @@ function checkAgentFiles(args) {
329
351
  // Check B: MCP Configuration
330
352
  // ---------------------------------------------------------------------------
331
353
 
354
+ /** Resolve the effective OpenCode user config directory. */
355
+ export function resolveOpenCodeConfigDir(env = process.env) {
356
+ if (env.PANTHEON_HOME) return resolve(env.PANTHEON_HOME)
357
+ const base = env.XDG_CONFIG_HOME || join(env.HOME ?? homedir(), '.config')
358
+ return join(base, 'opencode')
359
+ }
360
+
332
361
  /**
333
362
  * Collect every reachable opencode.json (project → repository → user config)
334
363
  * with its parsed content, for the config, spawn-path and runtime layers.
335
364
  * @returns {{label:string, path:string, data:object}[]}
336
365
  */
337
- function collectMcpConfigs(args) {
366
+ export function collectMcpConfigs(args) {
338
367
  const candidates = [
339
368
  { label: 'project root', path: join(args.target, 'opencode.json') },
340
369
  { label: 'repository root', path: join(ROOT, 'opencode.json') },
341
370
  ]
342
371
 
343
- // Also check user config if target matches Pantheon ROOT
344
- if (args.target === ROOT) {
345
- candidates.push({
346
- label: 'user config',
347
- path: join(ROOT, '.opencode', 'opencode.json'),
348
- })
349
- } else {
350
- const homeConfig = join(process.env.HOME ?? '~', '.config', 'opencode', 'opencode.json')
351
- candidates.push({ label: 'user config', path: homeConfig })
352
- }
372
+ // OpenCode resolves user configuration independently of cwd. Keep the
373
+ // project-local .opencode config for repository checks, but always include
374
+ // the effective user config so a sandbox HOME is not mistaken for cwd.
375
+ candidates.push({
376
+ label: 'project .opencode config',
377
+ path: join(args.target, '.opencode', 'opencode.json'),
378
+ })
379
+ candidates.push({
380
+ label: 'user config',
381
+ path: join(resolveOpenCodeConfigDir(args.env ?? process.env), 'opencode.json'),
382
+ })
353
383
 
354
384
  const configs = []
385
+ const seen = new Set()
355
386
  for (const c of candidates) {
387
+ if (seen.has(c.path)) continue
388
+ seen.add(c.path)
356
389
  const data = readJson(c.path)
357
390
  if (data) {
358
391
  configs.push({ label: c.label, data, path: c.path })
@@ -369,7 +402,9 @@ function checkMcpConfig(args) {
369
402
 
370
403
  if (configs.length === 0) {
371
404
  if (requiredMcpNames.length > 0) {
372
- error(`No opencode.json found; required MCPs missing: ${requiredMcpNames.join(', ')}`)
405
+ error(
406
+ `No opencode.json found (searched project/repository and ${resolveOpenCodeConfigDir(args.env ?? process.env)}); required MCPs missing: ${requiredMcpNames.join(', ')}`,
407
+ )
373
408
  } else {
374
409
  info(`No opencode.json found for ${args.profile} profile — MCP check skipped`)
375
410
  }
@@ -928,7 +963,7 @@ function checkRequirementLine(line, installed) {
928
963
  function checkVenvLayer(args) {
929
964
  section('F. Runtime Layer — Venv, Python & Dependencies')
930
965
 
931
- const configDir = join(homedir(), '.config', 'opencode')
966
+ const configDir = resolveOpenCodeConfigDir()
932
967
  const venvPython = join(configDir, '.venv', 'bin', 'python3')
933
968
 
934
969
  if (!existsSync(venvPython)) {
@@ -1125,7 +1160,15 @@ export function deriveInstalledAgentFiles(configs) {
1125
1160
  continue
1126
1161
  }
1127
1162
  for (const entry of entries) {
1128
- if (entry.isFile() && entry.name.endsWith('.md')) files.push(join(directory, entry.name))
1163
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue
1164
+ const filePath = join(directory, entry.name)
1165
+ try {
1166
+ if (isValidAgentFile(readFileSync(filePath, 'utf8'))) {
1167
+ files.push(filePath)
1168
+ }
1169
+ } catch {
1170
+ // unreadable — skip
1171
+ }
1129
1172
  }
1130
1173
  }
1131
1174
  return [...new Set(files)].sort()
@@ -16,9 +16,12 @@ import sys, json
16
16
  d = json.load(sys.stdin)
17
17
  print(d.get('tool_name', ''))
18
18
  " 2>/dev/null || echo "")
19
+ # Normalize to lowercase — opencode sends lowercase (edit/write/bash),
20
+ # Claude Code protocol may send capitalized (Edit/Write/Bash).
21
+ TOOL_NAME_LOWER=$(echo "$TOOL_NAME" | tr '[:upper:]' '[:lower:]')
19
22
 
20
- case "$TOOL_NAME" in
21
- Edit|Write)
23
+ case "$TOOL_NAME_LOWER" in
24
+ edit|write)
22
25
  JSON_PATH=$(echo "$STDIN_JSON" | python3 -c "
23
26
  import sys, json
24
27
  d = json.load(sys.stdin)
@@ -28,7 +31,7 @@ print(d.get('tool_input', {}).get('file_path', ''))
28
31
  FILE_PATH="$JSON_PATH"
29
32
  fi
30
33
  ;;
31
- Bash)
34
+ bash)
32
35
  COMMAND=$(echo "$STDIN_JSON" | python3 -c "
33
36
  import sys, json
34
37
  d = json.load(sys.stdin)
@@ -4,10 +4,16 @@
4
4
  set -euo pipefail
5
5
 
6
6
  # Project-local by default; set XDG_STATE_HOME for system-wide logging
7
+ # Anchor to PANTHEON_HOME or PANTHEON_PROJECT when available to avoid
8
+ # CWD-dependent relative path ("logs/agent-sessions" wrote to cwd).
7
9
  if [ -n "${LOG_DIR:-}" ]; then
8
10
  LOG_DIR="$LOG_DIR"
9
11
  elif [ -n "${XDG_STATE_HOME:-}" ]; then
10
12
  LOG_DIR="$XDG_STATE_HOME/pantheon/hooks"
13
+ elif [ -n "${PANTHEON_HOME:-}" ]; then
14
+ LOG_DIR="$PANTHEON_HOME/logs/agent-sessions"
15
+ elif [ -n "${PANTHEON_PROJECT:-}" ]; then
16
+ LOG_DIR="$PANTHEON_PROJECT/logs/agent-sessions"
11
17
  else
12
18
  LOG_DIR="logs/agent-sessions"
13
19
  fi
@@ -24,10 +24,16 @@
24
24
  set -euo pipefail
25
25
 
26
26
  # Project-local by default; set XDG_STATE_HOME for system-wide logging
27
+ # Anchor to PANTHEON_HOME or PANTHEON_PROJECT when available to avoid
28
+ # CWD-dependent relative path ("logs/agent-sessions" wrote to cwd).
27
29
  if [ -n "${LOG_DIR:-}" ]; then
28
30
  LOG_DIR="$LOG_DIR"
29
31
  elif [ -n "${XDG_STATE_HOME:-}" ]; then
30
32
  LOG_DIR="$XDG_STATE_HOME/pantheon/hooks"
33
+ elif [ -n "${PANTHEON_HOME:-}" ]; then
34
+ LOG_DIR="$PANTHEON_HOME/logs/agent-sessions"
35
+ elif [ -n "${PANTHEON_PROJECT:-}" ]; then
36
+ LOG_DIR="$PANTHEON_PROJECT/logs/agent-sessions"
31
37
  else
32
38
  LOG_DIR="logs/agent-sessions"
33
39
  fi
@@ -133,20 +139,30 @@ echo "[DELEGATION] $AGENT_NAME started → $LOG_DIR/delegations.log" >&2
133
139
 
134
140
  # --- Dispatch-time validation: check target against routing.yml ---
135
141
  SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
136
- REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
137
- ROUTING_FILE="$REPO_ROOT/routing.yml"
138
- if [ -f "$ROUTING_FILE" ] && [ -n "$AGENT_NAME" ] && [ "$AGENT_NAME" != "unknown" ]; then
142
+ REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." 2>/dev/null && pwd)"
143
+
144
+ # Find routing.yml: try repo root first, then PANTHEON_HOME, then PANTHEON_PROJECT.
145
+ # Silently skip if none found (deployed users may not have routing.yml locally).
146
+ ROUTING_FILE=""
147
+ if [ -n "$REPO_ROOT" ] && [ -f "$REPO_ROOT/routing.yml" ]; then
148
+ ROUTING_FILE="$REPO_ROOT/routing.yml"
149
+ elif [ -n "${PANTHEON_HOME:-}" ] && [ -f "$PANTHEON_HOME/routing.yml" ]; then
150
+ ROUTING_FILE="$PANTHEON_HOME/routing.yml"
151
+ elif [ -n "${PANTHEON_PROJECT:-}" ] && [ -f "$PANTHEON_PROJECT/routing.yml" ]; then
152
+ ROUTING_FILE="$PANTHEON_PROJECT/routing.yml"
153
+ fi
154
+
155
+ if [ -n "$ROUTING_FILE" ] && [ -n "$AGENT_NAME" ] && [ "$AGENT_NAME" != "unknown" ]; then
139
156
  ESCAPED_AGENT_NAME=$(printf '%s\n' "$AGENT_NAME" | sed 's/[][(){}.^$*+?|\/\\-]/\\&/g')
140
157
  if grep -qE "^[[:space:]]*-[[:space:]]+target:[[:space:]]+$ESCAPED_AGENT_NAME[[:space:]]*$" "$ROUTING_FILE" 2>/dev/null; then
141
158
  echo "[VALIDATION] ✅ Target '$AGENT_NAME' found in routing.yml" >&2
142
159
  else
143
160
  echo "[VALIDATION] ⚠️ Target '$AGENT_NAME' NOT in routing.yml delegation rules" >&2
144
161
  fi
145
- elif [ -f "$ROUTING_FILE" ]; then
162
+ elif [ -n "$ROUTING_FILE" ]; then
146
163
  echo "[VALIDATION] ⚠️ Could not identify the delegated agent (agent='$AGENT_NAME') — skipping routing.yml validation" >&2
147
- elif [ ! -f "$ROUTING_FILE" ]; then
148
- echo "[VALIDATION] ⚠️ Could not read routing.yml for validation" >&2
149
164
  fi
165
+ # else: no routing.yml found — silently skip (deployed users may not have it locally)
150
166
 
151
167
  # --- Cost estimation for delegation ---
152
168
  declare -A TIER_MAP
@@ -9,7 +9,7 @@ SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.st
9
9
 
10
10
  REVIEW_DIR=".pantheon/memory-bank/.tmp"
11
11
  if [ -d "$REVIEW_DIR" ]; then
12
- REVIEW_COUNT=$(ls "$REVIEW_DIR"/REVIEW-* 2>/dev/null | wc -l)
12
+ REVIEW_COUNT=$(find "$REVIEW_DIR" -maxdepth 1 -name 'REVIEW-*' -type f 2>/dev/null | wc -l)
13
13
  if [ "$REVIEW_COUNT" -eq 0 ]; then
14
14
  echo "[POST-CONDITION] ⚠️ No REVIEW artifact found in $REVIEW_DIR/" >&2
15
15
  echo "[POST-CONDITION] 💡 Remember: implementation agents MUST call @themis for review" >&2
@@ -18,8 +18,11 @@ BLOCKED_PATTERNS=(
18
18
  "DROP\s+DATABASE"
19
19
  "TRUNCATE\s+TABLE"
20
20
  "DELETE\s+FROM\s+.*;?\s*$"
21
- "rm\s+.*\s+--no-preserve-root"
22
- ":(){ :|:& };:"
21
+ # DEAD PATTERN REMOVED: `rm\s+.*\s+--no-preserve-root` — redundant with
22
+ # pattern #1 which already blocks `rm -rf /` regardless of flags.
23
+ # DEAD PATTERN REMOVED: `:(){ :|:& };:` — fork bomb syntax never appears
24
+ # in real tool_input.command payloads (the entire stdin would need to be
25
+ # exactly this 13-char token with zero surrounding JSON/context).
23
26
  ">\s*/dev/\(sd[a-z]\|hd[a-z]\|disk[0-9]*\)"
24
27
  "dd\s+if=.*of=/dev/"
25
28
  "mkfs\."
@@ -76,7 +76,9 @@ export function readCanonicalAgents() {
76
76
  if (!existsSync(AGENTS_DIR)) return []
77
77
 
78
78
  const files = readdirSync(AGENTS_DIR)
79
- .filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
79
+ .filter(
80
+ (f) => (f.endsWith('.agent.md') || f.endsWith('.md')) && f.toLowerCase() !== 'readme.md',
81
+ )
80
82
  .sort()
81
83
 
82
84
  const agents = []
@@ -95,22 +95,42 @@ export function summaryTable(items) {
95
95
  const bot = `\u2514${'\u2500'.repeat(nameWidth + 2)}\u2534${'\u2500'.repeat(statusWidth + 2)}\u2518`
96
96
 
97
97
  console.log(tag(top))
98
- console.log(tag(`\u2502 ${'Componente'.padEnd(nameWidth)} \u2502 ${'Status'.padEnd(statusWidth)} \u2502`))
98
+ console.log(
99
+ tag(`\u2502 ${'Componente'.padEnd(nameWidth)} \u2502 ${'Status'.padEnd(statusWidth)} \u2502`),
100
+ )
99
101
  console.log(tag(sep))
100
102
  for (const item of items) {
101
- console.log(tag(`\u2502 ${item.name.padEnd(nameWidth)} \u2502 ${String(item.status).padEnd(statusWidth)} \u2502`))
103
+ console.log(
104
+ tag(
105
+ `\u2502 ${item.name.padEnd(nameWidth)} \u2502 ${String(item.status).padEnd(statusWidth)} \u2502`,
106
+ ),
107
+ )
102
108
  }
103
109
  console.log(tag(bot))
104
110
  }
105
111
 
106
112
  export function spinner(message) {
107
- const frames = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
113
+ const frames = [
114
+ '\u280b',
115
+ '\u2819',
116
+ '\u2839',
117
+ '\u2838',
118
+ '\u283c',
119
+ '\u2834',
120
+ '\u2826',
121
+ '\u2827',
122
+ '\u2807',
123
+ '\u280f',
124
+ ]
108
125
  let i = 0
109
126
  let running = true
110
127
 
111
128
  if (tty() && !_quiet) {
112
129
  const interval = setInterval(() => {
113
- if (!running) { clearInterval(interval); return }
130
+ if (!running) {
131
+ clearInterval(interval)
132
+ return
133
+ }
114
134
  process.stdout.write(`\r${frames[i % frames.length]} ${tag(message)}...`)
115
135
  i++
116
136
  }, 80)
@@ -31,7 +31,10 @@ function teardownStdin() {
31
31
  }
32
32
  }
33
33
 
34
- export async function promptMultiSelect(options, { title = '', selected = new Set(), pageSize = 6 } = {}) {
34
+ export async function promptMultiSelect(
35
+ options,
36
+ { title = '', selected = new Set(), pageSize = 6 } = {},
37
+ ) {
35
38
  if (options.length === 0) return new Set()
36
39
 
37
40
  const preSelected = selected instanceof Set ? selected : new Set(selected)
@@ -83,7 +86,11 @@ export async function promptMultiSelect(options, { title = '', selected = new Se
83
86
  const count = items.filter((i) => i.checked).length
84
87
  const total = items.length
85
88
  const noun = count === 1 ? 'selected' : 'selected'
86
- lines.push(colors.dim(`(${count}/${total} ${noun}) — ${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Space')}${colors.dim(' toggle · ')}${colors.cyan('Enter')}${colors.dim(' confirm')}`))
89
+ lines.push(
90
+ colors.dim(
91
+ `(${count}/${total} ${noun}) — ${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Space')}${colors.dim(' toggle · ')}${colors.cyan('Enter')}${colors.dim(' confirm')}`,
92
+ ),
93
+ )
87
94
 
88
95
  const lineCount = lines.length
89
96
  if (previousLineCount > 0) cursorUp(previousLineCount)
@@ -247,7 +254,11 @@ export async function promptList(options, { title = '' } = {}) {
247
254
 
248
255
  if (hasMoreBelow) lines.push(colors.dim(' ...'))
249
256
 
250
- lines.push(colors.dim(`(${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Enter')}${colors.dim(' select')}`))
257
+ lines.push(
258
+ colors.dim(
259
+ `(${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Enter')}${colors.dim(' select')}`,
260
+ ),
261
+ )
251
262
 
252
263
  const lineCount = lines.length
253
264
  if (previousLineCount > 0) cursorUp(previousLineCount)
@@ -307,9 +318,7 @@ export async function promptList(options, { title = '' } = {}) {
307
318
  }
308
319
 
309
320
  export async function runInteractiveInstall({ components = [], defaultComponents = [] } = {}) {
310
- const defaultSet = new Set(
311
- defaultComponents.length > 0 ? defaultComponents : components,
312
- )
321
+ const defaultSet = new Set(defaultComponents.length > 0 ? defaultComponents : components)
313
322
 
314
323
  const componentOptions = [
315
324
  {
@@ -362,7 +371,9 @@ export async function runInteractiveInstall({ components = [], defaultComponents
362
371
 
363
372
  if (selectedComponents.size === 0) {
364
373
  process.stdout.write(`${colors.yellow('No components selected — using defaults\n')}`)
365
- defaultSet.forEach((v) => selectedComponents.add(v))
374
+ defaultSet.forEach((v) => {
375
+ selectedComponents.add(v)
376
+ })
366
377
  }
367
378
 
368
379
  const targetOptions = [
@@ -16,20 +16,20 @@
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
17
17
  import { join } from 'node:path'
18
18
  import {
19
- readState,
20
- writeState,
21
- createInitialState,
22
- addMigrationRecord,
23
- updateVersion,
24
- } from './state.mjs'
25
- import {
26
- readCheckpoint,
27
- writeCheckpoint,
28
19
  advanceCheckpoint,
20
+ listAppliedMigrations,
21
+ readCheckpoint,
29
22
  removeCheckpoint,
30
23
  resumePendingMigration,
31
- listAppliedMigrations,
24
+ writeCheckpoint,
32
25
  } from './migrate.mjs'
26
+ import {
27
+ addMigrationRecord,
28
+ createInitialState,
29
+ readState,
30
+ updateVersion,
31
+ writeState,
32
+ } from './state.mjs'
33
33
 
34
34
  // ---------------------------------------------------------------------------
35
35
  // Migration step definitions
@@ -290,16 +290,10 @@ function main() {
290
290
  addMigrationRecord(state, config.from, config.to, 'completed')
291
291
  removeCheckpoint(config.target)
292
292
  writeState(config.target, state)
293
- console.log(
294
- `\n \u2705 Migration complete: ${config.from} \u2192 ${config.to}`,
295
- )
296
- console.log(
297
- ` State written to: ${join(config.target, '.pantheon', 'install-state.json')}`,
298
- )
293
+ console.log(`\n \u2705 Migration complete: ${config.from} \u2192 ${config.to}`)
294
+ console.log(` State written to: ${join(config.target, '.pantheon', 'install-state.json')}`)
299
295
  } else {
300
- console.log(
301
- `\n \u2705 Migration would complete (dry-run): ${config.from} \u2192 ${config.to}`,
302
- )
296
+ console.log(`\n \u2705 Migration would complete (dry-run): ${config.from} \u2192 ${config.to}`)
303
297
  }
304
298
  }
305
299
 
@@ -11,7 +11,7 @@
11
11
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
12
12
  import { dirname, join } from 'node:path'
13
13
  import { fileURLToPath } from 'node:url'
14
- import { readState, writeState, createInitialState } from './state.mjs'
14
+ import { createInitialState, readState, writeState } from './state.mjs'
15
15
 
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url))
17
17
  const ROOT = join(__dirname, '..', '..')
@@ -243,9 +243,7 @@ export function rollbackMigration(target, version) {
243
243
  }
244
244
 
245
245
  const migrations = state.applied_migrations || []
246
- const targetMigration = migrations.find(
247
- (m) => m.to === version && m.status === 'completed',
248
- )
246
+ const targetMigration = migrations.find((m) => m.to === version && m.status === 'completed')
249
247
  if (!targetMigration) {
250
248
  console.log(` \u26A0\uFE0F No completed migration found for version ${version}`)
251
249
  return { success: false, reason: 'Migration not found' }
@@ -341,7 +339,12 @@ function main() {
341
339
  console.log(` Previous version: ${history.previousVersion || 'N/A'}`)
342
340
  console.log(` Applied migrations: ${history.migrations.length}`)
343
341
  for (const m of history.migrations) {
344
- const icon = m.status === 'completed' ? '\u2705' : m.status === 'rolled_back' ? '\u{1F504}' : '\u26A0\uFE0F'
342
+ const icon =
343
+ m.status === 'completed'
344
+ ? '\u2705'
345
+ : m.status === 'rolled_back'
346
+ ? '\u{1F504}'
347
+ : '\u26A0\uFE0F'
345
348
  console.log(` ${icon} ${m.from} \u2192 ${m.to} [${m.status}] (${m.applied_at})`)
346
349
  }
347
350
  return
@@ -55,7 +55,10 @@ export function writeActivePreset(
55
55
  copyFileSync(filePath, backupPath)
56
56
  backupMade = true
57
57
  }
58
- writeFileSync(`${filePath}.tmp-${process.pid}`, `${JSON.stringify(buildActivePresetFile(presetName, source), null, 2)}\n`)
58
+ writeFileSync(
59
+ `${filePath}.tmp-${process.pid}`,
60
+ `${JSON.stringify(buildActivePresetFile(presetName, source), null, 2)}\n`,
61
+ )
59
62
  renameSync(`${filePath}.tmp-${process.pid}`, filePath)
60
63
  return { written: true, path: filePath, backupPath: backupMade ? backupPath : null }
61
64
  }
@@ -452,6 +452,7 @@ export async function installOpenCode(
452
452
  '_pantheon_paths.py',
453
453
  'mcp_persistence_server.py',
454
454
  'pantheon_vision_server.py',
455
+ 'eval_store.py',
455
456
  ]
456
457
  const srcScriptsDir = join(ROOT, 'scripts')
457
458
  // Canonical MCP server sources live in src/mcp/. Map them explicitly so the
@@ -466,6 +467,7 @@ export async function installOpenCode(
466
467
  '_pantheon_paths.py': join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
467
468
  'mcp_persistence_server.py': join(ROOT, 'src', 'mcp', 'mcp_persistence_server.py'),
468
469
  'pantheon_vision_server.py': join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py'),
470
+ 'eval_store.py': join(ROOT, 'src', 'mcp', 'eval_store.py'),
469
471
  }
470
472
  const dstScriptsDir = join(runtimeTarget, 'scripts')
471
473
  if (!dryRun) mkdirSync(dstScriptsDir, { recursive: true })
@@ -558,7 +560,9 @@ export async function installOpenCode(
558
560
  const agentsDir = join(ROOT, 'src', 'agents')
559
561
  if (!existsSync(agentsDir)) return {}
560
562
  const sources = {}
561
- const files = readdirSync(agentsDir).filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
563
+ const files = readdirSync(agentsDir).filter(
564
+ (f) => (f.endsWith('.agent.md') || f.endsWith('.md')) && f.toLowerCase() !== 'readme.md',
565
+ )
562
566
  for (const f of files) {
563
567
  const name = f.replace(/\.(agent\.)?md$/, '')
564
568
  sources[name] = `${agentPrefix}/${name}.md`
@@ -570,7 +574,9 @@ export async function installOpenCode(
570
574
  const agentsDir = join(ROOT, 'src', 'agents')
571
575
  if (!existsSync(agentsDir)) return {}
572
576
  const config = {}
573
- const files = readdirSync(agentsDir).filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
577
+ const files = readdirSync(agentsDir).filter(
578
+ (f) => (f.endsWith('.agent.md') || f.endsWith('.md')) && f.toLowerCase() !== 'readme.md',
579
+ )
574
580
  for (const f of files) {
575
581
  const name = f.replace(/\.(agent\.)?md$/, '')
576
582
  const content = readFileSync(join(agentsDir, f), 'utf8')
@@ -15,10 +15,10 @@ import {
15
15
  import { basename, dirname, join } from 'node:path'
16
16
  import { fileURLToPath } from 'node:url'
17
17
 
18
- const REQUIRED_NODE_MAJOR = 18;
18
+ const REQUIRED_NODE_MAJOR = 18
19
19
  if (parseInt(process.versions.node.split('.')[0], 10) < REQUIRED_NODE_MAJOR) {
20
- console.error(`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`);
21
- process.exit(1);
20
+ console.error(`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`)
21
+ process.exit(1)
22
22
  }
23
23
 
24
24
  import yaml from 'js-yaml'
@@ -33,7 +33,9 @@ export const PLATFORM_DIR = join(ROOT, 'platform')
33
33
  export function getAgentNames() {
34
34
  if (!existsSync(AGENTS_DIR)) return []
35
35
  return readdirSync(AGENTS_DIR)
36
- .filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
36
+ .filter(
37
+ (f) => (f.endsWith('.agent.md') || f.endsWith('.md')) && f.toLowerCase() !== 'readme.md',
38
+ )
37
39
  .map((f) => f.replace(/\.(agent\.)?md$/, ''))
38
40
  .sort()
39
41
  }
@@ -277,9 +279,11 @@ export function copyFiles(srcDir, dstDir, dryRun, renameMap = null, clean = fals
277
279
  // Build set of expected destination filenames (after rename)
278
280
  const dstNames = new Set()
279
281
  for (const entry of entries) {
282
+ if (entry.toLowerCase() === 'readme.md') continue
280
283
  const srcFile = join(srcDir, entry)
281
284
  if (!existsSync(srcFile)) continue
282
285
  const dstName = renameMap ? (renameMap(entry) ?? entry) : entry
286
+ if (dstName.toLowerCase() === 'readme.md') continue
283
287
  dstNames.add(dstName)
284
288
 
285
289
  const dstFile = join(dstDir, dstName)
@@ -95,12 +95,15 @@ export function upgradeStateV1toV2(state) {
95
95
  updated_at: state.updated_at || now,
96
96
  migration_checkpoint: null,
97
97
  applied_migrations: Array.isArray(state.applied_migrations) ? state.applied_migrations : [],
98
- components: state.components && typeof state.components === 'object' ? state.components : {
99
- agents: { version: 1, status: 'installed' },
100
- skills: { version: 1, status: 'installed' },
101
- plugins: { version: 1, status: 'unknown' },
102
- runtime: { version: 1, status: 'unknown' },
103
- },
98
+ components:
99
+ state.components && typeof state.components === 'object'
100
+ ? state.components
101
+ : {
102
+ agents: { version: 1, status: 'installed' },
103
+ skills: { version: 1, status: 'installed' },
104
+ plugins: { version: 1, status: 'unknown' },
105
+ runtime: { version: 1, status: 'unknown' },
106
+ },
104
107
  }
105
108
  }
106
109