pantheon-opencode 1.6.0-beta.11 → 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.
Files changed (50) hide show
  1. package/AGENTS.global.md +3 -3
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +69 -30
  5. package/README.pt-BR.md +5 -4
  6. package/bin/pantheon-init.mjs +279 -101
  7. package/package.json +4 -3
  8. package/plugin.json +1 -1
  9. package/pyproject.toml +1 -1
  10. package/scripts/build-agents-md.mjs +77 -13
  11. package/scripts/check-plugin-v2-coverage.mjs +7 -7
  12. package/scripts/doctor.mjs +242 -19
  13. package/scripts/install/backup.mjs +191 -18
  14. package/scripts/install/cli-ui.mjs +73 -0
  15. package/scripts/install/file-inventory.mjs +555 -0
  16. package/scripts/install/migrate.mjs +45 -38
  17. package/scripts/install/model-picker.mjs +63 -22
  18. package/scripts/install/opencode-version.mjs +9 -1
  19. package/scripts/install/opencode.mjs +265 -79
  20. package/scripts/install/plan-artifact.mjs +517 -0
  21. package/scripts/install/plan.mjs +165 -3
  22. package/scripts/install/plugin.mjs +118 -14
  23. package/scripts/install/shared.mjs +186 -65
  24. package/scripts/install/state.mjs +63 -8
  25. package/scripts/install/venv.mjs +15 -6
  26. package/scripts/install/verify.mjs +128 -0
  27. package/scripts/install/vision-prompt.mjs +2 -0
  28. package/scripts/install-mcp.mjs +1 -1
  29. package/scripts/manifest-inventory.mjs +49 -3
  30. package/scripts/node-engine-policy.mjs +74 -0
  31. package/scripts/package-evidence.mjs +6 -0
  32. package/scripts/postinstall.mjs +3 -4
  33. package/scripts/reconcile.mjs +325 -0
  34. package/scripts/uninstall.mjs +258 -34
  35. package/scripts/update.mjs +164 -0
  36. package/scripts/validate-node-engine-policy.mjs +13 -0
  37. package/scripts/version-check.mjs +34 -1
  38. package/scripts/versioning.mjs +9 -1
  39. package/skills-lock.json +3 -3
  40. package/src/agents/mnemosyne.md +7 -2
  41. package/src/instructions/memory-protocol.instructions.md +2 -2
  42. package/src/instructions/zeus-memory-operations.instructions.md +4 -3
  43. package/src/pantheon/v2-unsupported.mjs +2 -3
  44. package/src/plugins/tui/dist/tui-v2.js +2 -2
  45. package/src/plugins/tui/dist/tui.js +1 -1
  46. package/src/plugins/tui/package-lock.json +3 -3
  47. package/src/plugins/tui/package.json +2 -2
  48. package/src/skills/artifact-management/SKILL.md +5 -1
  49. package/src/skills/memory-bank/SKILL.md +5 -1
  50. package/src/skills/worktrees/SKILL.md +30 -11
@@ -20,15 +20,25 @@ import fs from 'node:fs'
20
20
  import os from 'node:os'
21
21
  import path from 'node:path'
22
22
  import { fileURLToPath } from 'node:url'
23
-
24
23
  import { resolveInstallExitCode } from '../scripts/install/exit-codes.mjs'
24
+ import {
25
+ createPlanArtifact,
26
+ loadPlanArtifact,
27
+ savePlanArtifact,
28
+ } from '../scripts/install/plan-artifact.mjs'
29
+ import {
30
+ isSupportedNodeVersion,
31
+ MIN_SUPPORTED_NODE_VERSION,
32
+ } from '../scripts/node-engine-policy.mjs'
33
+ import { runUpdate } from '../scripts/update.mjs'
25
34
 
26
35
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
27
36
  const ROOT = path.resolve(__dirname, '..')
28
37
 
29
- const REQUIRED_NODE_MAJOR = 22
30
- if (parseInt(process.versions.node.split('.')[0], 10) < REQUIRED_NODE_MAJOR) {
31
- console.error(`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`)
38
+ if (!isSupportedNodeVersion(process.versions.node)) {
39
+ console.error(
40
+ `❌ Pantheon requires Node.js >= ${MIN_SUPPORTED_NODE_VERSION} (current: ${process.versions.node})`,
41
+ )
32
42
  console.error(' Install a newer Node via nvm: https://github.com/nvm-sh/nvm')
33
43
  process.exit(1)
34
44
  }
@@ -49,6 +59,9 @@ function printUsage() {
49
59
  console.log(' npx pantheon-opencode init # Install globally')
50
60
  console.log(' npx pantheon-opencode init --project # Install in project')
51
61
  console.log(' npx pantheon-opencode init --dry-run # Preview only')
62
+ console.log(' npx pantheon-opencode init --dry-run --save-plan <file> # Save reviewed plan')
63
+ console.log(' npx pantheon-opencode init --apply-plan <file> [--yes] # Apply reviewed plan')
64
+ console.log(' npx pantheon-opencode init --dry-run --json # Versioned JSON preview')
52
65
  console.log(' npx pantheon-opencode init --no-mcp # Skip MCP + venv')
53
66
  console.log(' npx pantheon-opencode init --force # Overwrite + recreate venv')
54
67
  console.log(' npx pantheon-opencode init --doctor # Run health check after install')
@@ -76,6 +89,7 @@ function printUsage() {
76
89
  )
77
90
  console.log(' npx pantheon-opencode uninstall --project # Remove project artifacts safely')
78
91
  console.log(' npx pantheon-opencode uninstall --global # Remove global artifacts safely')
92
+ console.log(' npx pantheon-opencode reconcile --project # Review stale owned installer files')
79
93
  console.log(' npx pantheon-opencode --version, -v # Print version and exit')
80
94
  console.log(' npx pantheon-opencode --help # Show this help')
81
95
  console.log('')
@@ -91,6 +105,22 @@ function printUsage() {
91
105
  console.log(' 130 canceled')
92
106
  }
93
107
 
108
+ async function authorizeReviewedPlan(plan) {
109
+ const operations = plan.operations.filter((operation) => operation.action !== 'unchanged')
110
+ console.log(`Reviewed plan: ${plan.target.path} (${operations.length} change(s))`)
111
+ for (const operation of operations) {
112
+ console.log(` ${operation.action.padEnd(9)} ${operation.path}`)
113
+ }
114
+ const readline = await import('node:readline/promises')
115
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout })
116
+ try {
117
+ const answer = await prompt.question('Apply this reviewed plan? [y/N] ')
118
+ return ['y', 'yes'].includes(answer.trim().toLowerCase())
119
+ } finally {
120
+ prompt.close()
121
+ }
122
+ }
123
+
94
124
  async function main() {
95
125
  const args = process.argv.slice(2)
96
126
  const command = args[0]
@@ -125,6 +155,16 @@ async function main() {
125
155
  process.exit(result.status ?? 1)
126
156
  }
127
157
 
158
+ if (command === 'reconcile') {
159
+ const { spawnSync } = await import('node:child_process')
160
+ const reconcileScript = path.join(ROOT, 'scripts', 'reconcile.mjs')
161
+ const result = spawnSync(process.execPath, [reconcileScript, ...args.slice(1)], {
162
+ stdio: 'inherit',
163
+ cwd: process.cwd(),
164
+ })
165
+ process.exit(result.status ?? 1)
166
+ }
167
+
128
168
  // restore delegates to scripts/restore.mjs (same spawnSync pattern as prune).
129
169
  if (command === 'restore') {
130
170
  const { spawnSync } = await import('node:child_process')
@@ -152,72 +192,12 @@ async function main() {
152
192
  // beta.5: one-command update — check npm dist-tag, install, re-init.
153
193
  // Default channel is `beta` (the prerelease line); `--stable` uses `latest`.
154
194
  if (command === 'update') {
155
- const channel = args.includes('--stable') ? 'latest' : 'beta'
156
- const current = readVersion()
157
- const { spawnSync } = await import('node:child_process')
158
- const _S = (await import('../scripts/install/strings.mjs')).strings()
159
-
160
- console.log(`Pantheon OpenCode v${current} — checking the "${channel}" channel...`)
161
- const view = spawnSync('npm', ['view', `pantheon-opencode@${channel}`, 'version'], {
162
- encoding: 'utf8',
163
- timeout: 30_000,
195
+ const status = runUpdate(args, {
196
+ root: ROOT,
197
+ currentVersion: readVersion(),
198
+ runningScript: process.argv[1],
164
199
  })
165
- const latest = (view.stdout || '').trim().split('\n').filter(Boolean).pop()
166
- if (view.status !== 0 || !latest) {
167
- console.error(
168
- `❌ Could not reach the npm registry: ${(view.stderr || view.stdout || '').trim() || 'no version returned'}`,
169
- )
170
- process.exit(1)
171
- }
172
-
173
- // Prerelease-aware compare: core (major.minor.patch) first, then the
174
- // beta counter; a stable release outranks any beta of the same core.
175
- const parsePantheonVersion = (v) => {
176
- const m = String(v)
177
- .trim()
178
- .match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/)
179
- if (!m) return null
180
- return {
181
- core: [+m[1], +m[2], +m[3]],
182
- beta: m[4] === undefined ? Number.POSITIVE_INFINITY : +m[4],
183
- }
184
- }
185
- const cmp = (a, b) => {
186
- const pa = parsePantheonVersion(a)
187
- const pb = parsePantheonVersion(b)
188
- if (!pa || !pb) return 0
189
- for (let i = 0; i < 3; i++) {
190
- if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
191
- }
192
- return pa.beta - pb.beta
193
- }
194
- if (cmp(latest, current) <= 0) {
195
- console.log(`✅ Already up to date (installed v${current}; ${channel} channel: v${latest}).`)
196
- return
197
- }
198
-
199
- console.log(`Updating v${current} → v${latest} (channel: ${channel})...`)
200
- const install = spawnSync('npm', ['install', '-g', `pantheon-opencode@${channel}`], {
201
- stdio: 'inherit',
202
- })
203
- if (install.status !== 0) {
204
- console.error('❌ npm install failed — the previous installation is untouched.')
205
- process.exit(install.status ?? 1)
206
- }
207
-
208
- // Refresh config/venv/MCP entries with the NEW package (resolved via
209
- // PATH — the freshly installed global bin).
210
- const init = spawnSync('pantheon-opencode', ['init', '--yes', '--headless'], {
211
- stdio: 'inherit',
212
- shell: process.platform === 'win32',
213
- })
214
- if (init.status !== 0 || init.error) {
215
- console.error(
216
- '⚠️ Updated, but the config refresh did not complete — run `npx pantheon-opencode init` manually.',
217
- )
218
- process.exit(init.status ?? 1)
219
- }
220
- console.log(`✅ Updated to v${latest}.`)
200
+ if (status !== 0) process.exit(status)
221
201
  return
222
202
  }
223
203
 
@@ -301,15 +281,89 @@ async function main() {
301
281
  }
302
282
 
303
283
  if (command === 'init' || !command) {
304
- const isProject = args.includes('--project')
305
- const isDryRun = args.includes('--dry-run')
284
+ const applyPlanIndex = args.indexOf('--apply-plan')
285
+ const inlineApplyPlan = args.find((arg) => arg.startsWith('--apply-plan='))
286
+ const applyPlanPath =
287
+ applyPlanIndex >= 0
288
+ ? (args[applyPlanIndex + 1] ?? null)
289
+ : (inlineApplyPlan?.split('=', 2)[1] ?? null)
290
+ if (applyPlanIndex >= 0 && !applyPlanPath) {
291
+ console.error('❌ --apply-plan requires a plan file path')
292
+ process.exit(1)
293
+ }
294
+ let reviewedPlan = null
295
+ if (applyPlanPath !== null) {
296
+ const allowed = new Set(['init', '--apply-plan', applyPlanPath, '--yes', '-y', '--headless'])
297
+ const normalized = args.map((value) =>
298
+ value.startsWith('--apply-plan=') ? '--apply-plan' : value,
299
+ )
300
+ if (normalized.some((value) => !allowed.has(value))) {
301
+ console.error('❌ --apply-plan accepts only the plan path and fresh authorization flags')
302
+ process.exit(1)
303
+ }
304
+ reviewedPlan = loadPlanArtifact(applyPlanPath)
305
+ if (
306
+ reviewedPlan.artifact_path &&
307
+ path.resolve(applyPlanPath) !== reviewedPlan.artifact_path
308
+ ) {
309
+ console.error('❌ The reviewed plan artifact was moved; use its original path')
310
+ process.exit(1)
311
+ }
312
+ const options = reviewedPlan.options
313
+ if (
314
+ !Array.isArray(options.components) ||
315
+ options.components.some(
316
+ (component) =>
317
+ !['agents', 'skills', 'instructions', 'commands', 'plugins', 'runtime'].includes(
318
+ component,
319
+ ),
320
+ ) ||
321
+ options.clean !== false ||
322
+ !['v1', 'v2', 'auto'].includes(options.version) ||
323
+ typeof options.isProject !== 'boolean'
324
+ ) {
325
+ console.error('❌ Install-plan options are invalid or unsupported')
326
+ process.exit(1)
327
+ }
328
+ }
329
+ const isProject = reviewedPlan ? reviewedPlan.options.isProject : args.includes('--project')
330
+ const isDryRun = reviewedPlan ? false : args.includes('--dry-run')
306
331
  const skipMCP = args.includes('--no-mcp')
307
332
  // --clean is an alias of --force (both wipe component dirs + recreate venv).
308
- const forceReinstall = args.includes('--force') || args.includes('--clean')
309
- const runDoctor = args.includes('--doctor')
310
- const forceInteractive = args.includes('--interactive')
333
+ const forceReinstall = reviewedPlan
334
+ ? reviewedPlan.options.clean
335
+ : args.includes('--force') || args.includes('--clean')
336
+ const runDoctor = !reviewedPlan && args.includes('--doctor')
337
+ const forceInteractive = !reviewedPlan && args.includes('--interactive')
311
338
  const forceHeadless = args.includes('--headless')
312
- const autoYes = args.includes('--yes') || args.includes('-y')
339
+ let autoYes = args.includes('--yes') || args.includes('-y')
340
+ const isJson = args.includes('--json')
341
+ const savePlanIndex = args.indexOf('--save-plan')
342
+ const inlineSavePlan = args.find((arg) => arg.startsWith('--save-plan='))
343
+ const savePlanPath =
344
+ savePlanIndex >= 0
345
+ ? (args[savePlanIndex + 1] ?? null)
346
+ : (inlineSavePlan?.split('=', 2)[1] ?? null)
347
+ if (savePlanIndex >= 0 && !savePlanPath) {
348
+ console.error('❌ --save-plan requires a plan file path')
349
+ process.exit(1)
350
+ }
351
+ if (savePlanPath !== null && (!isDryRun || reviewedPlan)) {
352
+ console.error('❌ --save-plan is available only with init --dry-run')
353
+ process.exit(1)
354
+ }
355
+ if (savePlanPath !== null && forceReinstall) {
356
+ console.error('❌ Saving a reviewed plan with --force/--clean is not supported safely')
357
+ process.exit(1)
358
+ }
359
+ if (isJson && (!isDryRun || reviewedPlan)) {
360
+ console.error('❌ --json is supported only with init --dry-run')
361
+ process.exit(1)
362
+ }
363
+ if (isJson && forceReinstall) {
364
+ console.error('❌ --force/--clean cleanup is not supported with --json')
365
+ process.exit(1)
366
+ }
313
367
 
314
368
  // beta.5: warn on unrecognized flags instead of silently ignoring them.
315
369
  const VALUE_FLAGS = new Set([
@@ -320,6 +374,8 @@ async function main() {
320
374
  '--opencode-version',
321
375
  '--components',
322
376
  '--merge-dev-config',
377
+ '--apply-plan',
378
+ '--save-plan',
323
379
  ])
324
380
  for (let i = 1; i < args.length; i++) {
325
381
  const arg = args[i]
@@ -342,6 +398,7 @@ async function main() {
342
398
  '--headless',
343
399
  '--yes',
344
400
  '-y',
401
+ '--json',
345
402
  ].includes(bare)
346
403
  ) {
347
404
  console.error(`⚠️ Unknown option ignored: ${arg}`)
@@ -349,20 +406,33 @@ async function main() {
349
406
  }
350
407
 
351
408
  const presetIndex = args.indexOf('--preset')
352
- const presetOpt = presetIndex >= 0 ? (args[presetIndex + 1] ?? null) : null
409
+ const presetOpt = reviewedPlan
410
+ ? (reviewedPlan.options.preset ?? null)
411
+ : presetIndex >= 0
412
+ ? (args[presetIndex + 1] ?? null)
413
+ : null
353
414
  // Explicit top-level model overrides for the generated config.
354
415
  const modelIndex = args.indexOf('--model')
355
- const modelOpt = modelIndex >= 0 ? (args[modelIndex + 1] ?? null) : null
416
+ const modelOpt = reviewedPlan
417
+ ? (reviewedPlan.options.model ?? null)
418
+ : modelIndex >= 0
419
+ ? (args[modelIndex + 1] ?? null)
420
+ : null
356
421
  const smallModelIndex = args.indexOf('--small-model')
357
- const smallModelOpt = smallModelIndex >= 0 ? (args[smallModelIndex + 1] ?? null) : null
422
+ const smallModelOpt = reviewedPlan
423
+ ? (reviewedPlan.options.smallModel ?? null)
424
+ : smallModelIndex >= 0
425
+ ? (args[smallModelIndex + 1] ?? null)
426
+ : null
358
427
  // --version v1|v2|auto labels the install target. `auto` resolves the
359
428
  // generation from the opencode binary at install time (see
360
429
  // scripts/install/opencode-version.mjs). Invalid values fail fast.
361
430
  const versionIndex = args.indexOf('--version')
362
431
  const legacyVersionIndex = args.indexOf('--opencode-version')
363
432
  const inlineVersion = args.find((arg) => arg.startsWith('--opencode-version='))
364
- const versionOpt =
365
- versionIndex >= 0
433
+ const versionOpt = reviewedPlan
434
+ ? reviewedPlan.options.version
435
+ : versionIndex >= 0
366
436
  ? (args[versionIndex + 1] ?? null)
367
437
  : legacyVersionIndex >= 0
368
438
  ? (args[legacyVersionIndex + 1] ?? null)
@@ -386,8 +456,8 @@ async function main() {
386
456
  componentsIndex >= 0
387
457
  ? (args[componentsIndex + 1] ?? null)
388
458
  : (inlineComponents?.split('=', 2)[1] ?? null)
389
- let requestedComponents = null
390
- if (componentsOpt !== null) {
459
+ let requestedComponents = reviewedPlan ? [...reviewedPlan.options.components] : null
460
+ if (!reviewedPlan && componentsOpt !== null) {
391
461
  requestedComponents = componentsOpt
392
462
  .split(',')
393
463
  .map((c) => c.trim())
@@ -408,8 +478,9 @@ async function main() {
408
478
  // from the installer code, never from a dev checkout).
409
479
  const mergeDevConfigIndex = args.indexOf('--merge-dev-config')
410
480
  const inlineMergeDevConfig = args.find((arg) => arg.startsWith('--merge-dev-config='))
411
- const mergeDevConfigOpt =
412
- mergeDevConfigIndex >= 0
481
+ const mergeDevConfigOpt = reviewedPlan
482
+ ? (reviewedPlan.options.mergeDevConfig ?? null)
483
+ : mergeDevConfigIndex >= 0
413
484
  ? (args[mergeDevConfigIndex + 1] ?? null)
414
485
  : (inlineMergeDevConfig?.split('=', 2)[1] ?? null)
415
486
  if (mergeDevConfigIndex >= 0 && mergeDevConfigOpt === null) {
@@ -417,15 +488,24 @@ async function main() {
417
488
  process.exit(1)
418
489
  }
419
490
 
420
- const components = requestedComponents ?? [
421
- 'agents',
422
- 'skills',
423
- 'instructions',
424
- 'commands',
425
- 'plugins',
426
- ]
427
- if (!skipMCP && !components.includes('runtime')) components.push('runtime')
428
- if (skipMCP) {
491
+ if (reviewedPlan && !autoYes) {
492
+ if (!forceHeadless && process.stdin.isTTY && process.stdout.isTTY) {
493
+ if (!(await authorizeReviewedPlan(reviewedPlan))) {
494
+ console.log('❌ Install canceled; no files were written.')
495
+ return
496
+ }
497
+ } else {
498
+ console.error('❌ Applying a reviewed plan requires --yes or an interactive TTY prompt.')
499
+ process.exit(2)
500
+ }
501
+ autoYes = true
502
+ }
503
+
504
+ const components = reviewedPlan
505
+ ? requestedComponents
506
+ : (requestedComponents ?? ['agents', 'skills', 'instructions', 'commands', 'plugins'])
507
+ if (!reviewedPlan && !skipMCP && !components.includes('runtime')) components.push('runtime')
508
+ if (!reviewedPlan && skipMCP) {
429
509
  const runtimeIndex = components.indexOf('runtime')
430
510
  if (runtimeIndex >= 0) components.splice(runtimeIndex, 1)
431
511
  }
@@ -435,33 +515,131 @@ async function main() {
435
515
  const { strings } = await import('../scripts/install/strings.mjs')
436
516
  const S = strings()
437
517
 
438
- console.log('')
439
- if (!forceInteractive || isDryRun) {
440
- console.log(`Pantheon OpenCode v${version} — ${isDryRun ? S.dryRun : S.installing}`)
518
+ const planTimestamp = reviewedPlan?.planned_at ?? new Date().toISOString()
519
+ const stdoutWrite = process.stdout.write
520
+ if (isJson) process.stdout.write = () => true
521
+ if (!isJson) {
441
522
  console.log('')
523
+ if (!forceInteractive || isDryRun) {
524
+ console.log(`Pantheon OpenCode v${version} — ${isDryRun ? S.dryRun : S.installing}`)
525
+ console.log('')
526
+ }
442
527
  }
443
528
 
444
529
  let result
530
+ let installError = null
445
531
  try {
446
532
  const { installOpenCode } = await import('../scripts/install/opencode.mjs')
447
- const target = isProject ? process.cwd() : undefined
533
+ const target = reviewedPlan ? reviewedPlan.target.path : isProject ? process.cwd() : undefined
448
534
  result = await installOpenCode(target, isDryRun, forceReinstall, components, {
449
535
  interactive: forceInteractive,
450
- headless: forceHeadless,
536
+ headless: reviewedPlan ? true : forceHeadless,
451
537
  yes: autoYes,
452
538
  model: modelOpt,
453
539
  smallModel: smallModelOpt,
454
540
  preset: presetOpt,
455
541
  version: versionOpt ?? 'auto',
456
542
  mergeDevConfig: mergeDevConfigOpt,
543
+ expectedPlan: reviewedPlan,
544
+ planTimestamp,
545
+ quiet: isJson,
457
546
  })
458
547
  } catch (err) {
459
- if (err?.message === 'Canceled') {
548
+ installError = err
549
+ } finally {
550
+ if (isJson) process.stdout.write = stdoutWrite
551
+ }
552
+ if (installError) {
553
+ if (isJson) {
554
+ process.stdout.write(
555
+ `${JSON.stringify({
556
+ schema_version: 1,
557
+ command: 'init',
558
+ mode: 'dry-run',
559
+ status: 'error',
560
+ error: installError.message,
561
+ })}\n`,
562
+ )
563
+ console.error(`❌ Init dry-run failed: ${installError.message}`)
564
+ } else if (installError?.message === 'Canceled') {
460
565
  console.error(S.canceled)
461
566
  process.exit(130)
567
+ } else {
568
+ console.error(
569
+ `${S.installFailed(installError.message)}\n${S.failHintNoMcp}\n${S.failHintForce}`,
570
+ )
462
571
  }
463
- console.error(`${S.installFailed(err.message)}\n${S.failHintNoMcp}\n${S.failHintForce}`)
464
- process.exit(1)
572
+ process.exitCode = 1
573
+ return
574
+ }
575
+
576
+ if (savePlanPath !== null && result?.reason === 'plan') {
577
+ try {
578
+ const artifact = createPlanArtifact({
579
+ target: result.target,
580
+ packageRoot: ROOT,
581
+ packageVersion: version,
582
+ plannedAt: planTimestamp,
583
+ artifactPath: savePlanPath,
584
+ options: {
585
+ clean: forceReinstall,
586
+ components,
587
+ version: result.openCodeVersion,
588
+ isProject,
589
+ model: modelOpt,
590
+ smallModel: smallModelOpt,
591
+ preset: presetOpt,
592
+ mergeDevConfig: mergeDevConfigOpt,
593
+ },
594
+ files: result.files,
595
+ snapshots: result.snapshots,
596
+ })
597
+ savePlanArtifact(savePlanPath, artifact)
598
+ } catch (error) {
599
+ if (!isJson) throw error
600
+ process.stdout.write(
601
+ `${JSON.stringify({
602
+ schema_version: 1,
603
+ command: 'init',
604
+ mode: 'dry-run',
605
+ status: 'error',
606
+ error: error.message,
607
+ })}\n`,
608
+ )
609
+ console.error(`❌ Could not save reviewed install plan: ${error.message}`)
610
+ process.exitCode = 1
611
+ return
612
+ }
613
+ if (!isJson) console.log(`Reviewed plan saved to ${path.resolve(savePlanPath)} (mode 0600).`)
614
+ }
615
+
616
+ if (isJson) {
617
+ const files = result.files ?? []
618
+ const totals = { create: 0, update: 0, remove: 0, unchanged: 0, conflict: 0 }
619
+ for (const file of files) {
620
+ if (Object.hasOwn(totals, file.action)) totals[file.action] += 1
621
+ }
622
+ process.stdout.write(
623
+ `${JSON.stringify({
624
+ schema_version: 1,
625
+ command: 'init',
626
+ mode: 'dry-run',
627
+ status: 'planned',
628
+ target: result.target,
629
+ open_code_version: result.openCodeVersion,
630
+ advisories: result.versionAssumed
631
+ ? ['OpenCode host probe skipped to keep dry-run write-free; V1 was assumed.']
632
+ : [],
633
+ totals,
634
+ files: files.map(({ path: filePath, action, bytes }) => ({
635
+ path: path.relative(result.target, filePath) || '.',
636
+ action,
637
+ bytes,
638
+ })),
639
+ saved_plan: savePlanPath ? path.resolve(savePlanPath) : null,
640
+ })}\n`,
641
+ )
642
+ return
465
643
  }
466
644
 
467
645
  // Beta 11 Setup phases 3-4: a run that did NOT apply (declined /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.6.0-beta.11",
3
+ "version": "1.6.0-beta.12",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -33,7 +33,8 @@
33
33
  "version:check": "node scripts/version-check.mjs",
34
34
  "release:dry-run": "node scripts/versioning.mjs status && node scripts/release-notes.mjs && node scripts/package-evidence.mjs",
35
35
  "package:evidence": "node scripts/package-evidence.mjs",
36
- "package:validate": "node scripts/validate-package.mjs",
36
+ "engine:check": "node scripts/validate-node-engine-policy.mjs",
37
+ "package:validate": "node scripts/validate-package.mjs && npm run engine:check",
37
38
  "lint": "biome ci .",
38
39
  "typecheck": "tsc --noEmit",
39
40
  "audit": "npm audit --audit-level=high",
@@ -138,7 +139,7 @@
138
139
  "author": "ils15",
139
140
  "license": "MIT",
140
141
  "engines": {
141
- "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
142
+ "node": ">=26.4.0"
142
143
  },
143
144
  "preferGlobal": true
144
145
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon",
3
3
  "publisher": "ils15",
4
- "version": "1.6.0-beta.11",
4
+ "version": "1.6.0-beta.12",
5
5
  "displayName": "Pantheon",
6
6
  "description": "Conductor-Delegate orchestration platform with 14 specialized agents, 20 skills, and OpenCode support.",
7
7
  "agents": "./src/agents",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
4
4
 
5
5
  [project]
6
6
  name = "pantheon"
7
- version = "1.6.0-beta.11"
7
+ version = "1.6.0-beta.12"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []