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.
- package/README.md +38 -6
- package/package.json +4 -3
- package/scripts/_pantheon_paths.py +0 -1
- package/scripts/build-agents-md.mjs +4 -2
- package/scripts/code_mode_server.py +307 -21
- package/scripts/doctor.mjs +58 -15
- package/scripts/hooks/format-multi-language.sh +6 -3
- package/scripts/hooks/log-session-start.sh +6 -0
- package/scripts/hooks/on-subagent-delegation-start.sh +22 -6
- package/scripts/hooks/validate-post-conditions.sh +1 -1
- package/scripts/hooks/validate-tool-safety.sh +5 -2
- package/scripts/install/agents-md.mjs +3 -1
- package/scripts/install/cli-ui.mjs +24 -4
- package/scripts/install/interactive.mjs +18 -7
- package/scripts/install/migrate-config.mjs +13 -19
- package/scripts/install/migrate.mjs +8 -5
- package/scripts/install/model-picker.mjs +4 -1
- package/scripts/install/opencode.mjs +8 -2
- package/scripts/install/shared.mjs +8 -4
- package/scripts/install/state.mjs +9 -6
- package/scripts/mcp_persistence_server.py +23 -22
- package/scripts/mcp_resources_server.py +60 -1
- package/scripts/memory_mcp_server.py +80 -4
- package/scripts/postinstall.mjs +27 -25
- package/scripts/prune.mjs +1 -1
- package/scripts/redaction-gate.mjs +3 -1
- package/scripts/uninstall.mjs +0 -2
- package/scripts/validation-policy.mjs +6 -1
- package/scripts/version-check.mjs +9 -4
- package/src/agents/aphrodite.md +1 -1
- package/src/agents/apollo.md +3 -0
- package/src/agents/demeter.md +1 -1
- package/src/agents/gaia.md +3 -0
- package/src/agents/hephaestus.md +1 -1
- package/src/agents/hermes.md +1 -1
- package/src/agents/themis.md +4 -0
- package/src/agents/zeus.md +5 -1
- package/src/mcp/code_mode_server.py +304 -18
- package/src/mcp/eval_store.py +233 -0
- package/src/mcp/mcp_persistence_server.py +23 -22
- package/src/mcp/mcp_resources_server.py +58 -0
- package/src/mcp/memory_mcp_server.py +80 -3
- package/src/pantheon/auto-wake.ts +2 -4
- package/src/pantheon/background-job-board.ts +17 -4
- package/src/pantheon/comment-checker.ts +48 -0
- package/src/pantheon/delegation-classifier.ts +307 -0
- package/src/pantheon/delegation-enforce.ts +67 -3
- package/src/pantheon/delegation-finalize.ts +44 -4
- package/src/pantheon/delegation.ts +413 -30
- package/src/pantheon/dispatch-guard.ts +2 -0
- package/src/pantheon/file-persistence.ts +7 -3
- package/src/pantheon/goal-loop.ts +13 -2
- package/src/pantheon/permission-eval.ts +284 -0
- package/src/pantheon/permission-globs.ts +86 -0
- package/src/pantheon/presets.d.mts +41 -0
- package/src/pantheon/presets.mjs +135 -0
- package/src/pantheon/retry-policy.ts +212 -0
- package/src/pantheon/session-guard.ts +48 -0
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/todo-enforcer.ts +31 -5
- package/src/pantheon/todo-preserve.ts +7 -1
- package/src/pantheon/vision.ts +7 -5
- package/src/pantheon/zeus-delegate-with-retry.ts +401 -0
- package/src/plugin.ts +72 -13
- package/src/plugins/hook-runner.ts +1 -1
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +36 -14
- package/src/plugins/tui/dist/tui.tsx +71 -16
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +71 -16
- package/src/plugins/tui/tsdown.config.ts +1 -1
- package/src/routing.yml +47 -0
- package/src/agents/README.md +0 -26
package/scripts/doctor.mjs
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
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(
|
|
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 =
|
|
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()
|
|
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 "$
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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 [ -
|
|
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=$(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 = [
|
|
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) {
|
|
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(
|
|
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(
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|