pantheon-opencode 1.4.3 → 1.5.0-beta.10
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/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/manifest.json +21 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/CHANGELOG.md +1026 -0
- package/README.md +292 -1080
- package/README.pt-BR.md +308 -0
- package/bin/pantheon-init.mjs +207 -31
- package/opencode.json +1 -0
- package/package.json +34 -14
- package/plugin.json +15 -0
- package/pyproject.toml +65 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +451 -44
- package/scripts/cost.mjs +30 -14
- package/scripts/doctor.mjs +306 -129
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +23 -10
- package/scripts/install/config-migration.mjs +399 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +396 -121
- package/scripts/install/plugin.mjs +89 -49
- package/scripts/install/shared.mjs +55 -159
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +45 -232
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +798 -60
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/probe-context-rehydrate.mjs +240 -0
- package/scripts/probe-pantheon-cost.mjs +313 -0
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/release-beta-version.mjs +56 -60
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +135 -42
- package/scripts/test-opencode-v1-v2-sandbox.sh +862 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +230 -86
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +27 -16
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +451 -44
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +798 -60
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/background-job-board.ts +110 -2
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/cost-command.ts +356 -69
- package/src/pantheon/delegate-manager.ts +766 -0
- package/src/pantheon/delegation-classifier.ts +307 -0
- package/src/pantheon/delegation-enforce.ts +13 -12
- package/src/pantheon/delegation-finalize.ts +406 -0
- package/src/pantheon/delegation-notify.ts +212 -0
- package/src/pantheon/delegation-toast.ts +73 -0
- package/src/pantheon/delegation.ts +1337 -0
- package/src/pantheon/file-persistence-salvage.ts +64 -0
- package/src/pantheon/file-persistence.ts +194 -45
- package/src/pantheon/goal-loop.ts +2 -2
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/hashline/tool.ts +2 -2
- package/src/pantheon/model-command.ts +2 -1
- package/src/pantheon/native-probe.ts +234 -0
- package/src/pantheon/native-task-status.ts +139 -0
- package/src/pantheon/permission-eval.ts +284 -0
- package/src/pantheon/provider-capabilities.ts +221 -0
- package/src/pantheon/provider-stream-tracker.ts +182 -0
- package/src/pantheon/shared-board.ts +37 -0
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/task-result-guard.ts +107 -0
- package/src/pantheon/token-opt.ts +689 -0
- package/src/pantheon/tool-ceiling.ts +159 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +148 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +824 -0
- package/src/plugin.ts +261 -51
- package/src/plugins/pantheon-hooks.ts +51 -0
- package/src/plugins/tui/dist/tui.d.ts +116 -24
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +416 -149
- package/src/plugins/tui/dist/tui.tsx +456 -87
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +462 -95
- package/src/skills/README.md +0 -2
- package/src/pantheon/tool-context.ts +0 -41
package/scripts/doctor.mjs
CHANGED
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
* doctor.mjs — Pantheon health check CLI tool
|
|
5
5
|
*
|
|
6
6
|
* Validates that a Pantheon installation is working correctly:
|
|
7
|
-
* -
|
|
7
|
+
* - Canonical agent file presence
|
|
8
8
|
* - MCP configuration (gh_grep, Context7, etc.)
|
|
9
9
|
* - Permission/frontmatter mismatches
|
|
10
|
-
* -
|
|
10
|
+
* - OpenCode artifact freshness
|
|
11
11
|
* - Git status
|
|
12
12
|
*
|
|
13
13
|
* Usage:
|
|
14
14
|
* node scripts/doctor.mjs # auto-detect, cwd
|
|
15
15
|
* node scripts/doctor.mjs --target /path/to/project
|
|
16
|
-
* node scripts/doctor.mjs --platform opencode
|
|
17
16
|
* node scripts/doctor.mjs --fix # attempt auto-fixes
|
|
18
17
|
* node scripts/doctor.mjs --verbose
|
|
19
18
|
* node scripts/doctor.mjs --help
|
|
@@ -23,7 +22,8 @@
|
|
|
23
22
|
*/
|
|
24
23
|
|
|
25
24
|
import { spawn as spawnAsync, spawnSync } from 'node:child_process'
|
|
26
|
-
import {
|
|
25
|
+
import { createHash } from 'node:crypto'
|
|
26
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
27
27
|
import { homedir } from 'node:os'
|
|
28
28
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
29
29
|
import { fileURLToPath } from 'node:url'
|
|
@@ -35,15 +35,6 @@ import { fileURLToPath } from 'node:url'
|
|
|
35
35
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
36
36
|
const ROOT = join(__dirname, '..')
|
|
37
37
|
const AGENTS_DIR = join(ROOT, 'src', 'agents')
|
|
38
|
-
const PLATFORM_DIR = join(ROOT, 'platform')
|
|
39
|
-
|
|
40
|
-
// All known platforms (must match platform/ subdirectories with adapter.json)
|
|
41
|
-
const _ALL_PLATFORMS = ['opencode']
|
|
42
|
-
|
|
43
|
-
const PLATFORM_LABELS = {
|
|
44
|
-
opencode: 'OpenCode',
|
|
45
|
-
}
|
|
46
|
-
|
|
47
38
|
// ---------------------------------------------------------------------------
|
|
48
39
|
// State
|
|
49
40
|
// ---------------------------------------------------------------------------
|
|
@@ -73,9 +64,6 @@ function parseArgs(argv) {
|
|
|
73
64
|
case '--target':
|
|
74
65
|
args.target = argv[++i]
|
|
75
66
|
break
|
|
76
|
-
case '--platform':
|
|
77
|
-
args.platform = argv[++i]
|
|
78
|
-
break
|
|
79
67
|
case '--fix':
|
|
80
68
|
args.fix = true
|
|
81
69
|
break
|
|
@@ -157,19 +145,18 @@ function showHelp() {
|
|
|
157
145
|
Usage:
|
|
158
146
|
node scripts/doctor.mjs auto-detect, cwd
|
|
159
147
|
node scripts/doctor.mjs --target /path/to/project target project
|
|
160
|
-
node scripts/doctor.mjs --platform opencode specific platform
|
|
161
148
|
node scripts/doctor.mjs --profile global|lite|sandbox profile policy
|
|
162
149
|
node scripts/doctor.mjs --fix attempt auto-fixes
|
|
163
150
|
node scripts/doctor.mjs --verbose detailed output
|
|
164
151
|
node scripts/doctor.mjs --help show this help
|
|
165
152
|
|
|
166
153
|
Checks:
|
|
167
|
-
A. Agent Files — canonical
|
|
154
|
+
A. Agent Files — canonical agent files
|
|
168
155
|
B. MCP Configuration — opencode.json MCP settings (Config layer)
|
|
169
156
|
B.5 MCP Spawn Paths — local MCP exe/args resolve to existing files
|
|
170
157
|
B.6 MCP Runtime Smoke — spawn each local MCP + JSON-RPC initialize handshake
|
|
171
158
|
C. Permission Checks — frontmatter validation
|
|
172
|
-
D.
|
|
159
|
+
D. OpenCode Status — OpenCode artifact layout
|
|
173
160
|
E. Git Status — uncommitted changes
|
|
174
161
|
F. Runtime Layer — venv python + pinned pip dependencies
|
|
175
162
|
|
|
@@ -256,7 +243,7 @@ function spawn(prog, args, cwd = null) {
|
|
|
256
243
|
// Check A: Agent Files
|
|
257
244
|
// ---------------------------------------------------------------------------
|
|
258
245
|
|
|
259
|
-
function checkAgentFiles(
|
|
246
|
+
function checkAgentFiles() {
|
|
260
247
|
section('A. Agent Files')
|
|
261
248
|
|
|
262
249
|
const canonical = getCanonicalAgentNames()
|
|
@@ -265,86 +252,6 @@ function checkAgentFiles(args) {
|
|
|
265
252
|
return
|
|
266
253
|
}
|
|
267
254
|
pass(`${canonical.length} canonical agents found in agents/`)
|
|
268
|
-
|
|
269
|
-
// Determine which platforms to check
|
|
270
|
-
if (!existsSync(PLATFORM_DIR)) {
|
|
271
|
-
info(
|
|
272
|
-
`Platform component not distributed in this package — platform check skipped (${args.profile} profile)`,
|
|
273
|
-
)
|
|
274
|
-
return
|
|
275
|
-
}
|
|
276
|
-
const platformDirs = readdirSync(PLATFORM_DIR, { withFileTypes: true })
|
|
277
|
-
.filter((d) => d.isDirectory() && d.name !== '_template' && d.name !== 'plans')
|
|
278
|
-
.map((d) => d.name)
|
|
279
|
-
.filter((name) => !args.platform || name === args.platform)
|
|
280
|
-
|
|
281
|
-
if (platformDirs.length === 0) {
|
|
282
|
-
info(
|
|
283
|
-
`No platform directories found in platform/ — platform check skipped (${args.profile} profile)`,
|
|
284
|
-
)
|
|
285
|
-
return
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
for (const platform of platformDirs) {
|
|
289
|
-
const adapterPath = join(PLATFORM_DIR, platform, 'adapter.json')
|
|
290
|
-
if (!existsSync(adapterPath)) {
|
|
291
|
-
info(`${platform}: no adapter.json — skipping`)
|
|
292
|
-
continue
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const adapter = readJson(adapterPath)
|
|
296
|
-
if (!adapter) {
|
|
297
|
-
warn(`${platform}: adapter.json parse error — skipping`)
|
|
298
|
-
continue
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const label = PLATFORM_LABELS[platform] ?? platform
|
|
302
|
-
const outputDir = adapter.outputDir ?? 'agents'
|
|
303
|
-
const ext = adapter.fileExtension ?? '.md'
|
|
304
|
-
const agentDir = join(PLATFORM_DIR, platform, outputDir)
|
|
305
|
-
|
|
306
|
-
if (!existsSync(agentDir)) {
|
|
307
|
-
if (adapter.skipAgentSync) {
|
|
308
|
-
info(`${label}: agent sync skipped (skipAgentSync) — n/a`)
|
|
309
|
-
continue
|
|
310
|
-
}
|
|
311
|
-
warn(`${label}: output directory missing: ${agentDir}`)
|
|
312
|
-
continue
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (adapter.skipAgentSync) {
|
|
316
|
-
info(`${label}: agent sync skipped (skipAgentSync) — skipping agent check`)
|
|
317
|
-
continue
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const platformFiles = readdirSync(agentDir)
|
|
321
|
-
.filter((f) => statSync(join(agentDir, f)).isFile() && f.endsWith(ext))
|
|
322
|
-
.map((f) => f.replace(new RegExp(`${ext}$`), ''))
|
|
323
|
-
|
|
324
|
-
// Check every canonical agent exists in platform output
|
|
325
|
-
let allPresent = true
|
|
326
|
-
const missing = []
|
|
327
|
-
for (const agent of canonical) {
|
|
328
|
-
if (!platformFiles.includes(agent)) {
|
|
329
|
-
missing.push(agent)
|
|
330
|
-
allPresent = false
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Detect phantom agents (in platform but not canonical — stale)
|
|
335
|
-
const phantom = platformFiles.filter((f) => !canonical.includes(f))
|
|
336
|
-
|
|
337
|
-
if (allPresent && phantom.length === 0) {
|
|
338
|
-
pass(`${label}: all ${canonical.length} agents present`)
|
|
339
|
-
} else {
|
|
340
|
-
if (missing.length > 0) {
|
|
341
|
-
error(`${label}: ${missing.length} canonical agent(s) missing: ${missing.join(', ')}`)
|
|
342
|
-
}
|
|
343
|
-
if (phantom.length > 0) {
|
|
344
|
-
warn(`${label}: ${phantom.length} stale agent(s) in output: ${phantom.join(', ')}`)
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
255
|
}
|
|
349
256
|
|
|
350
257
|
// ---------------------------------------------------------------------------
|
|
@@ -364,10 +271,12 @@ export function resolveOpenCodeConfigDir(env = process.env) {
|
|
|
364
271
|
* @returns {{label:string, path:string, data:object}[]}
|
|
365
272
|
*/
|
|
366
273
|
export function collectMcpConfigs(args) {
|
|
367
|
-
const candidates = [
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
274
|
+
const candidates = [{ label: 'project root', path: join(args.target, 'opencode.json') }]
|
|
275
|
+
// The package's own template is useful when doctor is run from the
|
|
276
|
+
// repository, but must not be treated as a user's config from an installed
|
|
277
|
+
// package. Otherwise every global/project sandbox reports false MCP errors.
|
|
278
|
+
if (resolve(args.target) === resolve(ROOT))
|
|
279
|
+
candidates.push({ label: 'repository root', path: join(ROOT, 'opencode.json') })
|
|
371
280
|
|
|
372
281
|
// OpenCode resolves user configuration independently of cwd. Keep the
|
|
373
282
|
// project-local .opencode config for repository checks, but always include
|
|
@@ -481,7 +390,7 @@ function checkMcpConfig(args) {
|
|
|
481
390
|
// ENOENT-on-spawn failure class (dead hardcoded path, ambiguous relative
|
|
482
391
|
// path) before opencode tries to spawn the server.
|
|
483
392
|
for (const [name, mcpEntry] of Object.entries(mcp)) {
|
|
484
|
-
if (
|
|
393
|
+
if (mcpEntry?.type !== 'local') continue
|
|
485
394
|
const cmd = mcpEntry.command
|
|
486
395
|
if (!Array.isArray(cmd) || cmd.length === 0) {
|
|
487
396
|
warn(`${cfg.label}: MCP "${name}" has no command array`)
|
|
@@ -495,11 +404,13 @@ function checkMcpConfig(args) {
|
|
|
495
404
|
)
|
|
496
405
|
continue
|
|
497
406
|
}
|
|
498
|
-
|
|
499
|
-
|
|
407
|
+
const effectiveCwd = resolveMcpCwd(mcpEntry, cfg.path)
|
|
408
|
+
const effectiveExe = exe.startsWith('/') ? exe : resolve(effectiveCwd, exe)
|
|
409
|
+
if (!existsSync(effectiveExe)) {
|
|
410
|
+
error(`${cfg.label}: MCP "${name}" executable does not exist: ${effectiveExe}`)
|
|
500
411
|
continue
|
|
501
412
|
}
|
|
502
|
-
const deadArgs =
|
|
413
|
+
const deadArgs = resolveMcpScriptPaths(mcpEntry, cfg.path).filter((path) => !existsSync(path))
|
|
503
414
|
if (deadArgs.length > 0) {
|
|
504
415
|
error(`${cfg.label}: MCP "${name}" has missing script path(s): ${deadArgs.join(', ')}`)
|
|
505
416
|
}
|
|
@@ -620,9 +531,13 @@ async function checkMcpRuntimeSmoke(args) {
|
|
|
620
531
|
const local = []
|
|
621
532
|
for (const cfg of collectMcpConfigs(args)) {
|
|
622
533
|
for (const [name, entry] of Object.entries(cfg.data.mcp ?? {})) {
|
|
623
|
-
if (
|
|
534
|
+
if (entry?.type !== 'local') continue
|
|
624
535
|
if (!Array.isArray(entry.command) || entry.command.length === 0) continue
|
|
625
|
-
|
|
536
|
+
const cwd = resolveMcpCwd(entry, cfg.path)
|
|
537
|
+
const command = entry.command.map((part, index) =>
|
|
538
|
+
index === 0 || part.startsWith('-') || part.startsWith('/') ? part : resolve(cwd, part),
|
|
539
|
+
)
|
|
540
|
+
local.push({ name, command, cwd, source: cfg.label })
|
|
626
541
|
}
|
|
627
542
|
}
|
|
628
543
|
|
|
@@ -702,8 +617,7 @@ function checkPermissionMismatches(args) {
|
|
|
702
617
|
}
|
|
703
618
|
|
|
704
619
|
// Determine which agent directories to check
|
|
705
|
-
//
|
|
706
|
-
// Let's run it regardless
|
|
620
|
+
// Validate the canonical agents and the OpenCode installation independently.
|
|
707
621
|
info('Running validate-agent-frontmatter.py...')
|
|
708
622
|
|
|
709
623
|
const result = spawn('python3', [validator])
|
|
@@ -789,14 +703,35 @@ function checkPermissionMismatches(args) {
|
|
|
789
703
|
}
|
|
790
704
|
|
|
791
705
|
// ---------------------------------------------------------------------------
|
|
792
|
-
// Check D:
|
|
706
|
+
// Check D: OpenCode Status
|
|
793
707
|
// ---------------------------------------------------------------------------
|
|
794
708
|
|
|
795
|
-
function checkSyncStatus(
|
|
796
|
-
section('D.
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
709
|
+
function checkSyncStatus(args) {
|
|
710
|
+
section('D. OpenCode Status')
|
|
711
|
+
const agentsDir = join(args.target, '.opencode', 'agents')
|
|
712
|
+
if (existsSync(agentsDir)) {
|
|
713
|
+
pass('OpenCode agents directory is present')
|
|
714
|
+
// Content gate: validate pantheon://agents CONTENT (zeus/hermes present),
|
|
715
|
+
// not just connectivity/directory presence.
|
|
716
|
+
const agentFiles = readdirSync(agentsDir).filter((f) => f.endsWith('.md'))
|
|
717
|
+
if (agentFiles.length === 0) {
|
|
718
|
+
warn('OpenCode agents directory is empty — pantheon://agents will serve no agents')
|
|
719
|
+
} else {
|
|
720
|
+
const names = new Set(agentFiles.map((f) => f.replace(/\.md$/, '').toLowerCase()))
|
|
721
|
+
const missingCanonical = ['zeus', 'hermes'].filter((n) => !names.has(n))
|
|
722
|
+
if (missingCanonical.length > 0) {
|
|
723
|
+
error(
|
|
724
|
+
`pantheon://agents content check failed — missing canonical agents in ${agentsDir}: ${missingCanonical.join(', ')}`,
|
|
725
|
+
)
|
|
726
|
+
} else {
|
|
727
|
+
pass(
|
|
728
|
+
`pantheon://agents content validated (zeus, hermes + ${agentFiles.length - 2} more agents)`,
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
info('OpenCode agents directory not present — project may use global agents')
|
|
734
|
+
}
|
|
800
735
|
}
|
|
801
736
|
|
|
802
737
|
// ---------------------------------------------------------------------------
|
|
@@ -813,8 +748,16 @@ function checkGitStatus(args) {
|
|
|
813
748
|
return
|
|
814
749
|
}
|
|
815
750
|
|
|
816
|
-
// Check
|
|
817
|
-
const gitStatus = spawn('git', [
|
|
751
|
+
// Check the repository source and OpenCode configuration paths.
|
|
752
|
+
const gitStatus = spawn('git', [
|
|
753
|
+
'status',
|
|
754
|
+
'--porcelain',
|
|
755
|
+
'--',
|
|
756
|
+
'src/agents/',
|
|
757
|
+
'src/instructions/',
|
|
758
|
+
'.opencode/',
|
|
759
|
+
'opencode.json',
|
|
760
|
+
])
|
|
818
761
|
|
|
819
762
|
if (gitStatus.status !== 0) {
|
|
820
763
|
warn('Could not check git status')
|
|
@@ -824,7 +767,7 @@ function checkGitStatus(args) {
|
|
|
824
767
|
const lines = gitStatus.stdout ? gitStatus.stdout.split('\n').filter(Boolean) : []
|
|
825
768
|
|
|
826
769
|
if (lines.length === 0) {
|
|
827
|
-
pass('No uncommitted changes in
|
|
770
|
+
pass('No uncommitted changes in Pantheon/OpenCode paths')
|
|
828
771
|
return
|
|
829
772
|
}
|
|
830
773
|
|
|
@@ -851,7 +794,7 @@ function checkGitStatus(args) {
|
|
|
851
794
|
if (deleted.length > 0) details.push(`${deleted.length} deleted`)
|
|
852
795
|
if (untracked.length > 0) details.push(`${untracked.length} untracked`)
|
|
853
796
|
|
|
854
|
-
warn(`${total} uncommitted change(s) in
|
|
797
|
+
warn(`${total} uncommitted change(s) in Pantheon/OpenCode paths (${details.join(', ')})`)
|
|
855
798
|
if (args.verbose) {
|
|
856
799
|
for (const f of modified.slice(0, 10)) {
|
|
857
800
|
console.log(` M ${f}`)
|
|
@@ -963,8 +906,7 @@ function checkRequirementLine(line, installed) {
|
|
|
963
906
|
function checkVenvLayer(args) {
|
|
964
907
|
section('F. Runtime Layer — Venv, Python & Dependencies')
|
|
965
908
|
|
|
966
|
-
const
|
|
967
|
-
const venvPython = join(configDir, '.venv', 'bin', 'python3')
|
|
909
|
+
const venvPython = resolveRuntimePython(args)
|
|
968
910
|
|
|
969
911
|
if (!existsSync(venvPython)) {
|
|
970
912
|
if (args.profile === 'lite') {
|
|
@@ -1013,6 +955,236 @@ function checkVenvLayer(args) {
|
|
|
1013
955
|
}
|
|
1014
956
|
}
|
|
1015
957
|
|
|
958
|
+
// ---------------------------------------------------------------------------
|
|
959
|
+
// Check F.2: Code-Mode Scripts Directory
|
|
960
|
+
// ---------------------------------------------------------------------------
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Verify the code-mode scripts directory exists for the resolved runtime
|
|
964
|
+
* root (project → <target>/.opencode, otherwise the global config dir).
|
|
965
|
+
* When absent, create it with a warning so the pantheon-code-mode MCP
|
|
966
|
+
* server has a stable home (mirrors the installer's runtime seeding).
|
|
967
|
+
*/
|
|
968
|
+
export function checkCodeModeDir(args) {
|
|
969
|
+
section('F.2 Code-Mode Scripts')
|
|
970
|
+
const env = args.env ?? process.env
|
|
971
|
+
const target = resolve(env.PANTHEON_PROJECT ?? args.target)
|
|
972
|
+
const projectCandidates = [
|
|
973
|
+
join(target, '.opencode', '.pantheon', 'code-mode'),
|
|
974
|
+
join(target, '.pantheon', 'code-mode'),
|
|
975
|
+
]
|
|
976
|
+
// Seeding semantics: an existing project candidate wins; a project
|
|
977
|
+
// install (.opencode present) gets its runtime dir created when missing.
|
|
978
|
+
// Only targets with no project layout fall back to the global config dir.
|
|
979
|
+
const codeModeDir =
|
|
980
|
+
projectCandidates.find((candidate) => existsSync(candidate)) ??
|
|
981
|
+
(existsSync(join(target, '.opencode'))
|
|
982
|
+
? projectCandidates[0]
|
|
983
|
+
: join(resolveOpenCodeConfigDir(env), '.pantheon', 'code-mode'))
|
|
984
|
+
if (existsSync(codeModeDir)) {
|
|
985
|
+
pass(`Code-mode scripts directory exists: ${codeModeDir}`)
|
|
986
|
+
return codeModeDir
|
|
987
|
+
}
|
|
988
|
+
warn(`Code-mode scripts directory missing — creating empty ${codeModeDir}`)
|
|
989
|
+
try {
|
|
990
|
+
mkdirSync(codeModeDir, { recursive: true })
|
|
991
|
+
info(`Created ${codeModeDir} (run \`pantheon-opencode init --components runtime\` to seed it)`)
|
|
992
|
+
} catch (err) {
|
|
993
|
+
error(`Cannot create code-mode directory ${codeModeDir}: ${err.message}`)
|
|
994
|
+
}
|
|
995
|
+
return codeModeDir
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/** Resolve code-mode using the same project-first rule as the MCP server. */
|
|
999
|
+
export function resolveCodeModeDir(args) {
|
|
1000
|
+
const env = args.env ?? process.env
|
|
1001
|
+
const target = resolve(env.PANTHEON_PROJECT ?? args.target)
|
|
1002
|
+
const projectCandidates = [
|
|
1003
|
+
join(target, '.opencode', '.pantheon', 'code-mode'),
|
|
1004
|
+
join(target, '.pantheon', 'code-mode'),
|
|
1005
|
+
]
|
|
1006
|
+
const projectDir = projectCandidates.find((candidate) => existsSync(candidate))
|
|
1007
|
+
if (projectDir) return projectDir
|
|
1008
|
+
return join(resolveOpenCodeConfigDir(env), '.pantheon', 'code-mode')
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** Validate a code-mode manifest without modifying it. */
|
|
1012
|
+
export function validateCodeModeManifest(codeModeDir) {
|
|
1013
|
+
const manifestPath = join(codeModeDir, 'manifest.json')
|
|
1014
|
+
if (!existsSync(manifestPath)) {
|
|
1015
|
+
return { ok: false, message: `Code-mode manifest missing: ${manifestPath}` }
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
let data
|
|
1019
|
+
try {
|
|
1020
|
+
data = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
1021
|
+
} catch {
|
|
1022
|
+
return { ok: false, message: `Code-mode manifest malformed: ${manifestPath}` }
|
|
1023
|
+
}
|
|
1024
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
1025
|
+
return { ok: false, message: 'Code-mode manifest malformed: root must be an object' }
|
|
1026
|
+
}
|
|
1027
|
+
if (data.version !== 1) {
|
|
1028
|
+
return { ok: false, message: 'Code-mode manifest malformed: version must be 1' }
|
|
1029
|
+
}
|
|
1030
|
+
if (!data.scripts || typeof data.scripts !== 'object' || Array.isArray(data.scripts)) {
|
|
1031
|
+
return { ok: false, message: 'Code-mode manifest malformed: scripts must be an object' }
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const listed = Object.keys(data.scripts)
|
|
1035
|
+
const invalid = listed.filter(
|
|
1036
|
+
(name) => typeof data.scripts[name] !== 'string' || !/^[a-f0-9]{64}$/i.test(data.scripts[name]),
|
|
1037
|
+
)
|
|
1038
|
+
if (invalid.length) {
|
|
1039
|
+
return { ok: false, message: `Code-mode manifest invalid digest: ${invalid.join(', ')}` }
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const actualScripts = readdirSync(codeModeDir).filter(
|
|
1043
|
+
(name) =>
|
|
1044
|
+
(name.endsWith('.py') || name.endsWith('.sh')) && statSync(join(codeModeDir, name)).isFile(),
|
|
1045
|
+
)
|
|
1046
|
+
const missing = listed.filter((name) => !existsSync(join(codeModeDir, name)))
|
|
1047
|
+
const extra = actualScripts.filter((name) => !Object.hasOwn(data.scripts, name))
|
|
1048
|
+
const differing = listed.filter((name) => {
|
|
1049
|
+
const path = join(codeModeDir, name)
|
|
1050
|
+
if (!existsSync(path)) return false
|
|
1051
|
+
const digest = createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
1052
|
+
return digest !== data.scripts[name].toLowerCase()
|
|
1053
|
+
})
|
|
1054
|
+
if (missing.length || extra.length || differing.length) {
|
|
1055
|
+
const names = [...new Set([...missing, ...extra, ...differing])]
|
|
1056
|
+
return {
|
|
1057
|
+
ok: false,
|
|
1058
|
+
message: `Code-mode manifest mismatch: ${differing.length + missing.length + extra.length}/${listed.length} scripts differ — ${names.join(', ')}`,
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return { ok: true, count: listed.length, total: actualScripts.length }
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** Run the diagnostic manifest check after directory validation. */
|
|
1065
|
+
export function checkCodeModeManifest(args) {
|
|
1066
|
+
const codeModeDir = resolveCodeModeDir(args)
|
|
1067
|
+
const result = validateCodeModeManifest(codeModeDir)
|
|
1068
|
+
if (!result.ok) {
|
|
1069
|
+
error(result.message)
|
|
1070
|
+
return
|
|
1071
|
+
}
|
|
1072
|
+
pass(`✅ Code-mode manifest valid: ${result.count}/${result.total} scripts verified`)
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** Resolve the venv created by init for either a project or global install. */
|
|
1076
|
+
export function resolveRuntimePython(args) {
|
|
1077
|
+
const target = resolve(args.target)
|
|
1078
|
+
const isProject = existsSync(join(target, '.opencode'))
|
|
1079
|
+
const runtimeRoot = isProject ? target : resolveOpenCodeConfigDir(args.env ?? process.env)
|
|
1080
|
+
return join(runtimeRoot, '.venv', 'bin', 'python3')
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/** Return relative script arguments as absolute paths using an MCP entry cwd. */
|
|
1084
|
+
export function resolveMcpScriptPaths(entry, configPath = '') {
|
|
1085
|
+
const cwd = resolveMcpCwd(entry, configPath)
|
|
1086
|
+
if (!Array.isArray(entry?.command)) return []
|
|
1087
|
+
return entry.command
|
|
1088
|
+
.slice(1)
|
|
1089
|
+
.filter(
|
|
1090
|
+
(arg) =>
|
|
1091
|
+
typeof arg === 'string' &&
|
|
1092
|
+
!arg.startsWith('-') &&
|
|
1093
|
+
(arg.endsWith('.py') || arg.includes('/')),
|
|
1094
|
+
)
|
|
1095
|
+
.map((arg) => (arg.startsWith('/') ? arg : resolve(cwd, arg)))
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/** Resolve an MCP entry's cwd relative to the config file, as OpenCode does. */
|
|
1099
|
+
export function resolveMcpCwd(entry, configPath = '') {
|
|
1100
|
+
const configured = entry?.cwd
|
|
1101
|
+
if (typeof configured === 'string' && configured !== '') {
|
|
1102
|
+
return configured.startsWith('/') ? configured : resolve(dirname(configPath), configured)
|
|
1103
|
+
}
|
|
1104
|
+
return dirname(configPath)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// ---------------------------------------------------------------------------
|
|
1108
|
+
// Check H2: installed-version drift (beta.5)
|
|
1109
|
+
// ---------------------------------------------------------------------------
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Prerelease-aware comparison for pantheon versions (X.Y.Z or X.Y.Z-beta.N).
|
|
1113
|
+
* A stable release outranks any beta of the same core version. Returns
|
|
1114
|
+
* <0, 0, >0 (a - b). The shared compareVersions/parseVersion above ignores
|
|
1115
|
+
* the `-beta.N` suffix (it only reads 4 numeric groups), which would make
|
|
1116
|
+
* every beta pair compare equal.
|
|
1117
|
+
*/
|
|
1118
|
+
function comparePantheonVersions(a, b) {
|
|
1119
|
+
const parse = (v) => {
|
|
1120
|
+
const m = String(v)
|
|
1121
|
+
.trim()
|
|
1122
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/)
|
|
1123
|
+
if (!m) return null
|
|
1124
|
+
return {
|
|
1125
|
+
core: [+m[1], +m[2], +m[3]],
|
|
1126
|
+
beta: m[4] === undefined ? Number.POSITIVE_INFINITY : +m[4],
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const pa = parse(a)
|
|
1130
|
+
const pb = parse(b)
|
|
1131
|
+
if (!pa || !pb) return 0
|
|
1132
|
+
for (let i = 0; i < 3; i++) {
|
|
1133
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
|
|
1134
|
+
}
|
|
1135
|
+
return pa.beta - pb.beta
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Compare the version recorded in the installation marker
|
|
1140
|
+
* (<config>/.pantheon/install-state.json, written by the postinstall sync)
|
|
1141
|
+
* against this package's version. When the package is newer, the config
|
|
1142
|
+
* merge/venv/MCP wiring may be stale — point the user at `update`.
|
|
1143
|
+
* Advisory only: a missing marker (pre-beta.5 install) is not an error.
|
|
1144
|
+
*/
|
|
1145
|
+
export function checkInstallVersionDrift(args) {
|
|
1146
|
+
section('H2 Installed Version')
|
|
1147
|
+
const env = args.env ?? process.env
|
|
1148
|
+
let pkgVersion = null
|
|
1149
|
+
try {
|
|
1150
|
+
pkgVersion = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version
|
|
1151
|
+
} catch {
|
|
1152
|
+
return 'no-marker'
|
|
1153
|
+
}
|
|
1154
|
+
if (!pkgVersion) return 'no-marker'
|
|
1155
|
+
|
|
1156
|
+
const statePath = join(resolveOpenCodeConfigDir(env), '.pantheon', 'install-state.json')
|
|
1157
|
+
let installed = null
|
|
1158
|
+
if (existsSync(statePath)) {
|
|
1159
|
+
try {
|
|
1160
|
+
installed = JSON.parse(readFileSync(statePath, 'utf8')).pantheon_version ?? null
|
|
1161
|
+
} catch {
|
|
1162
|
+
installed = null
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (!installed) {
|
|
1166
|
+
info('No installation version marker found (pre-beta.5 install) — skipping drift check')
|
|
1167
|
+
return 'no-marker'
|
|
1168
|
+
}
|
|
1169
|
+
const drift = comparePantheonVersions(pkgVersion, installed)
|
|
1170
|
+
if (drift > 0) {
|
|
1171
|
+
warn(
|
|
1172
|
+
`Package v${pkgVersion} is newer than the last-synced installation (v${installed}) — ` +
|
|
1173
|
+
'run `npx pantheon-opencode update` (or re-run init) to refresh config, venv and MCP entries',
|
|
1174
|
+
)
|
|
1175
|
+
return 'package-newer'
|
|
1176
|
+
}
|
|
1177
|
+
if (drift < 0) {
|
|
1178
|
+
warn(
|
|
1179
|
+
`Installation marker (v${installed}) is newer than this package (v${pkgVersion}) — ` +
|
|
1180
|
+
'the doctor is running from a different install than the last sync',
|
|
1181
|
+
)
|
|
1182
|
+
return 'install-newer'
|
|
1183
|
+
}
|
|
1184
|
+
pass(`Installation in sync with package v${pkgVersion}`)
|
|
1185
|
+
return 'sync'
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1016
1188
|
// ---------------------------------------------------------------------------
|
|
1017
1189
|
// Check G: AGENTS.md freshness
|
|
1018
1190
|
// ---------------------------------------------------------------------------
|
|
@@ -1285,16 +1457,18 @@ async function main() {
|
|
|
1285
1457
|
process.exit(2)
|
|
1286
1458
|
}
|
|
1287
1459
|
|
|
1288
|
-
// Verify it looks like a Pantheon project
|
|
1289
|
-
const agentsOk = existsSync(join(args.target, 'agents'))
|
|
1290
|
-
const
|
|
1460
|
+
// Verify it looks like a Pantheon project or an OpenCode install target.
|
|
1461
|
+
const agentsOk = existsSync(join(args.target, 'src', 'agents'))
|
|
1462
|
+
const opencodeOk = existsSync(join(args.target, '.opencode'))
|
|
1291
1463
|
|
|
1292
|
-
if (!agentsOk && !
|
|
1464
|
+
if (!agentsOk && !opencodeOk) {
|
|
1293
1465
|
// Maybe they pointed at a project root that isn't Pantheon
|
|
1294
1466
|
// Check if this is a Pantheon install target (has opencode.json with mcp)
|
|
1295
1467
|
const opencodeJson = readJson(join(args.target, 'opencode.json'))
|
|
1296
1468
|
if (!opencodeJson) {
|
|
1297
|
-
warn(
|
|
1469
|
+
warn(
|
|
1470
|
+
'Target does not appear to be a Pantheon project (no src/agents/ or .opencode/ directory)',
|
|
1471
|
+
)
|
|
1298
1472
|
}
|
|
1299
1473
|
}
|
|
1300
1474
|
|
|
@@ -1306,6 +1480,9 @@ async function main() {
|
|
|
1306
1480
|
checkAgentFiles(args)
|
|
1307
1481
|
checkMcpConfig(args)
|
|
1308
1482
|
checkVenvLayer(args)
|
|
1483
|
+
checkCodeModeDir(args)
|
|
1484
|
+
checkCodeModeManifest(args)
|
|
1485
|
+
checkInstallVersionDrift(args)
|
|
1309
1486
|
await checkMcpRuntimeSmoke(args)
|
|
1310
1487
|
checkPermissionMismatches(args)
|
|
1311
1488
|
checkSyncStatus(args)
|