pantheon-opencode 1.6.0-beta.11 → 1.6.0-beta.13

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 (53) hide show
  1. package/AGENTS.global.md +3 -3
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +39 -0
  4. package/README.md +85 -31
  5. package/README.pt-BR.md +7 -4
  6. package/bin/pantheon-init.mjs +278 -102
  7. package/package.json +5 -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/interactive.mjs +69 -391
  17. package/scripts/install/migrate.mjs +45 -38
  18. package/scripts/install/model-picker.mjs +63 -22
  19. package/scripts/install/opencode-version.mjs +9 -1
  20. package/scripts/install/opencode.mjs +279 -79
  21. package/scripts/install/plan-artifact.mjs +517 -0
  22. package/scripts/install/plan.mjs +165 -3
  23. package/scripts/install/plugin.mjs +118 -14
  24. package/scripts/install/prompt-result.mjs +2 -0
  25. package/scripts/install/shared.mjs +188 -65
  26. package/scripts/install/state.mjs +63 -8
  27. package/scripts/install/venv.mjs +15 -6
  28. package/scripts/install/verify.mjs +128 -0
  29. package/scripts/install/vision-prompt.mjs +2 -0
  30. package/scripts/install-mcp.mjs +1 -1
  31. package/scripts/manifest-inventory.mjs +49 -3
  32. package/scripts/node-engine-policy.mjs +85 -0
  33. package/scripts/package-evidence.mjs +6 -0
  34. package/scripts/postinstall.mjs +26 -26
  35. package/scripts/reconcile.mjs +325 -0
  36. package/scripts/sync-tui.mjs +5 -1
  37. package/scripts/uninstall.mjs +258 -34
  38. package/scripts/update.mjs +164 -0
  39. package/scripts/validate-node-engine-policy.mjs +13 -0
  40. package/scripts/version-check.mjs +34 -1
  41. package/scripts/versioning.mjs +9 -1
  42. package/skills-lock.json +3 -3
  43. package/src/agents/mnemosyne.md +7 -2
  44. package/src/instructions/memory-protocol.instructions.md +2 -2
  45. package/src/instructions/zeus-memory-operations.instructions.md +4 -3
  46. package/src/pantheon/v2-unsupported.mjs +2 -3
  47. package/src/plugins/tui/dist/tui-v2.js +2 -2
  48. package/src/plugins/tui/dist/tui.js +1 -1
  49. package/src/plugins/tui/package-lock.json +3 -3
  50. package/src/plugins/tui/package.json +2 -2
  51. package/src/skills/artifact-management/SKILL.md +5 -1
  52. package/src/skills/memory-bank/SKILL.md +5 -1
  53. package/src/skills/worktrees/SKILL.md +30 -11
@@ -20,16 +20,24 @@ 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 { PROMPT_CANCELED } from '../scripts/install/prompt-result.mjs'
30
+ import {
31
+ isSupportedNodeVersion,
32
+ unsupportedNodeVersionMessage,
33
+ } from '../scripts/node-engine-policy.mjs'
34
+ import { runUpdate } from '../scripts/update.mjs'
25
35
 
26
36
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
27
37
  const ROOT = path.resolve(__dirname, '..')
28
38
 
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})`)
32
- console.error(' Install a newer Node via nvm: https://github.com/nvm-sh/nvm')
39
+ if (!isSupportedNodeVersion(process.versions.node)) {
40
+ console.error(unsupportedNodeVersionMessage(process.versions.node))
33
41
  process.exit(1)
34
42
  }
35
43
 
@@ -49,6 +57,9 @@ function printUsage() {
49
57
  console.log(' npx pantheon-opencode init # Install globally')
50
58
  console.log(' npx pantheon-opencode init --project # Install in project')
51
59
  console.log(' npx pantheon-opencode init --dry-run # Preview only')
60
+ console.log(' npx pantheon-opencode init --dry-run --save-plan <file> # Save reviewed plan')
61
+ console.log(' npx pantheon-opencode init --apply-plan <file> [--yes] # Apply reviewed plan')
62
+ console.log(' npx pantheon-opencode init --dry-run --json # Versioned JSON preview')
52
63
  console.log(' npx pantheon-opencode init --no-mcp # Skip MCP + venv')
53
64
  console.log(' npx pantheon-opencode init --force # Overwrite + recreate venv')
54
65
  console.log(' npx pantheon-opencode init --doctor # Run health check after install')
@@ -76,6 +87,7 @@ function printUsage() {
76
87
  )
77
88
  console.log(' npx pantheon-opencode uninstall --project # Remove project artifacts safely')
78
89
  console.log(' npx pantheon-opencode uninstall --global # Remove global artifacts safely')
90
+ console.log(' npx pantheon-opencode reconcile --project # Review stale owned installer files')
79
91
  console.log(' npx pantheon-opencode --version, -v # Print version and exit')
80
92
  console.log(' npx pantheon-opencode --help # Show this help')
81
93
  console.log('')
@@ -91,6 +103,16 @@ function printUsage() {
91
103
  console.log(' 130 canceled')
92
104
  }
93
105
 
106
+ async function authorizeReviewedPlan(plan) {
107
+ const operations = plan.operations.filter((operation) => operation.action !== 'unchanged')
108
+ console.log(`Reviewed plan: ${plan.target.path} (${operations.length} change(s))`)
109
+ for (const operation of operations) {
110
+ console.log(` ${operation.action.padEnd(9)} ${operation.path}`)
111
+ }
112
+ const { promptReview } = await import('../scripts/install/interactive.mjs')
113
+ return promptReview('Apply this reviewed plan?', { default: false })
114
+ }
115
+
94
116
  async function main() {
95
117
  const args = process.argv.slice(2)
96
118
  const command = args[0]
@@ -125,6 +147,16 @@ async function main() {
125
147
  process.exit(result.status ?? 1)
126
148
  }
127
149
 
150
+ if (command === 'reconcile') {
151
+ const { spawnSync } = await import('node:child_process')
152
+ const reconcileScript = path.join(ROOT, 'scripts', 'reconcile.mjs')
153
+ const result = spawnSync(process.execPath, [reconcileScript, ...args.slice(1)], {
154
+ stdio: 'inherit',
155
+ cwd: process.cwd(),
156
+ })
157
+ process.exit(result.status ?? 1)
158
+ }
159
+
128
160
  // restore delegates to scripts/restore.mjs (same spawnSync pattern as prune).
129
161
  if (command === 'restore') {
130
162
  const { spawnSync } = await import('node:child_process')
@@ -152,72 +184,12 @@ async function main() {
152
184
  // beta.5: one-command update — check npm dist-tag, install, re-init.
153
185
  // Default channel is `beta` (the prerelease line); `--stable` uses `latest`.
154
186
  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,
164
- })
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',
187
+ const status = runUpdate(args, {
188
+ root: ROOT,
189
+ currentVersion: readVersion(),
190
+ runningScript: process.argv[1],
202
191
  })
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}.`)
192
+ if (status !== 0) process.exit(status)
221
193
  return
222
194
  }
223
195
 
@@ -301,15 +273,89 @@ async function main() {
301
273
  }
302
274
 
303
275
  if (command === 'init' || !command) {
304
- const isProject = args.includes('--project')
305
- const isDryRun = args.includes('--dry-run')
276
+ const applyPlanIndex = args.indexOf('--apply-plan')
277
+ const inlineApplyPlan = args.find((arg) => arg.startsWith('--apply-plan='))
278
+ const applyPlanPath =
279
+ applyPlanIndex >= 0
280
+ ? (args[applyPlanIndex + 1] ?? null)
281
+ : (inlineApplyPlan?.split('=', 2)[1] ?? null)
282
+ if (applyPlanIndex >= 0 && !applyPlanPath) {
283
+ console.error('❌ --apply-plan requires a plan file path')
284
+ process.exit(1)
285
+ }
286
+ let reviewedPlan = null
287
+ if (applyPlanPath !== null) {
288
+ const allowed = new Set(['init', '--apply-plan', applyPlanPath, '--yes', '-y', '--headless'])
289
+ const normalized = args.map((value) =>
290
+ value.startsWith('--apply-plan=') ? '--apply-plan' : value,
291
+ )
292
+ if (normalized.some((value) => !allowed.has(value))) {
293
+ console.error('❌ --apply-plan accepts only the plan path and fresh authorization flags')
294
+ process.exit(1)
295
+ }
296
+ reviewedPlan = loadPlanArtifact(applyPlanPath)
297
+ if (
298
+ reviewedPlan.artifact_path &&
299
+ path.resolve(applyPlanPath) !== reviewedPlan.artifact_path
300
+ ) {
301
+ console.error('❌ The reviewed plan artifact was moved; use its original path')
302
+ process.exit(1)
303
+ }
304
+ const options = reviewedPlan.options
305
+ if (
306
+ !Array.isArray(options.components) ||
307
+ options.components.some(
308
+ (component) =>
309
+ !['agents', 'skills', 'instructions', 'commands', 'plugins', 'runtime'].includes(
310
+ component,
311
+ ),
312
+ ) ||
313
+ options.clean !== false ||
314
+ !['v1', 'v2', 'auto'].includes(options.version) ||
315
+ typeof options.isProject !== 'boolean'
316
+ ) {
317
+ console.error('❌ Install-plan options are invalid or unsupported')
318
+ process.exit(1)
319
+ }
320
+ }
321
+ const isProject = reviewedPlan ? reviewedPlan.options.isProject : args.includes('--project')
322
+ const isDryRun = reviewedPlan ? false : args.includes('--dry-run')
306
323
  const skipMCP = args.includes('--no-mcp')
307
324
  // --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')
325
+ const forceReinstall = reviewedPlan
326
+ ? reviewedPlan.options.clean
327
+ : args.includes('--force') || args.includes('--clean')
328
+ const runDoctor = !reviewedPlan && args.includes('--doctor')
329
+ const forceInteractive = !reviewedPlan && args.includes('--interactive')
311
330
  const forceHeadless = args.includes('--headless')
312
- const autoYes = args.includes('--yes') || args.includes('-y')
331
+ let autoYes = args.includes('--yes') || args.includes('-y')
332
+ const isJson = args.includes('--json')
333
+ const savePlanIndex = args.indexOf('--save-plan')
334
+ const inlineSavePlan = args.find((arg) => arg.startsWith('--save-plan='))
335
+ const savePlanPath =
336
+ savePlanIndex >= 0
337
+ ? (args[savePlanIndex + 1] ?? null)
338
+ : (inlineSavePlan?.split('=', 2)[1] ?? null)
339
+ if (savePlanIndex >= 0 && !savePlanPath) {
340
+ console.error('❌ --save-plan requires a plan file path')
341
+ process.exit(1)
342
+ }
343
+ if (savePlanPath !== null && (!isDryRun || reviewedPlan)) {
344
+ console.error('❌ --save-plan is available only with init --dry-run')
345
+ process.exit(1)
346
+ }
347
+ if (savePlanPath !== null && forceReinstall) {
348
+ console.error('❌ Saving a reviewed plan with --force/--clean is not supported safely')
349
+ process.exit(1)
350
+ }
351
+ if (isJson && (!isDryRun || reviewedPlan)) {
352
+ console.error('❌ --json is supported only with init --dry-run')
353
+ process.exit(1)
354
+ }
355
+ if (isJson && forceReinstall) {
356
+ console.error('❌ --force/--clean cleanup is not supported with --json')
357
+ process.exit(1)
358
+ }
313
359
 
314
360
  // beta.5: warn on unrecognized flags instead of silently ignoring them.
315
361
  const VALUE_FLAGS = new Set([
@@ -320,6 +366,8 @@ async function main() {
320
366
  '--opencode-version',
321
367
  '--components',
322
368
  '--merge-dev-config',
369
+ '--apply-plan',
370
+ '--save-plan',
323
371
  ])
324
372
  for (let i = 1; i < args.length; i++) {
325
373
  const arg = args[i]
@@ -342,6 +390,7 @@ async function main() {
342
390
  '--headless',
343
391
  '--yes',
344
392
  '-y',
393
+ '--json',
345
394
  ].includes(bare)
346
395
  ) {
347
396
  console.error(`⚠️ Unknown option ignored: ${arg}`)
@@ -349,20 +398,33 @@ async function main() {
349
398
  }
350
399
 
351
400
  const presetIndex = args.indexOf('--preset')
352
- const presetOpt = presetIndex >= 0 ? (args[presetIndex + 1] ?? null) : null
401
+ const presetOpt = reviewedPlan
402
+ ? (reviewedPlan.options.preset ?? null)
403
+ : presetIndex >= 0
404
+ ? (args[presetIndex + 1] ?? null)
405
+ : null
353
406
  // Explicit top-level model overrides for the generated config.
354
407
  const modelIndex = args.indexOf('--model')
355
- const modelOpt = modelIndex >= 0 ? (args[modelIndex + 1] ?? null) : null
408
+ const modelOpt = reviewedPlan
409
+ ? (reviewedPlan.options.model ?? null)
410
+ : modelIndex >= 0
411
+ ? (args[modelIndex + 1] ?? null)
412
+ : null
356
413
  const smallModelIndex = args.indexOf('--small-model')
357
- const smallModelOpt = smallModelIndex >= 0 ? (args[smallModelIndex + 1] ?? null) : null
414
+ const smallModelOpt = reviewedPlan
415
+ ? (reviewedPlan.options.smallModel ?? null)
416
+ : smallModelIndex >= 0
417
+ ? (args[smallModelIndex + 1] ?? null)
418
+ : null
358
419
  // --version v1|v2|auto labels the install target. `auto` resolves the
359
420
  // generation from the opencode binary at install time (see
360
421
  // scripts/install/opencode-version.mjs). Invalid values fail fast.
361
422
  const versionIndex = args.indexOf('--version')
362
423
  const legacyVersionIndex = args.indexOf('--opencode-version')
363
424
  const inlineVersion = args.find((arg) => arg.startsWith('--opencode-version='))
364
- const versionOpt =
365
- versionIndex >= 0
425
+ const versionOpt = reviewedPlan
426
+ ? reviewedPlan.options.version
427
+ : versionIndex >= 0
366
428
  ? (args[versionIndex + 1] ?? null)
367
429
  : legacyVersionIndex >= 0
368
430
  ? (args[legacyVersionIndex + 1] ?? null)
@@ -386,8 +448,8 @@ async function main() {
386
448
  componentsIndex >= 0
387
449
  ? (args[componentsIndex + 1] ?? null)
388
450
  : (inlineComponents?.split('=', 2)[1] ?? null)
389
- let requestedComponents = null
390
- if (componentsOpt !== null) {
451
+ let requestedComponents = reviewedPlan ? [...reviewedPlan.options.components] : null
452
+ if (!reviewedPlan && componentsOpt !== null) {
391
453
  requestedComponents = componentsOpt
392
454
  .split(',')
393
455
  .map((c) => c.trim())
@@ -408,8 +470,9 @@ async function main() {
408
470
  // from the installer code, never from a dev checkout).
409
471
  const mergeDevConfigIndex = args.indexOf('--merge-dev-config')
410
472
  const inlineMergeDevConfig = args.find((arg) => arg.startsWith('--merge-dev-config='))
411
- const mergeDevConfigOpt =
412
- mergeDevConfigIndex >= 0
473
+ const mergeDevConfigOpt = reviewedPlan
474
+ ? (reviewedPlan.options.mergeDevConfig ?? null)
475
+ : mergeDevConfigIndex >= 0
413
476
  ? (args[mergeDevConfigIndex + 1] ?? null)
414
477
  : (inlineMergeDevConfig?.split('=', 2)[1] ?? null)
415
478
  if (mergeDevConfigIndex >= 0 && mergeDevConfigOpt === null) {
@@ -417,15 +480,30 @@ async function main() {
417
480
  process.exit(1)
418
481
  }
419
482
 
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) {
483
+ if (reviewedPlan && !autoYes) {
484
+ if (!forceHeadless && process.stdin.isTTY && process.stdout.isTTY) {
485
+ const authorization = await authorizeReviewedPlan(reviewedPlan)
486
+ if (authorization === PROMPT_CANCELED) {
487
+ console.log('❌ Install canceled; no files were written.')
488
+ process.exitCode = 130
489
+ return
490
+ }
491
+ if (!authorization) {
492
+ console.log('❌ Install declined; no files were written.')
493
+ return
494
+ }
495
+ } else {
496
+ console.error('❌ Applying a reviewed plan requires --yes or an interactive TTY prompt.')
497
+ process.exit(2)
498
+ }
499
+ autoYes = true
500
+ }
501
+
502
+ const components = reviewedPlan
503
+ ? requestedComponents
504
+ : (requestedComponents ?? ['agents', 'skills', 'instructions', 'commands', 'plugins'])
505
+ if (!reviewedPlan && !skipMCP && !components.includes('runtime')) components.push('runtime')
506
+ if (!reviewedPlan && skipMCP) {
429
507
  const runtimeIndex = components.indexOf('runtime')
430
508
  if (runtimeIndex >= 0) components.splice(runtimeIndex, 1)
431
509
  }
@@ -435,33 +513,131 @@ async function main() {
435
513
  const { strings } = await import('../scripts/install/strings.mjs')
436
514
  const S = strings()
437
515
 
438
- console.log('')
439
- if (!forceInteractive || isDryRun) {
440
- console.log(`Pantheon OpenCode v${version} — ${isDryRun ? S.dryRun : S.installing}`)
516
+ const planTimestamp = reviewedPlan?.planned_at ?? new Date().toISOString()
517
+ const stdoutWrite = process.stdout.write
518
+ if (isJson) process.stdout.write = () => true
519
+ if (!isJson) {
441
520
  console.log('')
521
+ if (!forceInteractive || isDryRun) {
522
+ console.log(`Pantheon OpenCode v${version} — ${isDryRun ? S.dryRun : S.installing}`)
523
+ console.log('')
524
+ }
442
525
  }
443
526
 
444
527
  let result
528
+ let installError = null
445
529
  try {
446
530
  const { installOpenCode } = await import('../scripts/install/opencode.mjs')
447
- const target = isProject ? process.cwd() : undefined
531
+ const target = reviewedPlan ? reviewedPlan.target.path : isProject ? process.cwd() : undefined
448
532
  result = await installOpenCode(target, isDryRun, forceReinstall, components, {
449
533
  interactive: forceInteractive,
450
- headless: forceHeadless,
534
+ headless: reviewedPlan ? true : forceHeadless,
451
535
  yes: autoYes,
452
536
  model: modelOpt,
453
537
  smallModel: smallModelOpt,
454
538
  preset: presetOpt,
455
539
  version: versionOpt ?? 'auto',
456
540
  mergeDevConfig: mergeDevConfigOpt,
541
+ expectedPlan: reviewedPlan,
542
+ planTimestamp,
543
+ quiet: isJson,
457
544
  })
458
545
  } catch (err) {
459
- if (err?.message === 'Canceled') {
546
+ installError = err
547
+ } finally {
548
+ if (isJson) process.stdout.write = stdoutWrite
549
+ }
550
+ if (installError) {
551
+ if (isJson) {
552
+ process.stdout.write(
553
+ `${JSON.stringify({
554
+ schema_version: 1,
555
+ command: 'init',
556
+ mode: 'dry-run',
557
+ status: 'error',
558
+ error: installError.message,
559
+ })}\n`,
560
+ )
561
+ console.error(`❌ Init dry-run failed: ${installError.message}`)
562
+ } else if (installError?.message === 'Canceled') {
460
563
  console.error(S.canceled)
461
564
  process.exit(130)
565
+ } else {
566
+ console.error(
567
+ `${S.installFailed(installError.message)}\n${S.failHintNoMcp}\n${S.failHintForce}`,
568
+ )
462
569
  }
463
- console.error(`${S.installFailed(err.message)}\n${S.failHintNoMcp}\n${S.failHintForce}`)
464
- process.exit(1)
570
+ process.exitCode = 1
571
+ return
572
+ }
573
+
574
+ if (savePlanPath !== null && result?.reason === 'plan') {
575
+ try {
576
+ const artifact = createPlanArtifact({
577
+ target: result.target,
578
+ packageRoot: ROOT,
579
+ packageVersion: version,
580
+ plannedAt: planTimestamp,
581
+ artifactPath: savePlanPath,
582
+ options: {
583
+ clean: forceReinstall,
584
+ components,
585
+ version: result.openCodeVersion,
586
+ isProject,
587
+ model: modelOpt,
588
+ smallModel: smallModelOpt,
589
+ preset: presetOpt,
590
+ mergeDevConfig: mergeDevConfigOpt,
591
+ },
592
+ files: result.files,
593
+ snapshots: result.snapshots,
594
+ })
595
+ savePlanArtifact(savePlanPath, artifact)
596
+ } catch (error) {
597
+ if (!isJson) throw error
598
+ process.stdout.write(
599
+ `${JSON.stringify({
600
+ schema_version: 1,
601
+ command: 'init',
602
+ mode: 'dry-run',
603
+ status: 'error',
604
+ error: error.message,
605
+ })}\n`,
606
+ )
607
+ console.error(`❌ Could not save reviewed install plan: ${error.message}`)
608
+ process.exitCode = 1
609
+ return
610
+ }
611
+ if (!isJson) console.log(`Reviewed plan saved to ${path.resolve(savePlanPath)} (mode 0600).`)
612
+ }
613
+
614
+ if (isJson) {
615
+ const files = result.files ?? []
616
+ const totals = { create: 0, update: 0, remove: 0, unchanged: 0, conflict: 0 }
617
+ for (const file of files) {
618
+ if (Object.hasOwn(totals, file.action)) totals[file.action] += 1
619
+ }
620
+ process.stdout.write(
621
+ `${JSON.stringify({
622
+ schema_version: 1,
623
+ command: 'init',
624
+ mode: 'dry-run',
625
+ status: 'planned',
626
+ target: result.target,
627
+ open_code_version: result.openCodeVersion,
628
+ advisories: result.versionAssumed
629
+ ? ['OpenCode host probe skipped to keep dry-run write-free; V1 was assumed.']
630
+ : [],
631
+ totals,
632
+ files: files.map(({ path: filePath, action, bytes }) => ({
633
+ path: path.relative(result.target, filePath) || '.',
634
+ action,
635
+ bytes,
636
+ })),
637
+ saved_plan: savePlanPath ? path.resolve(savePlanPath) : null,
638
+ })}\n`,
639
+ )
640
+ return
465
641
  }
466
642
 
467
643
  // 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.13",
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",
@@ -98,6 +99,7 @@
98
99
  "!**/*.pyc"
99
100
  ],
100
101
  "dependencies": {
102
+ "@clack/prompts": "1.8.1",
101
103
  "@opencode-ai/plugin": "1.18.33",
102
104
  "js-yaml": "^5.4.2",
103
105
  "zod": "4.1.8",
@@ -138,7 +140,7 @@
138
140
  "author": "ils15",
139
141
  "license": "MIT",
140
142
  "engines": {
141
- "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
143
+ "node": ">=26.4.0"
142
144
  },
143
145
  "preferGlobal": true
144
146
  }
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.13",
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.13"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []