pantheon-opencode 1.6.0-beta.10 → 1.6.0-beta.12
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/AGENTS.global.md +42 -18
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +122 -38
- package/README.pt-BR.md +5 -4
- package/bin/pantheon-init.mjs +322 -101
- package/package.json +7 -6
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/build-agents-md.mjs +128 -10
- package/scripts/check-plugin-v2-coverage.mjs +7 -7
- package/scripts/doctor.mjs +242 -19
- package/scripts/install/backup.mjs +648 -0
- package/scripts/install/cli-ui.mjs +79 -1
- package/scripts/install/exit-codes.mjs +26 -0
- package/scripts/install/file-inventory.mjs +555 -0
- package/scripts/install/interactive.mjs +15 -1
- package/scripts/install/migrate.mjs +32 -6
- package/scripts/install/model-picker.mjs +63 -22
- package/scripts/install/opencode-version.mjs +9 -1
- package/scripts/install/opencode.mjs +761 -63
- package/scripts/install/plan-artifact.mjs +517 -0
- package/scripts/install/plan.mjs +449 -0
- package/scripts/install/plugin.mjs +136 -18
- package/scripts/install/shared.mjs +249 -94
- package/scripts/install/state.mjs +63 -8
- package/scripts/install/strings.mjs +20 -0
- package/scripts/install/venv.mjs +25 -6
- package/scripts/install/verify.mjs +128 -0
- package/scripts/install/vision-prompt.mjs +13 -1
- package/scripts/install-mcp.mjs +1 -1
- package/scripts/manifest-inventory.mjs +49 -3
- package/scripts/mcp_persistence.py +50 -15
- package/scripts/node-engine-policy.mjs +74 -0
- package/scripts/package-evidence.mjs +6 -0
- package/scripts/postinstall.mjs +3 -4
- package/scripts/prune.mjs +9 -3
- package/scripts/reconcile.mjs +325 -0
- package/scripts/restore.mjs +222 -0
- package/scripts/uninstall.mjs +258 -34
- package/scripts/update.mjs +164 -0
- package/scripts/validate-node-engine-policy.mjs +13 -0
- package/scripts/version-check.mjs +34 -1
- package/scripts/versioning.mjs +9 -1
- package/skills-lock.json +4 -4
- package/src/agents/iris.md +6 -0
- package/src/agents/mnemosyne.md +7 -2
- package/src/agents/zeus.md +19 -62
- package/src/instructions/agent-return-format.instructions.md +17 -15
- package/src/instructions/memory-protocol.instructions.md +3 -2
- package/src/instructions/test-economy.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +6 -6
- package/src/instructions/zeus-council-synthesis.instructions.md +10 -11
- package/src/instructions/zeus-memory-operations.instructions.md +11 -6
- package/src/instructions/zeus-timeout-retry.instructions.md +28 -9
- package/src/mcp/mcp_persistence.py +50 -15
- package/src/pantheon/cost-command.ts +5 -2
- package/src/pantheon/model-agents.ts +19 -0
- package/src/pantheon/model-command.ts +218 -56
- package/src/pantheon/v2-model-overrides.ts +275 -0
- package/src/pantheon/v2-tools.ts +18 -29
- package/src/pantheon/v2-unsupported.mjs +2 -3
- package/src/plugin-v2.ts +13 -0
- package/src/plugins/tui/dist/tui-v2.js +2 -2
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +3 -3
- package/src/plugins/tui/package.json +2 -2
- package/src/skills/artifact-management/SKILL.md +5 -1
- package/src/skills/auto-continue/SKILL.md +14 -6
- package/src/skills/memory-bank/SKILL.md +5 -1
- package/src/skills/worktrees/SKILL.md +30 -11
|
@@ -35,6 +35,8 @@ import { execFileSync } from 'node:child_process'
|
|
|
35
35
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
36
36
|
import { dirname, join, resolve } from 'node:path'
|
|
37
37
|
import { fileURLToPath } from 'node:url'
|
|
38
|
+
import { ACTIONS, fingerprintOutput, isRecording, recordChange } from './install/plan.mjs'
|
|
39
|
+
import { assertNoSymlinkAncestors, assertPathWithin, verifyRegularFile } from './install/verify.mjs'
|
|
38
40
|
|
|
39
41
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
40
42
|
export const ROOT = join(__dirname, '..')
|
|
@@ -323,16 +325,41 @@ export function generateProjectAgentsMd(git = readGitInfo()) {
|
|
|
323
325
|
* @param {boolean} [dryRun=false] - Do not write when true
|
|
324
326
|
* @returns {'created' | 'skipped' | 'updated' | 'upgraded' | 'merged'}
|
|
325
327
|
*/
|
|
326
|
-
export function writeAgentsMdManaged(filePath, generated, dryRun = false) {
|
|
328
|
+
export function writeAgentsMdManaged(filePath, generated, dryRun = false, recorder = null) {
|
|
329
|
+
assertPathWithin(dirname(filePath), filePath)
|
|
330
|
+
assertNoSymlinkAncestors(filePath)
|
|
327
331
|
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : null
|
|
328
332
|
if (existing === null) {
|
|
329
333
|
if (!dryRun) {
|
|
334
|
+
assertNoSymlinkAncestors(dirname(filePath))
|
|
330
335
|
mkdirSync(dirname(filePath), { recursive: true })
|
|
336
|
+
assertNoSymlinkAncestors(filePath)
|
|
331
337
|
writeFileSync(filePath, generated, 'utf8')
|
|
338
|
+
verifyRegularFile(filePath, generated)
|
|
332
339
|
}
|
|
340
|
+
recordChange(
|
|
341
|
+
{
|
|
342
|
+
path: filePath,
|
|
343
|
+
action: ACTIONS.CREATE,
|
|
344
|
+
bytes: Buffer.byteLength(generated, 'utf8'),
|
|
345
|
+
outputFingerprint: fingerprintOutput(generated),
|
|
346
|
+
},
|
|
347
|
+
recorder,
|
|
348
|
+
)
|
|
333
349
|
return 'created'
|
|
334
350
|
}
|
|
335
|
-
if (existing === generated)
|
|
351
|
+
if (existing === generated) {
|
|
352
|
+
recordChange(
|
|
353
|
+
{
|
|
354
|
+
path: filePath,
|
|
355
|
+
action: ACTIONS.UNCHANGED,
|
|
356
|
+
bytes: Buffer.byteLength(existing, 'utf8'),
|
|
357
|
+
outputFingerprint: fingerprintOutput(existing),
|
|
358
|
+
},
|
|
359
|
+
recorder,
|
|
360
|
+
)
|
|
361
|
+
return 'skipped'
|
|
362
|
+
}
|
|
336
363
|
|
|
337
364
|
const begin = existing.indexOf(MANAGED_BEGIN)
|
|
338
365
|
const end = existing.indexOf(MANAGED_END)
|
|
@@ -340,22 +367,71 @@ export function writeAgentsMdManaged(filePath, generated, dryRun = false) {
|
|
|
340
367
|
const before = existing.slice(0, begin)
|
|
341
368
|
const after = existing.slice(end + MANAGED_END.length)
|
|
342
369
|
const next = `${before}${generated}${after}`
|
|
343
|
-
if (next === existing)
|
|
344
|
-
|
|
370
|
+
if (next === existing) {
|
|
371
|
+
recordChange(
|
|
372
|
+
{
|
|
373
|
+
path: filePath,
|
|
374
|
+
action: ACTIONS.UNCHANGED,
|
|
375
|
+
bytes: Buffer.byteLength(existing, 'utf8'),
|
|
376
|
+
outputFingerprint: fingerprintOutput(existing),
|
|
377
|
+
},
|
|
378
|
+
recorder,
|
|
379
|
+
)
|
|
380
|
+
return 'skipped'
|
|
381
|
+
}
|
|
382
|
+
if (!dryRun) {
|
|
383
|
+
assertNoSymlinkAncestors(filePath)
|
|
384
|
+
writeFileSync(filePath, next, 'utf8')
|
|
385
|
+
verifyRegularFile(filePath, next)
|
|
386
|
+
}
|
|
387
|
+
recordChange(
|
|
388
|
+
{
|
|
389
|
+
path: filePath,
|
|
390
|
+
action: ACTIONS.UPDATE,
|
|
391
|
+
bytes: Buffer.byteLength(next, 'utf8'),
|
|
392
|
+
outputFingerprint: fingerprintOutput(next),
|
|
393
|
+
},
|
|
394
|
+
recorder,
|
|
395
|
+
)
|
|
345
396
|
return before.trim() === '' && after.trim() === '' ? 'updated' : 'merged'
|
|
346
397
|
}
|
|
347
398
|
|
|
348
399
|
if (existing.startsWith(GENERATED_BY)) {
|
|
349
400
|
// Legacy format: the header declares the whole file machine-generated.
|
|
350
|
-
if (!dryRun)
|
|
401
|
+
if (!dryRun) {
|
|
402
|
+
assertNoSymlinkAncestors(filePath)
|
|
403
|
+
writeFileSync(filePath, generated, 'utf8')
|
|
404
|
+
verifyRegularFile(filePath, generated)
|
|
405
|
+
}
|
|
406
|
+
recordChange(
|
|
407
|
+
{
|
|
408
|
+
path: filePath,
|
|
409
|
+
action: ACTIONS.UPDATE,
|
|
410
|
+
bytes: Buffer.byteLength(generated, 'utf8'),
|
|
411
|
+
outputFingerprint: fingerprintOutput(generated),
|
|
412
|
+
},
|
|
413
|
+
recorder,
|
|
414
|
+
)
|
|
351
415
|
return 'upgraded'
|
|
352
416
|
}
|
|
353
417
|
|
|
354
418
|
// Foreign content: preserve it verbatim and append the managed region.
|
|
419
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n'
|
|
420
|
+
const payload = `${existing}${separator}${generated}`
|
|
355
421
|
if (!dryRun) {
|
|
356
|
-
|
|
357
|
-
writeFileSync(filePath,
|
|
422
|
+
assertNoSymlinkAncestors(filePath)
|
|
423
|
+
writeFileSync(filePath, payload, 'utf8')
|
|
424
|
+
verifyRegularFile(filePath, payload)
|
|
358
425
|
}
|
|
426
|
+
recordChange(
|
|
427
|
+
{
|
|
428
|
+
path: filePath,
|
|
429
|
+
action: ACTIONS.UPDATE,
|
|
430
|
+
bytes: Buffer.byteLength(payload, 'utf8'),
|
|
431
|
+
outputFingerprint: fingerprintOutput(payload),
|
|
432
|
+
},
|
|
433
|
+
recorder,
|
|
434
|
+
)
|
|
359
435
|
return 'merged'
|
|
360
436
|
}
|
|
361
437
|
|
|
@@ -423,11 +499,23 @@ export function generateAgentPrompt(agentName, prompt, instructions = readInstru
|
|
|
423
499
|
* @param {string} targetDir - OpenCode agents directory
|
|
424
500
|
* @param {boolean} dryRun - Do not write when true
|
|
425
501
|
* @param {boolean} existingOnly - Refresh installed prompts without adding missing agents
|
|
502
|
+
* @param {(change: object) => void} [recorder] - optional change recorder for --dry-run preview
|
|
426
503
|
* @returns {{ created: number, skipped: number }}
|
|
427
504
|
*/
|
|
428
|
-
export function copyAgentPrompts(
|
|
505
|
+
export function copyAgentPrompts(
|
|
506
|
+
sourceDir,
|
|
507
|
+
targetDir,
|
|
508
|
+
dryRun = false,
|
|
509
|
+
existingOnly = false,
|
|
510
|
+
recorder = null,
|
|
511
|
+
) {
|
|
512
|
+
assertNoSymlinkAncestors(targetDir)
|
|
429
513
|
const instructions = readInstructions()
|
|
430
|
-
if (!dryRun && !existingOnly)
|
|
514
|
+
if (!dryRun && !existingOnly) {
|
|
515
|
+
assertNoSymlinkAncestors(targetDir)
|
|
516
|
+
mkdirSync(targetDir, { recursive: true })
|
|
517
|
+
assertNoSymlinkAncestors(targetDir)
|
|
518
|
+
}
|
|
431
519
|
let created = 0
|
|
432
520
|
let skipped = 0
|
|
433
521
|
for (const file of readdirSync(sourceDir)
|
|
@@ -439,13 +527,41 @@ export function copyAgentPrompts(sourceDir, targetDir, dryRun = false, existingO
|
|
|
439
527
|
const agentName = file.replace(/\.md$/, '')
|
|
440
528
|
const output = generateAgentPrompt(agentName, readFileSync(sourcePath, 'utf8'), instructions)
|
|
441
529
|
const targetPath = join(targetDir, file)
|
|
530
|
+
assertPathWithin(targetDir, targetPath)
|
|
531
|
+
assertNoSymlinkAncestors(targetPath)
|
|
442
532
|
if (existingOnly && !existsSync(targetPath)) continue
|
|
443
533
|
const existing = existsSync(targetPath) ? readFileSync(targetPath, 'utf8') : null
|
|
444
534
|
if (existing === output) {
|
|
445
535
|
skipped++
|
|
536
|
+
if (isRecording(recorder)) {
|
|
537
|
+
recordChange(
|
|
538
|
+
{
|
|
539
|
+
path: targetPath,
|
|
540
|
+
action: ACTIONS.UNCHANGED,
|
|
541
|
+
bytes: Buffer.byteLength(output, 'utf8'),
|
|
542
|
+
outputFingerprint: fingerprintOutput(output),
|
|
543
|
+
},
|
|
544
|
+
recorder,
|
|
545
|
+
)
|
|
546
|
+
}
|
|
446
547
|
} else {
|
|
447
|
-
if (!dryRun)
|
|
548
|
+
if (!dryRun) {
|
|
549
|
+
assertNoSymlinkAncestors(targetPath)
|
|
550
|
+
writeFileSync(targetPath, output, 'utf8')
|
|
551
|
+
verifyRegularFile(targetPath, output)
|
|
552
|
+
}
|
|
448
553
|
created++
|
|
554
|
+
if (isRecording(recorder)) {
|
|
555
|
+
recordChange(
|
|
556
|
+
{
|
|
557
|
+
path: targetPath,
|
|
558
|
+
action: existing === null ? ACTIONS.CREATE : ACTIONS.UPDATE,
|
|
559
|
+
bytes: Buffer.byteLength(output, 'utf8'),
|
|
560
|
+
outputFingerprint: fingerprintOutput(output),
|
|
561
|
+
},
|
|
562
|
+
recorder,
|
|
563
|
+
)
|
|
564
|
+
}
|
|
449
565
|
}
|
|
450
566
|
}
|
|
451
567
|
return { created, skipped }
|
|
@@ -504,6 +620,8 @@ function main() {
|
|
|
504
620
|
|
|
505
621
|
writeFileSync(AGENTS_GLOBAL_MD_PATH, global, 'utf8')
|
|
506
622
|
writeFileSync(AGENTS_MD_PATH, project, 'utf8')
|
|
623
|
+
verifyRegularFile(AGENTS_GLOBAL_MD_PATH, global)
|
|
624
|
+
verifyRegularFile(AGENTS_MD_PATH, project)
|
|
507
625
|
console.log(
|
|
508
626
|
`✅ Generated AGENTS.md (${logicalLineCount(project)} lines) + AGENTS.global.md ` +
|
|
509
627
|
`(${logicalLineCount(global)} lines, ${sharedCount} shared instruction sections)`,
|
|
@@ -3,8 +3,8 @@ import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { dirname, join, resolve } from 'node:path'
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
+
import { isSupportedNodeVersion, MIN_SUPPORTED_NODE_VERSION } from './node-engine-policy.mjs'
|
|
6
7
|
|
|
7
|
-
export const REQUIRED_NODE_VERSION = 'v24.15.0'
|
|
8
8
|
export const LINE_THRESHOLD_NUMERATOR = 4
|
|
9
9
|
export const LINE_THRESHOLD_DENOMINATOR = 5
|
|
10
10
|
|
|
@@ -103,13 +103,13 @@ export function parseTargetLcov(report, repoRoot, targetPath) {
|
|
|
103
103
|
const found = integerField(exactlyOneField(lines, 'LF'), 'LF', 1)
|
|
104
104
|
const hit = integerField(exactlyOneField(lines, 'LH'), 'LH', 0)
|
|
105
105
|
if (hit > found) throw new Error(`LH ${hit} exceeds LF ${found}`)
|
|
106
|
-
if (
|
|
107
|
-
throw new Error(`LF ${found}
|
|
106
|
+
if (lineRecords.size > found) {
|
|
107
|
+
throw new Error(`LF ${found} is less than ${lineRecords.size} unique DA entries`)
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const coveredRecords = [...lineRecords.values()].filter((count) => count > 0).length
|
|
111
|
-
if (hit
|
|
112
|
-
throw new Error(`LH ${hit}
|
|
111
|
+
if (hit < coveredRecords) {
|
|
112
|
+
throw new Error(`LH ${hit} is less than ${coveredRecords} covered DA entries`)
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
return {
|
|
@@ -142,9 +142,9 @@ function listTypeScriptTests(repoRoot) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
function runCoverage() {
|
|
145
|
-
if (process.
|
|
145
|
+
if (!isSupportedNodeVersion(process.versions.node)) {
|
|
146
146
|
throw new Error(
|
|
147
|
-
`plugin-v2 coverage requires Node ${
|
|
147
|
+
`plugin-v2 coverage requires Node.js >= ${MIN_SUPPORTED_NODE_VERSION}; got ${process.version}`,
|
|
148
148
|
)
|
|
149
149
|
}
|
|
150
150
|
|
package/scripts/doctor.mjs
CHANGED
|
@@ -30,6 +30,9 @@ import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
|
30
30
|
import { fileURLToPath } from 'node:url'
|
|
31
31
|
|
|
32
32
|
import { listV2UnsupportedFeatures } from '../src/pantheon/v2-unsupported.mjs'
|
|
33
|
+
import { classifyFileInventory } from './install/file-inventory.mjs'
|
|
34
|
+
import { readState } from './install/state.mjs'
|
|
35
|
+
import { isSupportedNodeVersion, MIN_SUPPORTED_NODE_VERSION } from './node-engine-policy.mjs'
|
|
33
36
|
|
|
34
37
|
// ---------------------------------------------------------------------------
|
|
35
38
|
// Paths
|
|
@@ -47,6 +50,9 @@ const counts = { pass: 0, warn: 0, error: 0, info: 0 }
|
|
|
47
50
|
|
|
48
51
|
const results = []
|
|
49
52
|
let exitCode = 0
|
|
53
|
+
let jsonMode = false
|
|
54
|
+
let jsonOriginalWrite = null
|
|
55
|
+
let jsonArgs = null
|
|
50
56
|
|
|
51
57
|
// ---------------------------------------------------------------------------
|
|
52
58
|
// CLI arg parsing
|
|
@@ -58,6 +64,7 @@ function parseArgs(argv) {
|
|
|
58
64
|
platform: null,
|
|
59
65
|
fix: false,
|
|
60
66
|
verbose: false,
|
|
67
|
+
json: false,
|
|
61
68
|
help: false,
|
|
62
69
|
profile: process.env.PANTHEON_PROFILE ?? 'auto',
|
|
63
70
|
}
|
|
@@ -73,6 +80,9 @@ function parseArgs(argv) {
|
|
|
73
80
|
case '--verbose':
|
|
74
81
|
args.verbose = true
|
|
75
82
|
break
|
|
83
|
+
case '--json':
|
|
84
|
+
args.json = true
|
|
85
|
+
break
|
|
76
86
|
case '--profile':
|
|
77
87
|
args.profile = argv[++i] ?? 'auto'
|
|
78
88
|
break
|
|
@@ -123,22 +133,22 @@ const ICON = {
|
|
|
123
133
|
info: '\u2139\uFE0F', // ℹ️
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
function emit(category, message) {
|
|
136
|
+
function emit(category, message, metadata = {}) {
|
|
127
137
|
counts[category]++
|
|
128
|
-
results.push({ category, message })
|
|
138
|
+
results.push({ category, message, ...metadata })
|
|
129
139
|
if (category === 'pass' && !process.argv.includes('--verbose')) return
|
|
130
140
|
const prefix = ICON[category] ?? category
|
|
131
141
|
console.log(` ${prefix} ${message}`)
|
|
132
142
|
}
|
|
133
143
|
|
|
134
|
-
function pass(msg) {
|
|
135
|
-
emit('pass', msg)
|
|
144
|
+
function pass(msg, metadata = {}) {
|
|
145
|
+
emit('pass', msg, metadata)
|
|
136
146
|
}
|
|
137
|
-
function warn(msg) {
|
|
138
|
-
emit('warn', msg)
|
|
147
|
+
function warn(msg, metadata = {}) {
|
|
148
|
+
emit('warn', msg, metadata)
|
|
139
149
|
}
|
|
140
|
-
function error(msg) {
|
|
141
|
-
emit('error', msg)
|
|
150
|
+
function error(msg, metadata = {}) {
|
|
151
|
+
emit('error', msg, metadata)
|
|
142
152
|
exitCode = 2
|
|
143
153
|
}
|
|
144
154
|
function info(msg) {
|
|
@@ -159,7 +169,8 @@ function showHelp() {
|
|
|
159
169
|
node scripts/doctor.mjs auto-detect, cwd
|
|
160
170
|
node scripts/doctor.mjs --target /path/to/project target project
|
|
161
171
|
node scripts/doctor.mjs --profile global|lite|sandbox profile policy
|
|
162
|
-
|
|
172
|
+
node scripts/doctor.mjs --fix attempt auto-fixes
|
|
173
|
+
node scripts/doctor.mjs --json read-only JSON report
|
|
163
174
|
node scripts/doctor.mjs --verbose detailed output
|
|
164
175
|
node scripts/doctor.mjs --help show this help
|
|
165
176
|
|
|
@@ -1265,6 +1276,7 @@ export function checkInstallVersionDrift(args) {
|
|
|
1265
1276
|
warn(
|
|
1266
1277
|
`Package v${pkgVersion} is newer than the last-synced installation (v${installed}) — ` +
|
|
1267
1278
|
'run `npx pantheon-opencode update` (or re-run init) to refresh config, venv and MCP entries',
|
|
1279
|
+
{ code: 'install_version_drift', classification: 'advisory' },
|
|
1268
1280
|
)
|
|
1269
1281
|
return 'package-newer'
|
|
1270
1282
|
}
|
|
@@ -1272,6 +1284,7 @@ export function checkInstallVersionDrift(args) {
|
|
|
1272
1284
|
warn(
|
|
1273
1285
|
`Installation marker (v${installed}) is newer than this package (v${pkgVersion}) — ` +
|
|
1274
1286
|
'the doctor is running from a different install than the last sync',
|
|
1287
|
+
{ code: 'install_version_drift', classification: 'advisory' },
|
|
1275
1288
|
)
|
|
1276
1289
|
return 'install-newer'
|
|
1277
1290
|
}
|
|
@@ -1841,13 +1854,47 @@ export function classifyNodeSqliteProbe(probe) {
|
|
|
1841
1854
|
}
|
|
1842
1855
|
}
|
|
1843
1856
|
|
|
1857
|
+
/** Classify the Node package-support policy independently of feature probes. */
|
|
1858
|
+
export function classifyNodePackageRuntime(version) {
|
|
1859
|
+
if (isSupportedNodeVersion(version)) {
|
|
1860
|
+
return {
|
|
1861
|
+
status: 'supported',
|
|
1862
|
+
code: 'node_version_supported',
|
|
1863
|
+
message: `Node.js v${String(version).replace(/^v/, '')} meets Pantheon's package requirement (>= ${MIN_SUPPORTED_NODE_VERSION})`,
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return {
|
|
1867
|
+
status: 'unsupported',
|
|
1868
|
+
code: 'node_version_unsupported',
|
|
1869
|
+
message: `Pantheon requires Node.js >= ${MIN_SUPPORTED_NODE_VERSION}; current runtime is v${String(version).replace(/^v/, '')}`,
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1844
1873
|
function checkNodeRuntime() {
|
|
1845
1874
|
section('K. Node Runtime')
|
|
1875
|
+
const packageRuntime = classifyNodePackageRuntime(process.versions.node)
|
|
1876
|
+
if (packageRuntime.status === 'supported') {
|
|
1877
|
+
pass(packageRuntime.message, {
|
|
1878
|
+
code: packageRuntime.code,
|
|
1879
|
+
classification: 'runtime',
|
|
1880
|
+
})
|
|
1881
|
+
} else {
|
|
1882
|
+
error(packageRuntime.message, { code: packageRuntime.code, classification: 'runtime' })
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// This capability check is independent of the package support floor: it
|
|
1886
|
+
// describes pantheon_cost's optional backend, not Pantheon's Node support.
|
|
1846
1887
|
const outcome = classifyNodeSqliteProbe(probeNodeSqlite())
|
|
1847
1888
|
if (outcome.status === 'ok') {
|
|
1848
|
-
pass(outcome.message
|
|
1889
|
+
pass(outcome.message, {
|
|
1890
|
+
code: 'node_sqlite_supported',
|
|
1891
|
+
classification: 'capability',
|
|
1892
|
+
})
|
|
1849
1893
|
} else {
|
|
1850
|
-
warn(outcome.message
|
|
1894
|
+
warn(outcome.message, {
|
|
1895
|
+
code: 'node_sqlite_unsupported',
|
|
1896
|
+
classification: 'capability',
|
|
1897
|
+
})
|
|
1851
1898
|
info(
|
|
1852
1899
|
`Upgrade to Node >= ${NODE_SQLITE_MIN} (or a runtime with node:sqlite) to enable pantheon_cost`,
|
|
1853
1900
|
)
|
|
@@ -1898,22 +1945,156 @@ function printSummary(targetPath) {
|
|
|
1898
1945
|
// Main
|
|
1899
1946
|
// ---------------------------------------------------------------------------
|
|
1900
1947
|
|
|
1948
|
+
async function checkInstallerFileInventory(args) {
|
|
1949
|
+
const state = readState(args.target)
|
|
1950
|
+
if (!state) return
|
|
1951
|
+
const selectedComponents = [
|
|
1952
|
+
'agents',
|
|
1953
|
+
'skills',
|
|
1954
|
+
'instructions',
|
|
1955
|
+
'prompts',
|
|
1956
|
+
'commands',
|
|
1957
|
+
'plugins',
|
|
1958
|
+
'runtime',
|
|
1959
|
+
]
|
|
1960
|
+
let desiredFiles = new Map()
|
|
1961
|
+
let desiredUnavailable = true
|
|
1962
|
+
let selectedLayouts = {}
|
|
1963
|
+
try {
|
|
1964
|
+
const { installOpenCode, isGlobalConfigDir } = await import('./install/opencode.mjs')
|
|
1965
|
+
const preview = await installOpenCode(args.target, true, false, selectedComponents, {
|
|
1966
|
+
version: 'auto',
|
|
1967
|
+
quiet: true,
|
|
1968
|
+
quietPlan: true,
|
|
1969
|
+
planOnly: true,
|
|
1970
|
+
headless: true,
|
|
1971
|
+
})
|
|
1972
|
+
desiredFiles = new Map(
|
|
1973
|
+
(preview?.files ?? [])
|
|
1974
|
+
.filter((file) => typeof file.outputFingerprint === 'string')
|
|
1975
|
+
.map((file) => [file.path, { sha256: file.outputFingerprint }]),
|
|
1976
|
+
)
|
|
1977
|
+
const layout = isGlobalConfigDir(args.target)
|
|
1978
|
+
? 'global'
|
|
1979
|
+
: preview?.openCodeVersion === 'v2'
|
|
1980
|
+
? 'project-v2'
|
|
1981
|
+
: 'project-v1'
|
|
1982
|
+
selectedLayouts = Object.fromEntries(selectedComponents.map((component) => [component, layout]))
|
|
1983
|
+
desiredUnavailable = false
|
|
1984
|
+
} catch {
|
|
1985
|
+
// Doctor remains advisory if a source/layout preview cannot be computed.
|
|
1986
|
+
// The fallback labels equal recorded hashes explicitly, never as current.
|
|
1987
|
+
}
|
|
1988
|
+
let report
|
|
1989
|
+
try {
|
|
1990
|
+
report = classifyFileInventory(args.target, state, {
|
|
1991
|
+
desiredFiles,
|
|
1992
|
+
desiredUnavailable,
|
|
1993
|
+
selectedComponents,
|
|
1994
|
+
selectedLayouts,
|
|
1995
|
+
})
|
|
1996
|
+
} catch {
|
|
1997
|
+
warn('Installer file inventory could not be safely inspected.', {
|
|
1998
|
+
code: 'installer_inventory_unreadable',
|
|
1999
|
+
classification: 'ambiguous',
|
|
2000
|
+
})
|
|
2001
|
+
return
|
|
2002
|
+
}
|
|
2003
|
+
if (report.legacyAmbiguous) {
|
|
2004
|
+
warn('Legacy installed files have no ownership inventory; cleanup requires manual review.', {
|
|
2005
|
+
code: 'installer_inventory_legacy_unknown',
|
|
2006
|
+
classification: 'ambiguous',
|
|
2007
|
+
})
|
|
2008
|
+
}
|
|
2009
|
+
const recordedCurrent = report.entries.filter(
|
|
2010
|
+
(entry) => entry.classification === 'recorded-current',
|
|
2011
|
+
).length
|
|
2012
|
+
if (recordedCurrent > 0) {
|
|
2013
|
+
emit(
|
|
2014
|
+
'info',
|
|
2015
|
+
`${recordedCurrent} inventoried file(s) match their recorded install hashes; current package payload was not compared.`,
|
|
2016
|
+
{ code: 'installer_file_recorded_current', classification: 'recorded-current' },
|
|
2017
|
+
)
|
|
2018
|
+
}
|
|
2019
|
+
for (const entry of report.entries) {
|
|
2020
|
+
if (entry.classification === 'modified') {
|
|
2021
|
+
warn(`Installer-owned file was modified and will be preserved: ${entry.path}`, {
|
|
2022
|
+
code: 'installer_file_modified',
|
|
2023
|
+
classification: 'modified',
|
|
2024
|
+
})
|
|
2025
|
+
} else if (entry.classification === 'missing') {
|
|
2026
|
+
warn(`Previously installer-owned file is missing: ${entry.path}`, {
|
|
2027
|
+
code: 'installer_file_missing',
|
|
2028
|
+
classification: 'missing',
|
|
2029
|
+
})
|
|
2030
|
+
} else if (entry.classification === 'update-drift') {
|
|
2031
|
+
warn(`Installer source has a newer version of an unchanged owned file: ${entry.path}`, {
|
|
2032
|
+
code: 'installer_file_update_available',
|
|
2033
|
+
classification: 'update-drift',
|
|
2034
|
+
})
|
|
2035
|
+
} else if (entry.classification === 'stale-owned') {
|
|
2036
|
+
warn(`Stale installer-owned file is eligible for reviewed cleanup: ${entry.path}`, {
|
|
2037
|
+
code: 'installer_file_stale_owned',
|
|
2038
|
+
classification: 'stale-owned',
|
|
2039
|
+
})
|
|
2040
|
+
} else if (entry.classification === 'ambiguous') {
|
|
2041
|
+
warn(`Installer ownership is ambiguous for ${entry.path}; file preserved.`, {
|
|
2042
|
+
code: 'installer_file_ambiguous',
|
|
2043
|
+
classification: 'ambiguous',
|
|
2044
|
+
})
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
1901
2049
|
async function main() {
|
|
1902
2050
|
const args = parseArgs(process.argv)
|
|
2051
|
+
jsonMode = args.json
|
|
2052
|
+
jsonArgs = args
|
|
2053
|
+
if (jsonMode) {
|
|
2054
|
+
jsonOriginalWrite = process.stdout.write
|
|
2055
|
+
process.stdout.write = () => true
|
|
2056
|
+
}
|
|
1903
2057
|
|
|
1904
2058
|
if (args.help) {
|
|
2059
|
+
if (jsonMode) {
|
|
2060
|
+
process.stdout.write = jsonOriginalWrite
|
|
2061
|
+
jsonOriginalWrite = null
|
|
2062
|
+
}
|
|
1905
2063
|
showHelp()
|
|
1906
|
-
process.
|
|
2064
|
+
process.exitCode = 0
|
|
2065
|
+
return
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
if (args.json && args.fix) {
|
|
2069
|
+
writeDoctorJson(
|
|
2070
|
+
{
|
|
2071
|
+
schema_version: 1,
|
|
2072
|
+
command: 'doctor',
|
|
2073
|
+
status: 'error',
|
|
2074
|
+
target: resolve(args.target),
|
|
2075
|
+
profile: args.profile,
|
|
2076
|
+
summary: { ...counts, exit_code: 1 },
|
|
2077
|
+
findings: [{ category: 'error', code: 'json_fix_unsupported', classification: 'usage' }],
|
|
2078
|
+
},
|
|
2079
|
+
1,
|
|
2080
|
+
)
|
|
2081
|
+
return
|
|
1907
2082
|
}
|
|
1908
2083
|
|
|
1909
2084
|
// Report the resolved validation profile up front (auto → global/sandbox)
|
|
1910
2085
|
// so callers — including the `doctor` CLI wrapper — can see which policy
|
|
1911
2086
|
// gate applied before any check output.
|
|
1912
|
-
console.log(`Validation profile: ${args.profile}`)
|
|
2087
|
+
if (!jsonMode) console.log(`Validation profile: ${args.profile}`)
|
|
1913
2088
|
|
|
1914
2089
|
if (!existsSync(args.target)) {
|
|
1915
|
-
|
|
1916
|
-
|
|
2090
|
+
if (jsonMode) {
|
|
2091
|
+
error(`Target directory does not exist: ${args.target}`)
|
|
2092
|
+
writeDoctorJson(createDoctorJsonReport(args), exitCode)
|
|
2093
|
+
} else {
|
|
2094
|
+
console.error(`❌ Target directory does not exist: ${args.target}`)
|
|
2095
|
+
process.exitCode = 2
|
|
2096
|
+
}
|
|
2097
|
+
return
|
|
1917
2098
|
}
|
|
1918
2099
|
|
|
1919
2100
|
// Verify it looks like a Pantheon project or an OpenCode install target.
|
|
@@ -1944,7 +2125,14 @@ async function main() {
|
|
|
1944
2125
|
checkCodeModeManifest(args)
|
|
1945
2126
|
checkInstallVersionDrift(args)
|
|
1946
2127
|
checkPluginVersionDrift(args)
|
|
1947
|
-
await
|
|
2128
|
+
await checkInstallerFileInventory(args)
|
|
2129
|
+
if (jsonMode) {
|
|
2130
|
+
info('MCP runtime smoke skipped to keep --json read-only')
|
|
2131
|
+
results.at(-1).code = 'mcp_runtime_smoke_skipped'
|
|
2132
|
+
results.at(-1).classification = 'not_run'
|
|
2133
|
+
} else {
|
|
2134
|
+
await checkMcpRuntimeSmoke(args)
|
|
2135
|
+
}
|
|
1948
2136
|
checkPermissionMismatches(args)
|
|
1949
2137
|
checkSyncStatus(args)
|
|
1950
2138
|
checkGitStatus(args)
|
|
@@ -1954,13 +2142,48 @@ async function main() {
|
|
|
1954
2142
|
checkV2UnsupportedFeatures(args)
|
|
1955
2143
|
|
|
1956
2144
|
printSummary(args.target)
|
|
2145
|
+
if (jsonMode) writeDoctorJson(createDoctorJsonReport(args), exitCode)
|
|
2146
|
+
else process.exitCode = exitCode
|
|
2147
|
+
}
|
|
1957
2148
|
|
|
1958
|
-
|
|
2149
|
+
function createDoctorJsonReport(args) {
|
|
2150
|
+
return {
|
|
2151
|
+
schema_version: 1,
|
|
2152
|
+
command: 'doctor',
|
|
2153
|
+
status: exitCode === 2 ? 'error' : counts.warn > 0 ? 'warning' : 'ok',
|
|
2154
|
+
target: resolve(args.target),
|
|
2155
|
+
profile: args.profile,
|
|
2156
|
+
summary: { ...counts, exit_code: exitCode },
|
|
2157
|
+
findings: results,
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
function writeDoctorJson(report, code) {
|
|
2162
|
+
const writer = jsonOriginalWrite ?? process.stdout.write
|
|
2163
|
+
process.stdout.write = writer
|
|
2164
|
+
jsonOriginalWrite = null
|
|
2165
|
+
writer.call(process.stdout, `${JSON.stringify(report)}\n`)
|
|
2166
|
+
process.exitCode = code
|
|
1959
2167
|
}
|
|
1960
2168
|
|
|
1961
2169
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
1962
2170
|
main().catch((err) => {
|
|
1963
|
-
|
|
1964
|
-
|
|
2171
|
+
if (jsonMode) {
|
|
2172
|
+
writeDoctorJson(
|
|
2173
|
+
{
|
|
2174
|
+
schema_version: 1,
|
|
2175
|
+
command: 'doctor',
|
|
2176
|
+
status: 'error',
|
|
2177
|
+
target: jsonArgs ? resolve(jsonArgs.target) : null,
|
|
2178
|
+
profile: jsonArgs?.profile ?? null,
|
|
2179
|
+
summary: { ...counts, exit_code: 2 },
|
|
2180
|
+
findings: [{ category: 'error', code: 'doctor_failure', classification: 'error' }],
|
|
2181
|
+
},
|
|
2182
|
+
2,
|
|
2183
|
+
)
|
|
2184
|
+
} else {
|
|
2185
|
+
console.error(`❌ Doctor crashed: ${err.stack ?? err.message}`)
|
|
2186
|
+
process.exitCode = 2
|
|
2187
|
+
}
|
|
1965
2188
|
})
|
|
1966
2189
|
}
|