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.
Files changed (71) hide show
  1. package/AGENTS.global.md +42 -18
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +122 -38
  5. package/README.pt-BR.md +5 -4
  6. package/bin/pantheon-init.mjs +322 -101
  7. package/package.json +7 -6
  8. package/plugin.json +1 -1
  9. package/pyproject.toml +1 -1
  10. package/scripts/build-agents-md.mjs +128 -10
  11. package/scripts/check-plugin-v2-coverage.mjs +7 -7
  12. package/scripts/doctor.mjs +242 -19
  13. package/scripts/install/backup.mjs +648 -0
  14. package/scripts/install/cli-ui.mjs +79 -1
  15. package/scripts/install/exit-codes.mjs +26 -0
  16. package/scripts/install/file-inventory.mjs +555 -0
  17. package/scripts/install/interactive.mjs +15 -1
  18. package/scripts/install/migrate.mjs +32 -6
  19. package/scripts/install/model-picker.mjs +63 -22
  20. package/scripts/install/opencode-version.mjs +9 -1
  21. package/scripts/install/opencode.mjs +761 -63
  22. package/scripts/install/plan-artifact.mjs +517 -0
  23. package/scripts/install/plan.mjs +449 -0
  24. package/scripts/install/plugin.mjs +136 -18
  25. package/scripts/install/shared.mjs +249 -94
  26. package/scripts/install/state.mjs +63 -8
  27. package/scripts/install/strings.mjs +20 -0
  28. package/scripts/install/venv.mjs +25 -6
  29. package/scripts/install/verify.mjs +128 -0
  30. package/scripts/install/vision-prompt.mjs +13 -1
  31. package/scripts/install-mcp.mjs +1 -1
  32. package/scripts/manifest-inventory.mjs +49 -3
  33. package/scripts/mcp_persistence.py +50 -15
  34. package/scripts/node-engine-policy.mjs +74 -0
  35. package/scripts/package-evidence.mjs +6 -0
  36. package/scripts/postinstall.mjs +3 -4
  37. package/scripts/prune.mjs +9 -3
  38. package/scripts/reconcile.mjs +325 -0
  39. package/scripts/restore.mjs +222 -0
  40. package/scripts/uninstall.mjs +258 -34
  41. package/scripts/update.mjs +164 -0
  42. package/scripts/validate-node-engine-policy.mjs +13 -0
  43. package/scripts/version-check.mjs +34 -1
  44. package/scripts/versioning.mjs +9 -1
  45. package/skills-lock.json +4 -4
  46. package/src/agents/iris.md +6 -0
  47. package/src/agents/mnemosyne.md +7 -2
  48. package/src/agents/zeus.md +19 -62
  49. package/src/instructions/agent-return-format.instructions.md +17 -15
  50. package/src/instructions/memory-protocol.instructions.md +3 -2
  51. package/src/instructions/test-economy.instructions.md +21 -0
  52. package/src/instructions/zeus-anti-stall.instructions.md +6 -6
  53. package/src/instructions/zeus-council-synthesis.instructions.md +10 -11
  54. package/src/instructions/zeus-memory-operations.instructions.md +11 -6
  55. package/src/instructions/zeus-timeout-retry.instructions.md +28 -9
  56. package/src/mcp/mcp_persistence.py +50 -15
  57. package/src/pantheon/cost-command.ts +5 -2
  58. package/src/pantheon/model-agents.ts +19 -0
  59. package/src/pantheon/model-command.ts +218 -56
  60. package/src/pantheon/v2-model-overrides.ts +275 -0
  61. package/src/pantheon/v2-tools.ts +18 -29
  62. package/src/pantheon/v2-unsupported.mjs +2 -3
  63. package/src/plugin-v2.ts +13 -0
  64. package/src/plugins/tui/dist/tui-v2.js +2 -2
  65. package/src/plugins/tui/dist/tui.js +1 -1
  66. package/src/plugins/tui/package-lock.json +3 -3
  67. package/src/plugins/tui/package.json +2 -2
  68. package/src/skills/artifact-management/SKILL.md +5 -1
  69. package/src/skills/auto-continue/SKILL.md +14 -6
  70. package/src/skills/memory-bank/SKILL.md +5 -1
  71. package/src/skills/worktrees/SKILL.md +30 -11
@@ -20,13 +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
+ 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'
23
34
 
24
35
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
25
36
  const ROOT = path.resolve(__dirname, '..')
26
37
 
27
- const REQUIRED_NODE_MAJOR = 22
28
- if (parseInt(process.versions.node.split('.')[0], 10) < REQUIRED_NODE_MAJOR) {
29
- 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
+ )
30
42
  console.error(' Install a newer Node via nvm: https://github.com/nvm-sh/nvm')
31
43
  process.exit(1)
32
44
  }
@@ -47,6 +59,9 @@ function printUsage() {
47
59
  console.log(' npx pantheon-opencode init # Install globally')
48
60
  console.log(' npx pantheon-opencode init --project # Install in project')
49
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')
50
65
  console.log(' npx pantheon-opencode init --no-mcp # Skip MCP + venv')
51
66
  console.log(' npx pantheon-opencode init --force # Overwrite + recreate venv')
52
67
  console.log(' npx pantheon-opencode init --doctor # Run health check after install')
@@ -69,10 +84,41 @@ function printUsage() {
69
84
  console.log(' npx pantheon-opencode prune # List legacy artifacts (dry-run)')
70
85
  console.log(' npx pantheon-opencode prune --apply # Remove stale backups')
71
86
  console.log(' npx pantheon-opencode doctor [--profile] # Run health check')
87
+ console.log(
88
+ ' npx pantheon-opencode restore [--project] [--yes] # Restore an exact installer backup',
89
+ )
72
90
  console.log(' npx pantheon-opencode uninstall --project # Remove project artifacts safely')
73
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')
74
93
  console.log(' npx pantheon-opencode --version, -v # Print version and exit')
75
94
  console.log(' npx pantheon-opencode --help # Show this help')
95
+ console.log('')
96
+ console.log('Apply authorization:')
97
+ console.log(' A non-interactive run without --yes prints the plan and applies NOTHING.')
98
+ console.log(' Re-run with --yes to apply; an interactive TTY run prompts (default: no).')
99
+ console.log('')
100
+ console.log('Exit codes:')
101
+ console.log(' 0 success, or plan/no-apply (declined)')
102
+ console.log(' 1 failure')
103
+ console.log(' 2 apply was requested without authorization (non-interactive, no --yes)')
104
+ console.log(' 3 drift detected (a planned file changed after the plan)')
105
+ console.log(' 130 canceled')
106
+ }
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
+ }
76
122
  }
77
123
 
78
124
  async function main() {
@@ -109,6 +155,27 @@ async function main() {
109
155
  process.exit(result.status ?? 1)
110
156
  }
111
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
+
168
+ // restore delegates to scripts/restore.mjs (same spawnSync pattern as prune).
169
+ if (command === 'restore') {
170
+ const { spawnSync } = await import('node:child_process')
171
+ const restoreScript = path.join(ROOT, 'scripts', 'restore.mjs')
172
+ const result = spawnSync(process.execPath, [restoreScript, ...args.slice(1)], {
173
+ stdio: 'inherit',
174
+ cwd: process.cwd(),
175
+ })
176
+ process.exit(result.status ?? 1)
177
+ }
178
+
112
179
  // doctor delegates to scripts/doctor.mjs (same spawnSync pattern as prune).
113
180
  // Forwarding the FULL args (profile, target, --fix, ...) keeps the packaged
114
181
  // CLI a thin wrapper; the doctor banner reports the resolved profile.
@@ -125,72 +192,12 @@ async function main() {
125
192
  // beta.5: one-command update — check npm dist-tag, install, re-init.
126
193
  // Default channel is `beta` (the prerelease line); `--stable` uses `latest`.
127
194
  if (command === 'update') {
128
- const channel = args.includes('--stable') ? 'latest' : 'beta'
129
- const current = readVersion()
130
- const { spawnSync } = await import('node:child_process')
131
- const _S = (await import('../scripts/install/strings.mjs')).strings()
132
-
133
- console.log(`Pantheon OpenCode v${current} — checking the "${channel}" channel...`)
134
- const view = spawnSync('npm', ['view', `pantheon-opencode@${channel}`, 'version'], {
135
- encoding: 'utf8',
136
- timeout: 30_000,
195
+ const status = runUpdate(args, {
196
+ root: ROOT,
197
+ currentVersion: readVersion(),
198
+ runningScript: process.argv[1],
137
199
  })
138
- const latest = (view.stdout || '').trim().split('\n').filter(Boolean).pop()
139
- if (view.status !== 0 || !latest) {
140
- console.error(
141
- `❌ Could not reach the npm registry: ${(view.stderr || view.stdout || '').trim() || 'no version returned'}`,
142
- )
143
- process.exit(1)
144
- }
145
-
146
- // Prerelease-aware compare: core (major.minor.patch) first, then the
147
- // beta counter; a stable release outranks any beta of the same core.
148
- const parsePantheonVersion = (v) => {
149
- const m = String(v)
150
- .trim()
151
- .match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/)
152
- if (!m) return null
153
- return {
154
- core: [+m[1], +m[2], +m[3]],
155
- beta: m[4] === undefined ? Number.POSITIVE_INFINITY : +m[4],
156
- }
157
- }
158
- const cmp = (a, b) => {
159
- const pa = parsePantheonVersion(a)
160
- const pb = parsePantheonVersion(b)
161
- if (!pa || !pb) return 0
162
- for (let i = 0; i < 3; i++) {
163
- if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
164
- }
165
- return pa.beta - pb.beta
166
- }
167
- if (cmp(latest, current) <= 0) {
168
- console.log(`✅ Already up to date (installed v${current}; ${channel} channel: v${latest}).`)
169
- return
170
- }
171
-
172
- console.log(`Updating v${current} → v${latest} (channel: ${channel})...`)
173
- const install = spawnSync('npm', ['install', '-g', `pantheon-opencode@${channel}`], {
174
- stdio: 'inherit',
175
- })
176
- if (install.status !== 0) {
177
- console.error('❌ npm install failed — the previous installation is untouched.')
178
- process.exit(install.status ?? 1)
179
- }
180
-
181
- // Refresh config/venv/MCP entries with the NEW package (resolved via
182
- // PATH — the freshly installed global bin).
183
- const init = spawnSync('pantheon-opencode', ['init', '--yes', '--headless'], {
184
- stdio: 'inherit',
185
- shell: process.platform === 'win32',
186
- })
187
- if (init.status !== 0 || init.error) {
188
- console.error(
189
- '⚠️ Updated, but the config refresh did not complete — run `npx pantheon-opencode init` manually.',
190
- )
191
- process.exit(init.status ?? 1)
192
- }
193
- console.log(`✅ Updated to v${latest}.`)
200
+ if (status !== 0) process.exit(status)
194
201
  return
195
202
  }
196
203
 
@@ -274,15 +281,89 @@ async function main() {
274
281
  }
275
282
 
276
283
  if (command === 'init' || !command) {
277
- const isProject = args.includes('--project')
278
- 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')
279
331
  const skipMCP = args.includes('--no-mcp')
280
332
  // --clean is an alias of --force (both wipe component dirs + recreate venv).
281
- const forceReinstall = args.includes('--force') || args.includes('--clean')
282
- const runDoctor = args.includes('--doctor')
283
- 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')
284
338
  const forceHeadless = args.includes('--headless')
285
- 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
+ }
286
367
 
287
368
  // beta.5: warn on unrecognized flags instead of silently ignoring them.
288
369
  const VALUE_FLAGS = new Set([
@@ -293,6 +374,8 @@ async function main() {
293
374
  '--opencode-version',
294
375
  '--components',
295
376
  '--merge-dev-config',
377
+ '--apply-plan',
378
+ '--save-plan',
296
379
  ])
297
380
  for (let i = 1; i < args.length; i++) {
298
381
  const arg = args[i]
@@ -315,6 +398,7 @@ async function main() {
315
398
  '--headless',
316
399
  '--yes',
317
400
  '-y',
401
+ '--json',
318
402
  ].includes(bare)
319
403
  ) {
320
404
  console.error(`⚠️ Unknown option ignored: ${arg}`)
@@ -322,20 +406,33 @@ async function main() {
322
406
  }
323
407
 
324
408
  const presetIndex = args.indexOf('--preset')
325
- 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
326
414
  // Explicit top-level model overrides for the generated config.
327
415
  const modelIndex = args.indexOf('--model')
328
- 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
329
421
  const smallModelIndex = args.indexOf('--small-model')
330
- 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
331
427
  // --version v1|v2|auto labels the install target. `auto` resolves the
332
428
  // generation from the opencode binary at install time (see
333
429
  // scripts/install/opencode-version.mjs). Invalid values fail fast.
334
430
  const versionIndex = args.indexOf('--version')
335
431
  const legacyVersionIndex = args.indexOf('--opencode-version')
336
432
  const inlineVersion = args.find((arg) => arg.startsWith('--opencode-version='))
337
- const versionOpt =
338
- versionIndex >= 0
433
+ const versionOpt = reviewedPlan
434
+ ? reviewedPlan.options.version
435
+ : versionIndex >= 0
339
436
  ? (args[versionIndex + 1] ?? null)
340
437
  : legacyVersionIndex >= 0
341
438
  ? (args[legacyVersionIndex + 1] ?? null)
@@ -359,8 +456,8 @@ async function main() {
359
456
  componentsIndex >= 0
360
457
  ? (args[componentsIndex + 1] ?? null)
361
458
  : (inlineComponents?.split('=', 2)[1] ?? null)
362
- let requestedComponents = null
363
- if (componentsOpt !== null) {
459
+ let requestedComponents = reviewedPlan ? [...reviewedPlan.options.components] : null
460
+ if (!reviewedPlan && componentsOpt !== null) {
364
461
  requestedComponents = componentsOpt
365
462
  .split(',')
366
463
  .map((c) => c.trim())
@@ -381,8 +478,9 @@ async function main() {
381
478
  // from the installer code, never from a dev checkout).
382
479
  const mergeDevConfigIndex = args.indexOf('--merge-dev-config')
383
480
  const inlineMergeDevConfig = args.find((arg) => arg.startsWith('--merge-dev-config='))
384
- const mergeDevConfigOpt =
385
- mergeDevConfigIndex >= 0
481
+ const mergeDevConfigOpt = reviewedPlan
482
+ ? (reviewedPlan.options.mergeDevConfig ?? null)
483
+ : mergeDevConfigIndex >= 0
386
484
  ? (args[mergeDevConfigIndex + 1] ?? null)
387
485
  : (inlineMergeDevConfig?.split('=', 2)[1] ?? null)
388
486
  if (mergeDevConfigIndex >= 0 && mergeDevConfigOpt === null) {
@@ -390,15 +488,24 @@ async function main() {
390
488
  process.exit(1)
391
489
  }
392
490
 
393
- const components = requestedComponents ?? [
394
- 'agents',
395
- 'skills',
396
- 'instructions',
397
- 'commands',
398
- 'plugins',
399
- ]
400
- if (!skipMCP && !components.includes('runtime')) components.push('runtime')
401
- 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) {
402
509
  const runtimeIndex = components.indexOf('runtime')
403
510
  if (runtimeIndex >= 0) components.splice(runtimeIndex, 1)
404
511
  }
@@ -408,32 +515,146 @@ async function main() {
408
515
  const { strings } = await import('../scripts/install/strings.mjs')
409
516
  const S = strings()
410
517
 
411
- console.log('')
412
- if (!forceInteractive || isDryRun) {
413
- 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) {
414
522
  console.log('')
523
+ if (!forceInteractive || isDryRun) {
524
+ console.log(`Pantheon OpenCode v${version} — ${isDryRun ? S.dryRun : S.installing}`)
525
+ console.log('')
526
+ }
415
527
  }
416
528
 
529
+ let result
530
+ let installError = null
417
531
  try {
418
532
  const { installOpenCode } = await import('../scripts/install/opencode.mjs')
419
- const target = isProject ? process.cwd() : undefined
420
- await installOpenCode(target, isDryRun, forceReinstall, components, {
533
+ const target = reviewedPlan ? reviewedPlan.target.path : isProject ? process.cwd() : undefined
534
+ result = await installOpenCode(target, isDryRun, forceReinstall, components, {
421
535
  interactive: forceInteractive,
422
- headless: forceHeadless,
536
+ headless: reviewedPlan ? true : forceHeadless,
423
537
  yes: autoYes,
424
538
  model: modelOpt,
425
539
  smallModel: smallModelOpt,
426
540
  preset: presetOpt,
427
541
  version: versionOpt ?? 'auto',
428
542
  mergeDevConfig: mergeDevConfigOpt,
543
+ expectedPlan: reviewedPlan,
544
+ planTimestamp,
545
+ quiet: isJson,
429
546
  })
430
547
  } catch (err) {
431
- 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') {
432
565
  console.error(S.canceled)
433
566
  process.exit(130)
567
+ } else {
568
+ console.error(
569
+ `${S.installFailed(installError.message)}\n${S.failHintNoMcp}\n${S.failHintForce}`,
570
+ )
434
571
  }
435
- console.error(`${S.installFailed(err.message)}\n${S.failHintNoMcp}\n${S.failHintForce}`)
436
- 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
643
+ }
644
+
645
+ // Beta 11 Setup phases 3-4: a run that did NOT apply (declined /
646
+ // unauthorized / drift) must never print a success banner. Exit codes:
647
+ // 0 ok / no-apply · 2 apply-requested-without-authorization · 3 drift
648
+ // 130 canceled · 1 failure (thrown above).
649
+ if (result?.applied === false) {
650
+ if (!isDryRun) {
651
+ if (result.reason === 'canceled') console.error(S.canceled)
652
+ process.exit(resolveInstallExitCode(result, isDryRun))
653
+ }
654
+ // Dry-run preview: report honestly instead of the "installed!" banner.
655
+ console.log('')
656
+ console.log(` ${S.dryRunNothingWritten}`)
657
+ return
437
658
  }
438
659
 
439
660
  if (runDoctor && !isDryRun) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.6.0-beta.10",
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": [
@@ -18,12 +18,12 @@
18
18
  "scripts": {
19
19
  "postinstall": "node scripts/postinstall.mjs && node scripts/sync-tui.mjs",
20
20
  "prepare": "husky",
21
- "setup": "node bin/pantheon-init.mjs init",
21
+ "setup": "node bin/pantheon-init.mjs init --yes",
22
22
  "build:agents-md": "node scripts/build-agents-md.mjs",
23
- "setup:lite": "node bin/pantheon-init.mjs init --clean --components agents,skills,instructions",
23
+ "setup:lite": "node bin/pantheon-init.mjs init --yes --clean --components agents,skills,instructions",
24
24
  "doctor": "node scripts/doctor.mjs",
25
25
  "prune": "node scripts/prune.mjs",
26
- "init": "node bin/pantheon-init.mjs init",
26
+ "init": "node bin/pantheon-init.mjs init --yes",
27
27
  "opencode": "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode",
28
28
  "start": "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode",
29
29
  "prepublishOnly": "node scripts/validate-routing.mjs",
@@ -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.10",
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.10"
7
+ version = "1.6.0-beta.12"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []