pantheon-opencode 1.3.3 → 1.3.5-beta.52.c75b04d

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 (52) hide show
  1. package/AGENTS.md +919 -0
  2. package/README.md +157 -15
  3. package/bin/pantheon-init.mjs +177 -120
  4. package/opencode.json +3 -6
  5. package/package.json +8 -4
  6. package/scripts/build-agents-md.mjs +195 -0
  7. package/scripts/cost.mjs +17 -0
  8. package/scripts/doctor.mjs +56 -22
  9. package/scripts/hooks/on-subagent-delegation-stop.sh +22 -5
  10. package/scripts/install/opencode.mjs +189 -38
  11. package/scripts/install/plugin.mjs +112 -28
  12. package/scripts/install-mcp.mjs +1 -1
  13. package/scripts/redaction-gate.mjs +69 -0
  14. package/scripts/release-beta-version.mjs +80 -0
  15. package/scripts/release-notes.mjs +317 -0
  16. package/scripts/test-context-compression.sh +5 -2
  17. package/scripts/validate-package.mjs +56 -0
  18. package/scripts/versioning.mjs +117 -18
  19. package/src/agents/zeus.md +6 -2
  20. package/src/pantheon/background-job-board.ts +24 -38
  21. package/src/pantheon/chat-reminders.ts +79 -0
  22. package/src/pantheon/compaction-assert.ts +160 -0
  23. package/src/pantheon/cost-command.ts +57 -18
  24. package/src/pantheon/delegation-compaction.ts +143 -16
  25. package/src/pantheon/delegation-finalize.ts +8 -1
  26. package/src/pantheon/delegation-notify.ts +19 -157
  27. package/src/pantheon/delegation.ts +330 -28
  28. package/src/pantheon/dispatch-guard.ts +9 -4
  29. package/src/pantheon/goal-loop.ts +7 -4
  30. package/src/pantheon/hashline/tool.ts +2 -1
  31. package/src/pantheon/logger.ts +117 -0
  32. package/src/pantheon/plugin-once.ts +76 -0
  33. package/src/pantheon/preemptive-compact.ts +76 -0
  34. package/src/pantheon/presets.d.mts +31 -0
  35. package/src/pantheon/presets.mjs +105 -3
  36. package/src/pantheon/todo-enforcer.ts +79 -27
  37. package/src/pantheon/todo-preserve.ts +295 -0
  38. package/src/plugin.ts +177 -50
  39. package/src/plugins/pantheon-hooks.ts +35 -22
  40. package/src/plugins/tui/dist/server.d.ts +17 -0
  41. package/src/plugins/tui/dist/server.d.ts.map +1 -0
  42. package/src/plugins/tui/dist/server.js +20 -0
  43. package/src/plugins/tui/dist/server.js.map +1 -0
  44. package/src/plugins/tui/dist/tui.d.ts +303 -1
  45. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  46. package/src/plugins/tui/dist/tui.js +977 -350
  47. package/src/plugins/tui/dist/tui.tsx +1427 -150
  48. package/src/plugins/tui/package.json +4 -3
  49. package/src/plugins/tui/src/index.tsx +1427 -150
  50. package/src/plugins/tui/src/server.ts +15 -0
  51. package/src/plugins/tui/tsdown.config.ts +1 -1
  52. package/src/routing.yml +3 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <h1 align="center">Pantheon</h1>
3
3
 
4
4
  <p align="center">
5
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-v1.0.0-blue" alt="Version"></a>
5
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-v1.3.4-blue" alt="Version"></a>
6
6
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
7
7
  <a href="src/agents/README.md"><img src="https://img.shields.io/badge/agents-14-purple" alt="Agents"></a>
8
8
  <a href="src/skills/README.md"><img src="https://img.shields.io/badge/skills-21-orange" alt="Skills"></a>
@@ -183,6 +183,39 @@ export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
183
183
  opencode
184
184
  ```
185
185
 
186
+ ### OpenCode V1/V2 — Dual Version
187
+
188
+ Pantheon's shared config is **V1-shaped and valid under both OpenCode versions**:
189
+
190
+ | | V1 | V2 (beta) |
191
+ |---|---|---|
192
+ | Binary | `opencode` (1.18.18) | `opencode2` (v0.0.0-next-17444) |
193
+ | Config locations | `~/.config/opencode/`, `<project>/opencode.json`, `.opencode/` | Same — V2 reads the same locations |
194
+ | Config handling | Loads V1 shape directly | Normalizes V1 fields **in memory** — no rewrite, no native V2 conversion |
195
+ | State DB | `~/.local/share/opencode/opencode.db` | `~/.local/share/opencode/opencode-v2.db` (isolated via `OPENCODE_DB`) |
196
+ | Service port | 49374 | 49375 (must differ from V1) |
197
+
198
+ The `pantheon_cost` report preserves the V1 database default. Set
199
+ `PANTHEON_OPENCODE_VERSION=v1` or `v2` to select the corresponding state DB
200
+ explicitly (`opencode.db` or `opencode-v2.db`). For custom installations,
201
+ `PANTHEON_COST_DB=/absolute/path/to/opencode.db` takes precedence over the
202
+ version selector; an explicit `dbPath` supplied by the tool caller takes
203
+ precedence over both. The resolver never probes the other version's database,
204
+ and reports an actionable error when the selected DB is missing or has an
205
+ incompatible schema.
206
+
207
+ **What changed for V2 compatibility (Phase 3):**
208
+
209
+ - `subagent_depth` moved from top-level to `experimental.subagent_depth` — V2 silently ignores the top-level key. The installer migrates existing configs automatically.
210
+ - Instruction content consolidated into `AGENTS.md` (generated by `scripts/build-agents-md.mjs` from `src/instructions/*.instructions.md`) — V2 accepts but does **not** load the `instructions` config key; both versions auto-load `AGENTS.md`.
211
+ - `npx pantheon-opencode init --version v2` labels an install for V2 (informational — the config is shared).
212
+
213
+ **Known V2 beta gaps:**
214
+
215
+ - `instructions` config key accepted but not loaded → content lives in `AGENTS.md`.
216
+ - `formatter` / `lsp` config not executed.
217
+ - `--subtask` delegation flag is a no-op.
218
+
186
219
  ### Background Delegation
187
220
 
188
221
  Zeus (and other root sessions) can dispatch agents as **background child sessions**
@@ -196,15 +229,27 @@ via three delegation tools, tracked on a persistent job board
196
229
  | `pantheon_delegation_read` | `{id}` | Blocks (`waitForTerminal`, up to 15 min) until the delegation reaches a terminal state, returns the report markdown, and marks the job reconciled. Resolves by alias or task ID. |
197
230
  | `pantheon_delegation_list` | `{}` | Lists the current session's delegations with `[unread]` on finished, unreconciled jobs. |
198
231
 
199
- **Notification model — no polling, no client push.** When a job reaches a
200
- terminal state (completion observed via `session.idle`/`session.error` on the
201
- child), a self-contained `<task-notification>` block is queued in memory for
202
- the parent session and injected into the parent's **next** `chat.message` hook
203
- fire (prepended onto the first text part — the graceful-degradation channel the
204
- TUI toast workaround uses). If the parent never sends another message, the
205
- notification stays queued. The `<task-notification>` carries the task ID, alias,
206
- agent, state, description and a `Result:`-prefixed summary so the parent can act
207
- without an extra tool call.
232
+ **Agent activity visibility.** While `pantheon_delegation_read` blocks on a
233
+ running job, it samples the child session's messages every ~2s and appends the
234
+ collected lines to the report as a trailing `## Agent Activity` section (latest
235
+ tool calls with truncated args, or user/assistant text, capped at ~200 chars per
236
+ line) — so you can see what the agent is doing during the wait. Likewise,
237
+ `pantheon_delegation_list` shows a `last activity:` line for running jobs.
238
+ Fail-open: if the child session messages are unavailable (or empty), the read
239
+ returns the report exactly as before — the activity sampling never breaks the
240
+ delegation read.
241
+
242
+ **Completion visibility — no chat injection.** User policy: ZERO delegation
243
+ notifications in the chat transcript — no `<task-notification>` block is ever
244
+ injected into a `chat.message` output (the old queue + flush channel was
245
+ removed). When a job reaches a terminal state (completion observed via
246
+ `session.idle`/`session.error` on the child, or the timeout finalize path), the
247
+ plugin writes a file-only audit log line (echo opt-in via `PANTHEON_HOOKS_LOG`).
248
+ Completion visibility lives in the legitimate channels: the board `[unread]`
249
+ marker (`pantheon_delegation_list`), `pantheon_delegation_read`, TUI toasts
250
+ (pantheon-hooks, `PANTHEON_TOASTS` gate) and compaction carry-forward. Reconcile
251
+ is an acknowledgment, not a completion — it never re-fires the terminal
252
+ transition.
208
253
 
209
254
  **Timeout:** `background_delegation.timeout_ms` (default `900000` = 15 min). A job
210
255
  that has not reached a terminal state is finalized as `error`/`timedOut`.
@@ -219,13 +264,80 @@ terminal delegations (capped at `background_delegation.max_compaction_items`,
219
264
  default 10) are carried into the compacted context so the parent doesn't lose
220
265
  track of in-flight work.
221
266
 
267
+ **Compaction summary v2 (1.3.4):** the `experimental.session.compacting` hook
268
+ now emits sections in a stable order — `<pantheon-context directive>`
269
+ ("preserve these sections verbatim in the summarized context", emitted only
270
+ when at least one other section follows), `<mission_context>` (active,
271
+ non-done goals), `<todo_context>` (pending todos), then the delegation blocks
272
+ (running, then unread terminal ≤ `max_compaction_items`) byte-for-byte
273
+ unchanged. Each source is fail-open: a disabled, unscoped (no sessionID),
274
+ throwing, or empty source omits its section (warned to `hooks.log`), and a
275
+ totally-empty state returns nothing — the hook injects nothing, preserving
276
+ the pre-1.3.4 behavior.
277
+
278
+ **Post-compaction todo preservation (1.3.4):** the session's todo list is
279
+ captured via the `session.todo` GET at compacting time and restored by
280
+ intercepting the first `todowrite` after `session.compacted` — opencode
281
+ 1.18.x exposes no todo write API, so that first `todowrite` has its args
282
+ rewritten with the exact captured list (no model cooperation, zero transcript
283
+ noise). Subsequent `todowrite` calls inside the 5s restore window are denied
284
+ with a clear "retry in a moment" error; the snapshot TTL is 60s. Fail-open: a
285
+ failed capture, expired snapshot, or malformed hook output degrades to a
286
+ logged warn and pass-through.
287
+
288
+ **Post-compaction state re-assertion (1.3.4):** on `session.compacted`, fresh
289
+ state — running/unread delegations from the board + active goals, capped at 10
290
+ lines — is re-injected as a `<system-reminder>` into the session's next
291
+ message via the shared chat-reminder buffer (`chat-reminders.ts`, the same P0
292
+ messageID guard that protects subagent fires). A session with nothing to
293
+ assert is a silent skip.
294
+
295
+ **Preemptive compaction check (1.3.4):** a pure threshold core
296
+ (`preemptive-compact.ts`) warns the model before the context fills: at 78%
297
+ usage, re-warning only when usage rose ≥5pp since the last warning. It is
298
+ dormant / ready-to-wire — opencode 1.18.x exposes no runtime context-usage
299
+ percentage, so nothing observes it yet; when a source appears, the caller
300
+ wires it in (the enqueue callback is injected).
301
+
302
+ **Model API-key validation (1.3.4):** `pantheon_delegate` gates the resolved
303
+ child model's provider before dispatching (single source of truth:
304
+ `routing.yml` preset definitions' `apiKeyEnv` — the same check `applyPreset`
305
+ enforces at startup). If an **auto-resolved** model (`options.agentModels` or
306
+ the active preset) points to a provider that requires an API key
307
+ (`apiKeyEnv`) and the env var is unset, the delegate falls back to
308
+ `opencode/deepseek-v4-flash-free` (validated the same way). If the fallback is
309
+ also unusable, the tool returns a clear error **text** (never throws) naming
310
+ the missing env var — and registers **no job** on the board. An **explicit**
311
+ `model` passed by the caller is always respected (warned, not overridden).
312
+ Setting `PANTHEON_MODEL_PRESET` to a preset whose providers have keys, or
313
+ filling the required `PANTHEON_*_API_KEY` env var for the preset's provider,
314
+ resolves the fallback path. If nothing resolves at all (no model, no preset),
315
+ the child keeps using opencode's default model (warned).
316
+
317
+ **agentModels wiring (1.3.4):** the delegate now also resolves the child model
318
+ via `routing.yml` — `loadRoutingAgentModels` extracts the per-agent models of
319
+ the FIRST-listed preset (the static default, `go-deepseek` today) and passes
320
+ them as `options.agentModels`, branch (b) of the resolve precedence: explicit
321
+ `model` > `options.agentModels` > active preset > opencode default. Delegation
322
+ no longer depends exclusively on the active preset — a delegated child gets a
323
+ sane per-agent model even when no preset is active. Fail-open: a missing or
324
+ unparseable routing.yml yields `{}` (previous behavior, warned).
325
+
326
+ **Delegation log hygiene (1.3.4):** `delegations.log` now records the real
327
+ `task_id` (omitted when empty, never `""`) and a **numeric** `duration_ms`
328
+ (null when unset) so downstream aggregation works. The idle-flush log entry is
329
+ deduplicated: the flush logs a summary (count + aggregated line count) and the
330
+ reminder content is logged exactly once, at `chat.message` delivery — no more
331
+ duplicate lines for the same notification.
332
+
222
333
  **Env vars & config:**
223
334
 
224
335
  - `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` — required (see launch block above).
225
336
  - `PANTHEON_TOASTS` — TUI toast gate for the delegation lifecycle signals.
226
337
  Default `{errors, delegations, council}`; `PANTHEON_TOASTS=off` disables all
227
- TUI toasts (delegation signals then only surface via the chat.message
228
- `<task-notification>`/`<system-reminder>` channel).
338
+ TUI toasts (delegation signals then only surface via the board `[unread]`
339
+ marker, `pantheon_delegation_read` and the on-disk audit log — never via
340
+ chat.message injection from the plugin).
229
341
  - `background_delegation` section in `src/routing.yml` — `timeout_ms`,
230
342
  `poll_ms`, `max_compaction_items`, `prune_ttl_ms`, `read_only_agents`,
231
343
  `session_max`, `retry_count`.
@@ -233,6 +345,30 @@ track of in-flight work.
233
345
  See `src/agents/zeus.md` for the agent-facing delegation protocol and
234
346
  `src/pantheon/delegation.ts` for the tool implementations.
235
347
 
348
+ ### Real-time Delegations panel (1.3.4)
349
+
350
+ The TUI sidebar ships a live **Delegations** panel (open by default, above the
351
+ collapsed Sessions list). Its primary source is `api.client.session.children`,
352
+ so it shows **both** kinds of background work in one place:
353
+
354
+ - **`pantheon_delegate` children** — rendered with their board alias tag
355
+ (`[apo-1]`) and enriched from the `.pantheon/delegations/` report.
356
+ - **Native `task()` children** — rendered with a distinct `[task]` tag (info
357
+ color) when no board report exists for the session.
358
+
359
+ Rows animate through the delegation lifecycle — a 140ms spinner plus
360
+ `DELEGATING` / `WORKING` / `READING RESULT` / `DONE` / `DONE (TIMED OUT)` /
361
+ `ERROR` / `CANCELLED` — and **clicking a row navigates to the child session**
362
+ (route API, guarded; mouse enabled in `tui.json`). The panel also reads the
363
+ md reports from **every** session (`readAllDelegationEntries`), so the full
364
+ delegation history renders even when no session is focused. Refresh is driven
365
+ by session events + `message.part.updated/removed` (live version bumps) with a
366
+ 1s safety poll; every re-fetch appends a diagnostic
367
+ `panel: children=N md=N events=N` line to `.pantheon/logs/hooks.log`
368
+ (silence-by-default policy — see `PANTHEON_HOOKS_LOG`). Everything is
369
+ fail-open: a missing children API or directory renders `Delegations (0)` /
370
+ the md history instead of erroring.
371
+
236
372
  > **Modes:** Interactive TUI with checkbox selection (default TTY), or `--headless` for scripts/CI.
237
373
  > **Minimal:** `--headless --no-mcp` installs only agents (~2s).
238
374
  > **Full:** `--headless` also creates Python venv + MCP servers (memory, persistence, KV, vision).
@@ -750,9 +886,15 @@ to Mnemosyne so counts stay accurate and consistent.
750
886
 
751
887
  ### What CI enforces automatically
752
888
 
753
- `release.yml` validates version consistency across all manifests (`package.json`,
754
- `plugin.json`, `CHANGELOG.md`, and the README badge) before publishing. If they diverge,
755
- the release is blocked until Mnemosyne reconciles them.
889
+ `release.yml` validates version consistency across the package manifests
890
+ (`package.json`, `plugin.json`, `pyproject.toml`, and the TUI package) before
891
+ publishing. If they diverge, the release is blocked until they are reconciled.
892
+
893
+ Beta releases are calculated from npm's published `latest` at workflow runtime:
894
+ the default is the next patch, formatted as
895
+ `<next-stable>-beta.<PR>.<short-sha>`. Labels `release:beta:minor` and
896
+ `release:beta:major` request minor or major calculation. The workflow owns the
897
+ manifest update, tag, and publish; it does not assume a fixed future version.
756
898
 
757
899
  ### Anti-patterns
758
900
 
@@ -6,205 +6,262 @@
6
6
  *
7
7
  * Thin CLI wrapper that parses arguments and delegates to
8
8
  * installOpenCode() from scripts/install/opencode.mjs.
9
+ *
10
+ * Dual-version install (Phase 3): the generated config is V1-shaped and valid
11
+ * under BOTH OpenCode V1 (`opencode` 1.18.x) and V2 (`opencode2`
12
+ * v0.0.0-next-17444). V2 reads the same config locations and normalizes V1
13
+ * fields in memory — no native V2 conversion. `--version v2` labels the
14
+ * install for the operator; state isolation is a runtime concern
15
+ * (OPENCODE_DB ~/.local/share/opencode/opencode-v2.db, service port 49375).
9
16
  */
10
- import fs from 'fs';
11
- import os from 'os';
12
- import path from 'path';
13
- import { fileURLToPath } from 'url';
17
+ import fs from 'fs'
18
+ import os from 'os'
19
+ import path from 'path'
20
+ import { fileURLToPath } from 'url'
14
21
 
15
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
- const ROOT = path.resolve(__dirname, '..');
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
23
+ const ROOT = path.resolve(__dirname, '..')
17
24
 
18
- const REQUIRED_NODE_MAJOR = 18;
25
+ const REQUIRED_NODE_MAJOR = 18
19
26
  if (parseInt(process.versions.node.split('.')[0], 10) < REQUIRED_NODE_MAJOR) {
20
- console.error(`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`);
21
- process.exit(1);
27
+ console.error(`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`)
28
+ process.exit(1)
29
+ }
30
+
31
+ function readVersion() {
32
+ try {
33
+ const pkgPath = path.join(ROOT, 'package.json')
34
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '?'
35
+ } catch {
36
+ return '?'
37
+ }
22
38
  }
23
39
 
24
40
  function printUsage() {
25
- console.log('Pantheon OpenCode — Multi-agent orchestration platform');
26
- console.log('');
27
- console.log('Usage:');
28
- console.log(' npx pantheon-opencode init # Install globally');
29
- console.log(' npx pantheon-opencode init --project # Install in project');
30
- console.log(' npx pantheon-opencode init --dry-run # Preview only');
31
- console.log(' npx pantheon-opencode init --no-mcp # Skip MCP + venv');
32
- console.log(' npx pantheon-opencode init --force # Overwrite + recreate venv');
33
- console.log(' npx pantheon-opencode init --doctor # Run health check after install');
34
- console.log(' npx pantheon-opencode init --interactive # Force interactive TUI mode');
35
- console.log(' npx pantheon-opencode init --headless # Force non-interactive mode');
36
- console.log(' npx pantheon-opencode init -y # Skip confirmations, use defaults');
37
- console.log(' npx pantheon-opencode init --preset <name> # Install and activate model preset');
38
- console.log(' npx pantheon-opencode set-tier <name> # Set active model preset (global)');
39
- console.log(' npx pantheon-opencode set-tier <name> --project # Set active model preset (project)');
40
- console.log(' npx pantheon-opencode prune # List legacy artifacts (dry-run)');
41
- console.log(' npx pantheon-opencode prune --apply # Remove stale backups');
42
- console.log(' npx pantheon-opencode --help # Show this help');
41
+ console.log('Pantheon OpenCode — Multi-agent orchestration platform')
42
+ console.log('')
43
+ console.log('Usage:')
44
+ console.log(' npx pantheon-opencode init # Install globally')
45
+ console.log(' npx pantheon-opencode init --project # Install in project')
46
+ console.log(' npx pantheon-opencode init --dry-run # Preview only')
47
+ console.log(' npx pantheon-opencode init --no-mcp # Skip MCP + venv')
48
+ console.log(' npx pantheon-opencode init --force # Overwrite + recreate venv')
49
+ console.log(' npx pantheon-opencode init --doctor # Run health check after install')
50
+ console.log(' npx pantheon-opencode init --interactive # Force interactive TUI mode')
51
+ console.log(' npx pantheon-opencode init --headless # Force non-interactive mode')
52
+ console.log(' npx pantheon-opencode init -y # Skip confirmations, use defaults')
53
+ console.log(' npx pantheon-opencode init --version v2 # Label install for OpenCode V2 (beta)')
54
+ console.log(' npx pantheon-opencode init --preset <name> # Install and activate model preset')
55
+ console.log(' npx pantheon-opencode set-tier <name> # Set active model preset (global)')
56
+ console.log(
57
+ ' npx pantheon-opencode set-tier <name> --project # Set active model preset (project)',
58
+ )
59
+ console.log(' npx pantheon-opencode prune # List legacy artifacts (dry-run)')
60
+ console.log(' npx pantheon-opencode prune --apply # Remove stale backups')
61
+ console.log(' npx pantheon-opencode doctor [--profile] # Run health check')
62
+ console.log(' npx pantheon-opencode --version, -v # Print version and exit')
63
+ console.log(' npx pantheon-opencode --help # Show this help')
43
64
  }
44
65
 
45
66
  async function main() {
46
- const args = process.argv.slice(2);
47
- const command = args[0];
67
+ const args = process.argv.slice(2)
68
+ const command = args[0]
69
+
70
+ // --version / -v: print the package version and exit. Must run before any
71
+ // install/help dispatch so it never touches the filesystem beyond reading
72
+ // package.json.
73
+ if (command === '--version' || command === '-v') {
74
+ console.log(readVersion())
75
+ process.exit(0)
76
+ }
48
77
 
49
78
  // prune handles its own --help (delegated to scripts/prune.mjs), so its
50
79
  // dispatch must run BEFORE the generic --help check.
51
80
  if (command === 'prune') {
52
- const { spawnSync } = await import('child_process');
53
- const pruneScript = path.join(ROOT, 'scripts', 'prune.mjs');
81
+ const { spawnSync } = await import('child_process')
82
+ const pruneScript = path.join(ROOT, 'scripts', 'prune.mjs')
54
83
  const result = spawnSync(process.execPath, [pruneScript, ...args.slice(1)], {
55
84
  stdio: 'inherit',
56
85
  cwd: ROOT,
57
- });
58
- process.exit(result.status ?? 1);
86
+ })
87
+ process.exit(result.status ?? 1)
59
88
  }
60
89
 
61
- if (args.includes('--help')) { printUsage(); process.exit(0); }
90
+ // doctor delegates to scripts/doctor.mjs (same spawnSync pattern as prune).
91
+ // Forwarding the FULL args (profile, target, --fix, ...) keeps the packaged
92
+ // CLI a thin wrapper; the doctor banner reports the resolved profile.
93
+ if (command === 'doctor') {
94
+ const { spawnSync } = await import('child_process')
95
+ const doctorScript = path.join(ROOT, 'scripts', 'doctor.mjs')
96
+ const result = spawnSync(process.execPath, [doctorScript, ...args.slice(1)], {
97
+ stdio: 'inherit',
98
+ cwd: ROOT,
99
+ })
100
+ process.exit(result.status ?? 1)
101
+ }
102
+
103
+ if (args.includes('--help')) {
104
+ printUsage()
105
+ process.exit(0)
106
+ }
62
107
 
63
108
  if (command === 'set-tier') {
64
- const isProject = args.includes('--project');
65
- const isDryRun = args.includes('--dry-run');
66
- const name = args[1];
109
+ const isProject = args.includes('--project')
110
+ const isDryRun = args.includes('--dry-run')
111
+ const name = args[1]
67
112
 
68
- const { loadPresetDefs } = await import('../src/pantheon/presets.mjs');
69
- const { writeActivePreset } = await import('../scripts/install/model-picker.mjs');
70
- const presets = loadPresetDefs();
71
- const presetList = `none, ${Object.keys(presets).join(', ')}`;
113
+ const { loadPresetDefs } = await import('../src/pantheon/presets.mjs')
114
+ const { writeActivePreset } = await import('../scripts/install/model-picker.mjs')
115
+ const presets = loadPresetDefs()
116
+ const presetList = `none, ${Object.keys(presets).join(', ')}`
72
117
 
73
118
  if (!name) {
74
- console.error('❌ set-tier requires a preset name');
75
- console.error(` Available: ${presetList}`);
76
- process.exit(1);
119
+ console.error('❌ set-tier requires a preset name')
120
+ console.error(` Available: ${presetList}`)
121
+ process.exit(1)
77
122
  }
78
123
  if (name !== 'none' && !presets[name]) {
79
- console.error(`❌ Unknown preset "${name}"`);
80
- console.error(` Available: ${presetList}`);
81
- process.exit(1);
124
+ console.error(`❌ Unknown preset "${name}"`)
125
+ console.error(` Available: ${presetList}`)
126
+ process.exit(1)
82
127
  }
83
128
 
84
129
  // Fail-fast: every provider's API key env var must be set before writing
85
130
  if (name !== 'none') {
86
131
  for (const [pid, provider] of Object.entries(presets[name].providers || {})) {
87
132
  if (!process.env[provider.apiKeyEnv]) {
88
- console.error(`❌ Missing required API key configuration for provider "${pid}"`);
89
- console.error(' Set the provider API key environment variable and retry.');
90
- process.exit(1);
133
+ console.error(`❌ Missing required API key configuration for provider "${pid}"`)
134
+ console.error(' Set the provider API key environment variable and retry.')
135
+ process.exit(1)
91
136
  }
92
137
  }
93
138
  }
94
139
 
95
140
  const presetDir = isProject
96
141
  ? process.cwd()
97
- : path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'opencode');
98
- const filePath = path.join(presetDir, '.pantheon', 'active-preset.json');
99
- const bakPath = path.join(presetDir, '.pantheon', 'active-preset.json.bak');
142
+ : path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'opencode')
143
+ const filePath = path.join(presetDir, '.pantheon', 'active-preset.json')
144
+ const bakPath = path.join(presetDir, '.pantheon', 'active-preset.json.bak')
100
145
 
101
- let prevName = '(none)';
146
+ let prevName = '(none)'
102
147
  if (fs.existsSync(filePath)) {
103
148
  try {
104
- prevName = JSON.parse(fs.readFileSync(filePath, 'utf8')).preset || '(unknown)';
105
- } catch { prevName = '(unknown)'; }
149
+ prevName = JSON.parse(fs.readFileSync(filePath, 'utf8')).preset || '(unknown)'
150
+ } catch {
151
+ prevName = '(unknown)'
152
+ }
106
153
  }
107
154
 
108
155
  if (name === 'none') {
109
156
  if (isDryRun) {
110
- console.log('[dry-run] Would clear model preset');
157
+ console.log('[dry-run] Would clear model preset')
111
158
  } else {
112
159
  if (fs.existsSync(filePath)) {
113
- fs.mkdirSync(path.dirname(bakPath), { recursive: true });
114
- fs.copyFileSync(filePath, bakPath);
115
- fs.rmSync(filePath, { force: true });
160
+ fs.mkdirSync(path.dirname(bakPath), { recursive: true })
161
+ fs.copyFileSync(filePath, bakPath)
162
+ fs.rmSync(filePath, { force: true })
116
163
  }
117
- console.log('Model preset cleared. Using opencode defaults.');
118
- console.log(` Backup: ${bakPath}`);
164
+ console.log('Model preset cleared. Using opencode defaults.')
165
+ console.log(` Backup: ${bakPath}`)
119
166
  }
120
- return;
167
+ return
121
168
  }
122
169
 
123
- const result = writeActivePreset(presetDir, name, { dryRun: isDryRun, source: 'cli' });
170
+ const result = writeActivePreset(presetDir, name, { dryRun: isDryRun, source: 'cli' })
124
171
 
125
- console.log(`Model preset set: ${prevName} → ${name}`);
172
+ console.log(`Model preset set: ${prevName} → ${name}`)
126
173
  for (const [agent, spec] of Object.entries(presets[name].agents || {})) {
127
- console.log(` ${agent}: ${spec.model} [${spec.reasoning_effort}]`);
174
+ console.log(` ${agent}: ${spec.model} [${spec.reasoning_effort}]`)
128
175
  }
129
- const providerNames = Object.keys(presets[name].providers || {});
130
- console.log(` Providers: ${providerNames.join(', ') || 'none'}`);
131
- if (result.backupPath) console.log(` Backup: ${result.backupPath}`);
132
- return;
176
+ const providerNames = Object.keys(presets[name].providers || {})
177
+ console.log(` Providers: ${providerNames.join(', ') || 'none'}`)
178
+ if (result.backupPath) console.log(` Backup: ${result.backupPath}`)
179
+ return
133
180
  }
134
181
 
135
182
  if (command === 'init' || !command) {
136
- const isProject = args.includes('--project');
137
- const isDryRun = args.includes('--dry-run');
138
- const skipMCP = args.includes('--no-mcp');
139
- const forceReinstall = args.includes('--force');
140
- const runDoctor = args.includes('--doctor');
141
- const forceInteractive = args.includes('--interactive');
142
- const forceHeadless = args.includes('--headless');
143
- const autoYes = args.includes('--yes') || args.includes('-y');
144
- const presetIndex = args.indexOf('--preset');
145
- const presetOpt = presetIndex >= 0 ? (args[presetIndex + 1] ?? null) : null;
146
-
147
- const components = ['agents', 'skills', 'instructions', 'commands', 'plugins'];
148
- if (!skipMCP) components.push('runtime');
183
+ const isProject = args.includes('--project')
184
+ const isDryRun = args.includes('--dry-run')
185
+ const skipMCP = args.includes('--no-mcp')
186
+ const forceReinstall = args.includes('--force')
187
+ const runDoctor = args.includes('--doctor')
188
+ const forceInteractive = args.includes('--interactive')
189
+ const forceHeadless = args.includes('--headless')
190
+ const autoYes = args.includes('--yes') || args.includes('-y')
191
+ const presetIndex = args.indexOf('--preset')
192
+ const presetOpt = presetIndex >= 0 ? (args[presetIndex + 1] ?? null) : null
193
+ // --version v1|v2 labels the install target (informational; the config is
194
+ // shared and V1-shaped under both versions). Invalid values fail fast.
195
+ const versionIndex = args.indexOf('--version')
196
+ const versionOpt = versionIndex >= 0 ? (args[versionIndex + 1] ?? null) : null
197
+ if (versionOpt !== null && versionOpt !== 'v1' && versionOpt !== 'v2') {
198
+ console.error(`❌ Invalid --version "${versionOpt}" — expected v1 or v2`)
199
+ process.exit(1)
200
+ }
201
+
202
+ const components = ['agents', 'skills', 'instructions', 'commands', 'plugins']
203
+ if (!skipMCP) components.push('runtime')
149
204
 
150
205
  // Version info
151
- let version = '?';
152
- try {
153
- const pkgPath = path.join(ROOT, 'package.json');
154
- version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '?';
155
- } catch { /* use default */ }
206
+ const version = readVersion()
156
207
 
157
- console.log('');
208
+ console.log('')
158
209
  if (!forceInteractive || isDryRun) {
159
- console.log(`Pantheon OpenCode v${version} — ${isDryRun ? 'DRY RUN' : 'Installing...'}`);
160
- console.log('');
210
+ console.log(`Pantheon OpenCode v${version} — ${isDryRun ? 'DRY RUN' : 'Installing...'}`)
211
+ console.log('')
161
212
  }
162
213
 
163
214
  try {
164
- const { installOpenCode } = await import('../scripts/install/opencode.mjs');
165
- const target = isProject ? process.cwd() : undefined;
215
+ const { installOpenCode } = await import('../scripts/install/opencode.mjs')
216
+ const target = isProject ? process.cwd() : undefined
166
217
  await installOpenCode(target, isDryRun, forceReinstall, components, {
167
218
  interactive: forceInteractive,
168
219
  headless: forceHeadless,
169
220
  yes: autoYes,
170
221
  preset: presetOpt,
171
- });
222
+ version: versionOpt ?? 'v1',
223
+ })
172
224
  } catch (err) {
173
- console.error(`❌ Installation failed: ${err.message}\n Run with --no-mcp to skip Python dependencies:\n npx pantheon-opencode init --no-mcp\n Or retry with --force to recreate the venv:\n npx pantheon-opencode init --force`);
174
- process.exit(1);
225
+ console.error(
226
+ `❌ Installation failed: ${err.message}\n Run with --no-mcp to skip Python dependencies:\n npx pantheon-opencode init --no-mcp\n Or retry with --force to recreate the venv:\n npx pantheon-opencode init --force`,
227
+ )
228
+ process.exit(1)
175
229
  }
176
230
 
177
231
  if (runDoctor && !isDryRun) {
178
- console.log('');
179
- console.log(' Running health check...');
232
+ console.log('')
233
+ console.log(' Running health check...')
180
234
  try {
181
- const { spawnSync } = await import('child_process');
182
- const doctorScript = path.join(ROOT, 'scripts', 'doctor.mjs');
183
- spawnSync(process.execPath, [doctorScript], { stdio: 'inherit', cwd: ROOT });
235
+ const { spawnSync } = await import('child_process')
236
+ const doctorScript = path.join(ROOT, 'scripts', 'doctor.mjs')
237
+ spawnSync(process.execPath, [doctorScript], { stdio: 'inherit', cwd: ROOT })
184
238
  } catch {
185
- console.log(' ⚠️ Could not run doctor.mjs');
239
+ console.log(' ⚠️ Could not run doctor.mjs')
186
240
  }
187
241
  }
188
242
 
189
- console.log('');
190
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
191
- console.log(` ✅ Pantheon OpenCode v${version} installed!`);
192
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
193
- console.log('');
194
- console.log(' Next steps:');
195
- console.log(' 1. Verify installation:');
196
- console.log(' npx pantheon-opencode doctor');
197
- console.log(' 2. Launch your AI coding tool (OpenCode, Claude Code, Cursor, etc.)');
198
- console.log(' 3. Invoke agents with @agent-name in chat');
199
- console.log(' 4. For project-local install:');
200
- console.log(' npx pantheon-opencode init --project');
201
- console.log('');
202
-
203
- return;
243
+ console.log('')
244
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
245
+ console.log(` ✅ Pantheon OpenCode v${version} installed!`)
246
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
247
+ console.log('')
248
+ console.log(' Next steps:')
249
+ console.log(' 1. Verify installation:')
250
+ console.log(' npx pantheon-opencode doctor')
251
+ console.log(' 2. Launch your AI coding tool (OpenCode, Claude Code, Cursor, etc.)')
252
+ console.log(' 3. Invoke agents with @agent-name in chat')
253
+ console.log(' 4. For project-local install:')
254
+ console.log(' npx pantheon-opencode init --project')
255
+ console.log('')
256
+
257
+ return
204
258
  }
205
259
 
206
- printUsage();
207
- process.exit(1);
260
+ printUsage()
261
+ process.exit(1)
208
262
  }
209
263
 
210
- main().catch(err => { console.error('Error:', err.stack); process.exit(1); });
264
+ main().catch((err) => {
265
+ console.error('Error:', err.stack)
266
+ process.exit(1)
267
+ })
package/opencode.json CHANGED
@@ -11,7 +11,9 @@
11
11
  }
12
12
  },
13
13
  "default_agent": "zeus",
14
- "subagent_depth": 2,
14
+ "experimental": {
15
+ "subagent_depth": 2
16
+ },
15
17
  "model": "opencode-go/deepseek-v4-flash",
16
18
  "small_model": "opencode-go/deepseek-v4-flash",
17
19
  "permission": {
@@ -28,11 +30,6 @@
28
30
  "pip *": "allow"
29
31
  }
30
32
  },
31
- "instructions": ["AGENTS.md", "src/instructions/*.instructions.md"],
32
- "plugin": [
33
- "/home/ils15/pantheon/src/plugin.ts",
34
- "/home/ils15/pantheon/src/plugins/pantheon-hooks.ts"
35
- ],
36
33
  "mcp": {
37
34
  "bifrost": {
38
35
  "type": "remote",