pantheon-opencode 1.3.3 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -12
- package/bin/pantheon-init.mjs +160 -120
- package/opencode.json +0 -4
- package/package.json +7 -4
- package/scripts/doctor.mjs +56 -22
- package/scripts/hooks/on-subagent-delegation-stop.sh +22 -5
- package/scripts/install/opencode.mjs +108 -26
- package/scripts/install/plugin.mjs +112 -28
- package/scripts/install-mcp.mjs +1 -1
- package/scripts/redaction-gate.mjs +69 -0
- package/scripts/release-notes.mjs +317 -0
- package/scripts/test-context-compression.sh +5 -2
- package/scripts/validate-package.mjs +56 -0
- package/scripts/versioning.mjs +117 -18
- package/src/agents/zeus.md +6 -2
- package/src/pantheon/background-job-board.ts +24 -38
- package/src/pantheon/chat-reminders.ts +79 -0
- package/src/pantheon/compaction-assert.ts +160 -0
- package/src/pantheon/delegation-compaction.ts +143 -16
- package/src/pantheon/delegation-finalize.ts +8 -1
- package/src/pantheon/delegation-notify.ts +19 -157
- package/src/pantheon/delegation.ts +330 -28
- package/src/pantheon/dispatch-guard.ts +9 -4
- package/src/pantheon/goal-loop.ts +7 -4
- package/src/pantheon/hashline/tool.ts +2 -1
- package/src/pantheon/logger.ts +117 -0
- package/src/pantheon/plugin-once.ts +76 -0
- package/src/pantheon/preemptive-compact.ts +76 -0
- package/src/pantheon/presets.d.mts +31 -0
- package/src/pantheon/presets.mjs +105 -3
- package/src/pantheon/todo-enforcer.ts +79 -27
- package/src/pantheon/todo-preserve.ts +295 -0
- package/src/plugin.ts +177 -50
- package/src/plugins/pantheon-hooks.ts +35 -22
- package/src/plugins/tui/dist/server.d.ts +17 -0
- package/src/plugins/tui/dist/server.d.ts.map +1 -0
- package/src/plugins/tui/dist/server.js +20 -0
- package/src/plugins/tui/dist/server.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +303 -1
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +977 -350
- package/src/plugins/tui/dist/tui.tsx +1427 -150
- package/src/plugins/tui/package.json +4 -3
- package/src/plugins/tui/src/index.tsx +1427 -150
- package/src/plugins/tui/src/server.ts +15 -0
- package/src/plugins/tui/tsdown.config.ts +1 -1
- 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.
|
|
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>
|
|
@@ -196,15 +196,27 @@ via three delegation tools, tracked on a persistent job board
|
|
|
196
196
|
| `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
197
|
| `pantheon_delegation_list` | `{}` | Lists the current session's delegations with `[unread]` on finished, unreconciled jobs. |
|
|
198
198
|
|
|
199
|
-
**
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
199
|
+
**Agent activity visibility.** While `pantheon_delegation_read` blocks on a
|
|
200
|
+
running job, it samples the child session's messages every ~2s and appends the
|
|
201
|
+
collected lines to the report as a trailing `## Agent Activity` section (latest
|
|
202
|
+
tool calls with truncated args, or user/assistant text, capped at ~200 chars per
|
|
203
|
+
line) — so you can see what the agent is doing during the wait. Likewise,
|
|
204
|
+
`pantheon_delegation_list` shows a `last activity:` line for running jobs.
|
|
205
|
+
Fail-open: if the child session messages are unavailable (or empty), the read
|
|
206
|
+
returns the report exactly as before — the activity sampling never breaks the
|
|
207
|
+
delegation read.
|
|
208
|
+
|
|
209
|
+
**Completion visibility — no chat injection.** User policy: ZERO delegation
|
|
210
|
+
notifications in the chat transcript — no `<task-notification>` block is ever
|
|
211
|
+
injected into a `chat.message` output (the old queue + flush channel was
|
|
212
|
+
removed). When a job reaches a terminal state (completion observed via
|
|
213
|
+
`session.idle`/`session.error` on the child, or the timeout finalize path), the
|
|
214
|
+
plugin writes a file-only audit log line (echo opt-in via `PANTHEON_HOOKS_LOG`).
|
|
215
|
+
Completion visibility lives in the legitimate channels: the board `[unread]`
|
|
216
|
+
marker (`pantheon_delegation_list`), `pantheon_delegation_read`, TUI toasts
|
|
217
|
+
(pantheon-hooks, `PANTHEON_TOASTS` gate) and compaction carry-forward. Reconcile
|
|
218
|
+
is an acknowledgment, not a completion — it never re-fires the terminal
|
|
219
|
+
transition.
|
|
208
220
|
|
|
209
221
|
**Timeout:** `background_delegation.timeout_ms` (default `900000` = 15 min). A job
|
|
210
222
|
that has not reached a terminal state is finalized as `error`/`timedOut`.
|
|
@@ -219,13 +231,80 @@ terminal delegations (capped at `background_delegation.max_compaction_items`,
|
|
|
219
231
|
default 10) are carried into the compacted context so the parent doesn't lose
|
|
220
232
|
track of in-flight work.
|
|
221
233
|
|
|
234
|
+
**Compaction summary v2 (1.3.4):** the `experimental.session.compacting` hook
|
|
235
|
+
now emits sections in a stable order — `<pantheon-context directive>`
|
|
236
|
+
("preserve these sections verbatim in the summarized context", emitted only
|
|
237
|
+
when at least one other section follows), `<mission_context>` (active,
|
|
238
|
+
non-done goals), `<todo_context>` (pending todos), then the delegation blocks
|
|
239
|
+
(running, then unread terminal ≤ `max_compaction_items`) byte-for-byte
|
|
240
|
+
unchanged. Each source is fail-open: a disabled, unscoped (no sessionID),
|
|
241
|
+
throwing, or empty source omits its section (warned to `hooks.log`), and a
|
|
242
|
+
totally-empty state returns nothing — the hook injects nothing, preserving
|
|
243
|
+
the pre-1.3.4 behavior.
|
|
244
|
+
|
|
245
|
+
**Post-compaction todo preservation (1.3.4):** the session's todo list is
|
|
246
|
+
captured via the `session.todo` GET at compacting time and restored by
|
|
247
|
+
intercepting the first `todowrite` after `session.compacted` — opencode
|
|
248
|
+
1.18.x exposes no todo write API, so that first `todowrite` has its args
|
|
249
|
+
rewritten with the exact captured list (no model cooperation, zero transcript
|
|
250
|
+
noise). Subsequent `todowrite` calls inside the 5s restore window are denied
|
|
251
|
+
with a clear "retry in a moment" error; the snapshot TTL is 60s. Fail-open: a
|
|
252
|
+
failed capture, expired snapshot, or malformed hook output degrades to a
|
|
253
|
+
logged warn and pass-through.
|
|
254
|
+
|
|
255
|
+
**Post-compaction state re-assertion (1.3.4):** on `session.compacted`, fresh
|
|
256
|
+
state — running/unread delegations from the board + active goals, capped at 10
|
|
257
|
+
lines — is re-injected as a `<system-reminder>` into the session's next
|
|
258
|
+
message via the shared chat-reminder buffer (`chat-reminders.ts`, the same P0
|
|
259
|
+
messageID guard that protects subagent fires). A session with nothing to
|
|
260
|
+
assert is a silent skip.
|
|
261
|
+
|
|
262
|
+
**Preemptive compaction check (1.3.4):** a pure threshold core
|
|
263
|
+
(`preemptive-compact.ts`) warns the model before the context fills: at 78%
|
|
264
|
+
usage, re-warning only when usage rose ≥5pp since the last warning. It is
|
|
265
|
+
dormant / ready-to-wire — opencode 1.18.x exposes no runtime context-usage
|
|
266
|
+
percentage, so nothing observes it yet; when a source appears, the caller
|
|
267
|
+
wires it in (the enqueue callback is injected).
|
|
268
|
+
|
|
269
|
+
**Model API-key validation (1.3.4):** `pantheon_delegate` gates the resolved
|
|
270
|
+
child model's provider before dispatching (single source of truth:
|
|
271
|
+
`routing.yml` preset definitions' `apiKeyEnv` — the same check `applyPreset`
|
|
272
|
+
enforces at startup). If an **auto-resolved** model (`options.agentModels` or
|
|
273
|
+
the active preset) points to a provider that requires an API key
|
|
274
|
+
(`apiKeyEnv`) and the env var is unset, the delegate falls back to
|
|
275
|
+
`opencode/deepseek-v4-flash-free` (validated the same way). If the fallback is
|
|
276
|
+
also unusable, the tool returns a clear error **text** (never throws) naming
|
|
277
|
+
the missing env var — and registers **no job** on the board. An **explicit**
|
|
278
|
+
`model` passed by the caller is always respected (warned, not overridden).
|
|
279
|
+
Setting `PANTHEON_MODEL_PRESET` to a preset whose providers have keys, or
|
|
280
|
+
filling the required `PANTHEON_*_API_KEY` env var for the preset's provider,
|
|
281
|
+
resolves the fallback path. If nothing resolves at all (no model, no preset),
|
|
282
|
+
the child keeps using opencode's default model (warned).
|
|
283
|
+
|
|
284
|
+
**agentModels wiring (1.3.4):** the delegate now also resolves the child model
|
|
285
|
+
via `routing.yml` — `loadRoutingAgentModels` extracts the per-agent models of
|
|
286
|
+
the FIRST-listed preset (the static default, `go-deepseek` today) and passes
|
|
287
|
+
them as `options.agentModels`, branch (b) of the resolve precedence: explicit
|
|
288
|
+
`model` > `options.agentModels` > active preset > opencode default. Delegation
|
|
289
|
+
no longer depends exclusively on the active preset — a delegated child gets a
|
|
290
|
+
sane per-agent model even when no preset is active. Fail-open: a missing or
|
|
291
|
+
unparseable routing.yml yields `{}` (previous behavior, warned).
|
|
292
|
+
|
|
293
|
+
**Delegation log hygiene (1.3.4):** `delegations.log` now records the real
|
|
294
|
+
`task_id` (omitted when empty, never `""`) and a **numeric** `duration_ms`
|
|
295
|
+
(null when unset) so downstream aggregation works. The idle-flush log entry is
|
|
296
|
+
deduplicated: the flush logs a summary (count + aggregated line count) and the
|
|
297
|
+
reminder content is logged exactly once, at `chat.message` delivery — no more
|
|
298
|
+
duplicate lines for the same notification.
|
|
299
|
+
|
|
222
300
|
**Env vars & config:**
|
|
223
301
|
|
|
224
302
|
- `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` — required (see launch block above).
|
|
225
303
|
- `PANTHEON_TOASTS` — TUI toast gate for the delegation lifecycle signals.
|
|
226
304
|
Default `{errors, delegations, council}`; `PANTHEON_TOASTS=off` disables all
|
|
227
|
-
TUI toasts (delegation signals then only surface via the
|
|
228
|
-
|
|
305
|
+
TUI toasts (delegation signals then only surface via the board `[unread]`
|
|
306
|
+
marker, `pantheon_delegation_read` and the on-disk audit log — never via
|
|
307
|
+
chat.message injection from the plugin).
|
|
229
308
|
- `background_delegation` section in `src/routing.yml` — `timeout_ms`,
|
|
230
309
|
`poll_ms`, `max_compaction_items`, `prune_ttl_ms`, `read_only_agents`,
|
|
231
310
|
`session_max`, `retry_count`.
|
|
@@ -233,6 +312,30 @@ track of in-flight work.
|
|
|
233
312
|
See `src/agents/zeus.md` for the agent-facing delegation protocol and
|
|
234
313
|
`src/pantheon/delegation.ts` for the tool implementations.
|
|
235
314
|
|
|
315
|
+
### Real-time Delegations panel (1.3.4)
|
|
316
|
+
|
|
317
|
+
The TUI sidebar ships a live **Delegations** panel (open by default, above the
|
|
318
|
+
collapsed Sessions list). Its primary source is `api.client.session.children`,
|
|
319
|
+
so it shows **both** kinds of background work in one place:
|
|
320
|
+
|
|
321
|
+
- **`pantheon_delegate` children** — rendered with their board alias tag
|
|
322
|
+
(`[apo-1]`) and enriched from the `.pantheon/delegations/` report.
|
|
323
|
+
- **Native `task()` children** — rendered with a distinct `[task]` tag (info
|
|
324
|
+
color) when no board report exists for the session.
|
|
325
|
+
|
|
326
|
+
Rows animate through the delegation lifecycle — a 140ms spinner plus
|
|
327
|
+
`DELEGATING` / `WORKING` / `READING RESULT` / `DONE` / `DONE (TIMED OUT)` /
|
|
328
|
+
`ERROR` / `CANCELLED` — and **clicking a row navigates to the child session**
|
|
329
|
+
(route API, guarded; mouse enabled in `tui.json`). The panel also reads the
|
|
330
|
+
md reports from **every** session (`readAllDelegationEntries`), so the full
|
|
331
|
+
delegation history renders even when no session is focused. Refresh is driven
|
|
332
|
+
by session events + `message.part.updated/removed` (live version bumps) with a
|
|
333
|
+
1s safety poll; every re-fetch appends a diagnostic
|
|
334
|
+
`panel: children=N md=N events=N` line to `.pantheon/logs/hooks.log`
|
|
335
|
+
(silence-by-default policy — see `PANTHEON_HOOKS_LOG`). Everything is
|
|
336
|
+
fail-open: a missing children API or directory renders `Delegations (0)` /
|
|
337
|
+
the md history instead of erroring.
|
|
338
|
+
|
|
236
339
|
> **Modes:** Interactive TUI with checkbox selection (default TTY), or `--headless` for scripts/CI.
|
|
237
340
|
> **Minimal:** `--headless --no-mcp` installs only agents (~2s).
|
|
238
341
|
> **Full:** `--headless` also creates Python venv + MCP servers (memory, persistence, KV, vision).
|
package/bin/pantheon-init.mjs
CHANGED
|
@@ -7,204 +7,244 @@
|
|
|
7
7
|
* Thin CLI wrapper that parses arguments and delegates to
|
|
8
8
|
* installOpenCode() from scripts/install/opencode.mjs.
|
|
9
9
|
*/
|
|
10
|
-
import fs from 'fs'
|
|
11
|
-
import os from 'os'
|
|
12
|
-
import path from 'path'
|
|
13
|
-
import { fileURLToPath } from 'url'
|
|
10
|
+
import fs from 'fs'
|
|
11
|
+
import os from 'os'
|
|
12
|
+
import path from 'path'
|
|
13
|
+
import { fileURLToPath } from 'url'
|
|
14
14
|
|
|
15
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
16
|
-
const ROOT = path.resolve(__dirname, '..')
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const ROOT = path.resolve(__dirname, '..')
|
|
17
17
|
|
|
18
|
-
const REQUIRED_NODE_MAJOR = 18
|
|
18
|
+
const REQUIRED_NODE_MAJOR = 18
|
|
19
19
|
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)
|
|
20
|
+
console.error(`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readVersion() {
|
|
25
|
+
try {
|
|
26
|
+
const pkgPath = path.join(ROOT, 'package.json')
|
|
27
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '?'
|
|
28
|
+
} catch {
|
|
29
|
+
return '?'
|
|
30
|
+
}
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
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(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
console.log(' npx pantheon-opencode
|
|
34
|
+
console.log('Pantheon OpenCode — Multi-agent orchestration platform')
|
|
35
|
+
console.log('')
|
|
36
|
+
console.log('Usage:')
|
|
37
|
+
console.log(' npx pantheon-opencode init # Install globally')
|
|
38
|
+
console.log(' npx pantheon-opencode init --project # Install in project')
|
|
39
|
+
console.log(' npx pantheon-opencode init --dry-run # Preview only')
|
|
40
|
+
console.log(' npx pantheon-opencode init --no-mcp # Skip MCP + venv')
|
|
41
|
+
console.log(' npx pantheon-opencode init --force # Overwrite + recreate venv')
|
|
42
|
+
console.log(' npx pantheon-opencode init --doctor # Run health check after install')
|
|
43
|
+
console.log(' npx pantheon-opencode init --interactive # Force interactive TUI mode')
|
|
44
|
+
console.log(' npx pantheon-opencode init --headless # Force non-interactive mode')
|
|
45
|
+
console.log(' npx pantheon-opencode init -y # Skip confirmations, use defaults')
|
|
46
|
+
console.log(' npx pantheon-opencode init --preset <name> # Install and activate model preset')
|
|
47
|
+
console.log(' npx pantheon-opencode set-tier <name> # Set active model preset (global)')
|
|
48
|
+
console.log(
|
|
49
|
+
' npx pantheon-opencode set-tier <name> --project # Set active model preset (project)',
|
|
50
|
+
)
|
|
51
|
+
console.log(' npx pantheon-opencode prune # List legacy artifacts (dry-run)')
|
|
52
|
+
console.log(' npx pantheon-opencode prune --apply # Remove stale backups')
|
|
53
|
+
console.log(' npx pantheon-opencode doctor [--profile] # Run health check')
|
|
54
|
+
console.log(' npx pantheon-opencode --version, -v # Print version and exit')
|
|
55
|
+
console.log(' npx pantheon-opencode --help # Show this help')
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
async function main() {
|
|
46
|
-
const args = process.argv.slice(2)
|
|
47
|
-
const command = args[0]
|
|
59
|
+
const args = process.argv.slice(2)
|
|
60
|
+
const command = args[0]
|
|
61
|
+
|
|
62
|
+
// --version / -v: print the package version and exit. Must run before any
|
|
63
|
+
// install/help dispatch so it never touches the filesystem beyond reading
|
|
64
|
+
// package.json.
|
|
65
|
+
if (command === '--version' || command === '-v') {
|
|
66
|
+
console.log(readVersion())
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}
|
|
48
69
|
|
|
49
70
|
// prune handles its own --help (delegated to scripts/prune.mjs), so its
|
|
50
71
|
// dispatch must run BEFORE the generic --help check.
|
|
51
72
|
if (command === 'prune') {
|
|
52
|
-
const { spawnSync } = await import('child_process')
|
|
53
|
-
const pruneScript = path.join(ROOT, 'scripts', 'prune.mjs')
|
|
73
|
+
const { spawnSync } = await import('child_process')
|
|
74
|
+
const pruneScript = path.join(ROOT, 'scripts', 'prune.mjs')
|
|
54
75
|
const result = spawnSync(process.execPath, [pruneScript, ...args.slice(1)], {
|
|
55
76
|
stdio: 'inherit',
|
|
56
77
|
cwd: ROOT,
|
|
57
|
-
})
|
|
58
|
-
process.exit(result.status ?? 1)
|
|
78
|
+
})
|
|
79
|
+
process.exit(result.status ?? 1)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// doctor delegates to scripts/doctor.mjs (same spawnSync pattern as prune).
|
|
83
|
+
// Forwarding the FULL args (profile, target, --fix, ...) keeps the packaged
|
|
84
|
+
// CLI a thin wrapper; the doctor banner reports the resolved profile.
|
|
85
|
+
if (command === 'doctor') {
|
|
86
|
+
const { spawnSync } = await import('child_process')
|
|
87
|
+
const doctorScript = path.join(ROOT, 'scripts', 'doctor.mjs')
|
|
88
|
+
const result = spawnSync(process.execPath, [doctorScript, ...args.slice(1)], {
|
|
89
|
+
stdio: 'inherit',
|
|
90
|
+
cwd: ROOT,
|
|
91
|
+
})
|
|
92
|
+
process.exit(result.status ?? 1)
|
|
59
93
|
}
|
|
60
94
|
|
|
61
|
-
if (args.includes('--help')) {
|
|
95
|
+
if (args.includes('--help')) {
|
|
96
|
+
printUsage()
|
|
97
|
+
process.exit(0)
|
|
98
|
+
}
|
|
62
99
|
|
|
63
100
|
if (command === 'set-tier') {
|
|
64
|
-
const isProject = args.includes('--project')
|
|
65
|
-
const isDryRun = args.includes('--dry-run')
|
|
66
|
-
const name = args[1]
|
|
101
|
+
const isProject = args.includes('--project')
|
|
102
|
+
const isDryRun = args.includes('--dry-run')
|
|
103
|
+
const name = args[1]
|
|
67
104
|
|
|
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(', ')}
|
|
105
|
+
const { loadPresetDefs } = await import('../src/pantheon/presets.mjs')
|
|
106
|
+
const { writeActivePreset } = await import('../scripts/install/model-picker.mjs')
|
|
107
|
+
const presets = loadPresetDefs()
|
|
108
|
+
const presetList = `none, ${Object.keys(presets).join(', ')}`
|
|
72
109
|
|
|
73
110
|
if (!name) {
|
|
74
|
-
console.error('❌ set-tier requires a preset name')
|
|
75
|
-
console.error(` Available: ${presetList}`)
|
|
76
|
-
process.exit(1)
|
|
111
|
+
console.error('❌ set-tier requires a preset name')
|
|
112
|
+
console.error(` Available: ${presetList}`)
|
|
113
|
+
process.exit(1)
|
|
77
114
|
}
|
|
78
115
|
if (name !== 'none' && !presets[name]) {
|
|
79
|
-
console.error(`❌ Unknown preset "${name}"`)
|
|
80
|
-
console.error(` Available: ${presetList}`)
|
|
81
|
-
process.exit(1)
|
|
116
|
+
console.error(`❌ Unknown preset "${name}"`)
|
|
117
|
+
console.error(` Available: ${presetList}`)
|
|
118
|
+
process.exit(1)
|
|
82
119
|
}
|
|
83
120
|
|
|
84
121
|
// Fail-fast: every provider's API key env var must be set before writing
|
|
85
122
|
if (name !== 'none') {
|
|
86
123
|
for (const [pid, provider] of Object.entries(presets[name].providers || {})) {
|
|
87
124
|
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)
|
|
125
|
+
console.error(`❌ Missing required API key configuration for provider "${pid}"`)
|
|
126
|
+
console.error(' Set the provider API key environment variable and retry.')
|
|
127
|
+
process.exit(1)
|
|
91
128
|
}
|
|
92
129
|
}
|
|
93
130
|
}
|
|
94
131
|
|
|
95
132
|
const presetDir = isProject
|
|
96
133
|
? 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')
|
|
134
|
+
: path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'opencode')
|
|
135
|
+
const filePath = path.join(presetDir, '.pantheon', 'active-preset.json')
|
|
136
|
+
const bakPath = path.join(presetDir, '.pantheon', 'active-preset.json.bak')
|
|
100
137
|
|
|
101
|
-
let prevName = '(none)'
|
|
138
|
+
let prevName = '(none)'
|
|
102
139
|
if (fs.existsSync(filePath)) {
|
|
103
140
|
try {
|
|
104
|
-
prevName = JSON.parse(fs.readFileSync(filePath, 'utf8')).preset || '(unknown)'
|
|
105
|
-
} catch {
|
|
141
|
+
prevName = JSON.parse(fs.readFileSync(filePath, 'utf8')).preset || '(unknown)'
|
|
142
|
+
} catch {
|
|
143
|
+
prevName = '(unknown)'
|
|
144
|
+
}
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
if (name === 'none') {
|
|
109
148
|
if (isDryRun) {
|
|
110
|
-
console.log('[dry-run] Would clear model preset')
|
|
149
|
+
console.log('[dry-run] Would clear model preset')
|
|
111
150
|
} else {
|
|
112
151
|
if (fs.existsSync(filePath)) {
|
|
113
|
-
fs.mkdirSync(path.dirname(bakPath), { recursive: true })
|
|
114
|
-
fs.copyFileSync(filePath, bakPath)
|
|
115
|
-
fs.rmSync(filePath, { force: true })
|
|
152
|
+
fs.mkdirSync(path.dirname(bakPath), { recursive: true })
|
|
153
|
+
fs.copyFileSync(filePath, bakPath)
|
|
154
|
+
fs.rmSync(filePath, { force: true })
|
|
116
155
|
}
|
|
117
|
-
console.log('Model preset cleared. Using opencode defaults.')
|
|
118
|
-
console.log(` Backup: ${bakPath}`)
|
|
156
|
+
console.log('Model preset cleared. Using opencode defaults.')
|
|
157
|
+
console.log(` Backup: ${bakPath}`)
|
|
119
158
|
}
|
|
120
|
-
return
|
|
159
|
+
return
|
|
121
160
|
}
|
|
122
161
|
|
|
123
|
-
const result = writeActivePreset(presetDir, name, { dryRun: isDryRun, source: 'cli' })
|
|
162
|
+
const result = writeActivePreset(presetDir, name, { dryRun: isDryRun, source: 'cli' })
|
|
124
163
|
|
|
125
|
-
console.log(`Model preset set: ${prevName} → ${name}`)
|
|
164
|
+
console.log(`Model preset set: ${prevName} → ${name}`)
|
|
126
165
|
for (const [agent, spec] of Object.entries(presets[name].agents || {})) {
|
|
127
|
-
console.log(` ${agent}: ${spec.model} [${spec.reasoning_effort}]`)
|
|
166
|
+
console.log(` ${agent}: ${spec.model} [${spec.reasoning_effort}]`)
|
|
128
167
|
}
|
|
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
|
|
168
|
+
const providerNames = Object.keys(presets[name].providers || {})
|
|
169
|
+
console.log(` Providers: ${providerNames.join(', ') || 'none'}`)
|
|
170
|
+
if (result.backupPath) console.log(` Backup: ${result.backupPath}`)
|
|
171
|
+
return
|
|
133
172
|
}
|
|
134
173
|
|
|
135
174
|
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')
|
|
175
|
+
const isProject = args.includes('--project')
|
|
176
|
+
const isDryRun = args.includes('--dry-run')
|
|
177
|
+
const skipMCP = args.includes('--no-mcp')
|
|
178
|
+
const forceReinstall = args.includes('--force')
|
|
179
|
+
const runDoctor = args.includes('--doctor')
|
|
180
|
+
const forceInteractive = args.includes('--interactive')
|
|
181
|
+
const forceHeadless = args.includes('--headless')
|
|
182
|
+
const autoYes = args.includes('--yes') || args.includes('-y')
|
|
183
|
+
const presetIndex = args.indexOf('--preset')
|
|
184
|
+
const presetOpt = presetIndex >= 0 ? (args[presetIndex + 1] ?? null) : null
|
|
185
|
+
|
|
186
|
+
const components = ['agents', 'skills', 'instructions', 'commands', 'plugins']
|
|
187
|
+
if (!skipMCP) components.push('runtime')
|
|
149
188
|
|
|
150
189
|
// Version info
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const pkgPath = path.join(ROOT, 'package.json');
|
|
154
|
-
version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '?';
|
|
155
|
-
} catch { /* use default */ }
|
|
190
|
+
const version = readVersion()
|
|
156
191
|
|
|
157
|
-
console.log('')
|
|
192
|
+
console.log('')
|
|
158
193
|
if (!forceInteractive || isDryRun) {
|
|
159
|
-
console.log(`Pantheon OpenCode v${version} — ${isDryRun ? 'DRY RUN' : 'Installing...'}`)
|
|
160
|
-
console.log('')
|
|
194
|
+
console.log(`Pantheon OpenCode v${version} — ${isDryRun ? 'DRY RUN' : 'Installing...'}`)
|
|
195
|
+
console.log('')
|
|
161
196
|
}
|
|
162
197
|
|
|
163
198
|
try {
|
|
164
|
-
const { installOpenCode } = await import('../scripts/install/opencode.mjs')
|
|
165
|
-
const target = isProject ? process.cwd() : undefined
|
|
199
|
+
const { installOpenCode } = await import('../scripts/install/opencode.mjs')
|
|
200
|
+
const target = isProject ? process.cwd() : undefined
|
|
166
201
|
await installOpenCode(target, isDryRun, forceReinstall, components, {
|
|
167
202
|
interactive: forceInteractive,
|
|
168
203
|
headless: forceHeadless,
|
|
169
204
|
yes: autoYes,
|
|
170
205
|
preset: presetOpt,
|
|
171
|
-
})
|
|
206
|
+
})
|
|
172
207
|
} catch (err) {
|
|
173
|
-
console.error(
|
|
174
|
-
|
|
208
|
+
console.error(
|
|
209
|
+
`❌ 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`,
|
|
210
|
+
)
|
|
211
|
+
process.exit(1)
|
|
175
212
|
}
|
|
176
213
|
|
|
177
214
|
if (runDoctor && !isDryRun) {
|
|
178
|
-
console.log('')
|
|
179
|
-
console.log(' Running health check...')
|
|
215
|
+
console.log('')
|
|
216
|
+
console.log(' Running health check...')
|
|
180
217
|
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 })
|
|
218
|
+
const { spawnSync } = await import('child_process')
|
|
219
|
+
const doctorScript = path.join(ROOT, 'scripts', 'doctor.mjs')
|
|
220
|
+
spawnSync(process.execPath, [doctorScript], { stdio: 'inherit', cwd: ROOT })
|
|
184
221
|
} catch {
|
|
185
|
-
console.log(' ⚠️ Could not run doctor.mjs')
|
|
222
|
+
console.log(' ⚠️ Could not run doctor.mjs')
|
|
186
223
|
}
|
|
187
224
|
}
|
|
188
225
|
|
|
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
|
|
226
|
+
console.log('')
|
|
227
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
228
|
+
console.log(` ✅ Pantheon OpenCode v${version} installed!`)
|
|
229
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
230
|
+
console.log('')
|
|
231
|
+
console.log(' Next steps:')
|
|
232
|
+
console.log(' 1. Verify installation:')
|
|
233
|
+
console.log(' npx pantheon-opencode doctor')
|
|
234
|
+
console.log(' 2. Launch your AI coding tool (OpenCode, Claude Code, Cursor, etc.)')
|
|
235
|
+
console.log(' 3. Invoke agents with @agent-name in chat')
|
|
236
|
+
console.log(' 4. For project-local install:')
|
|
237
|
+
console.log(' npx pantheon-opencode init --project')
|
|
238
|
+
console.log('')
|
|
239
|
+
|
|
240
|
+
return
|
|
204
241
|
}
|
|
205
242
|
|
|
206
|
-
printUsage()
|
|
207
|
-
process.exit(1)
|
|
243
|
+
printUsage()
|
|
244
|
+
process.exit(1)
|
|
208
245
|
}
|
|
209
246
|
|
|
210
|
-
main().catch(err => {
|
|
247
|
+
main().catch((err) => {
|
|
248
|
+
console.error('Error:', err.stack)
|
|
249
|
+
process.exit(1)
|
|
250
|
+
})
|
package/opencode.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"version:status": "node scripts/versioning.mjs status",
|
|
22
22
|
"version:apply": "node scripts/versioning.mjs apply",
|
|
23
23
|
"version:check": "node scripts/version-check.mjs",
|
|
24
|
-
"release:dry-run": "node scripts/versioning.mjs status && npm pack --dry-run",
|
|
24
|
+
"release:dry-run": "node scripts/versioning.mjs status && node scripts/release-notes.mjs && npm pack --dry-run",
|
|
25
25
|
"publish:next": "npm publish --tag next --access public",
|
|
26
26
|
"lint": "npx biome ci .",
|
|
27
27
|
"typecheck": "npx tsc --noEmit",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"hooks:install": "pre-commit install",
|
|
31
31
|
"hooks:update": "pre-commit autoupdate",
|
|
32
32
|
"test:security": "npm run secret-scan && node tests/test_install_mcp_vision.mjs && node tests/test_doctor_layers.mjs",
|
|
33
|
-
"test:node": "node --test tests/*.mjs",
|
|
33
|
+
"test:node": "node --test tests/*.mjs tests/pantheon/*.mjs",
|
|
34
34
|
"test:ts": "for f in tests/pantheon/*.test.ts; do npx tsx \"$f\" || exit 1; done",
|
|
35
35
|
"test": "python3 -m pytest tests/ -q 2>&1 || echo \"No tests found\"",
|
|
36
36
|
"test:ci": "python3 -m pytest tests/ -q --tb=short 2>&1 || true"
|
|
@@ -64,7 +64,10 @@
|
|
|
64
64
|
"scripts/hooks/**",
|
|
65
65
|
"src/mcp/requirements*",
|
|
66
66
|
"src/plugins/tui/**",
|
|
67
|
-
"src/
|
|
67
|
+
"!src/plugins/tui/node_modules/**",
|
|
68
|
+
"src/skills/**",
|
|
69
|
+
"!**/__pycache__/**",
|
|
70
|
+
"!**/*.pyc"
|
|
68
71
|
],
|
|
69
72
|
"dependencies": {
|
|
70
73
|
"js-yaml": "^4.3.0",
|