pantheon-opencode 1.5.0-beta.4 β 1.5.0-beta.6
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/CHANGELOG.md +61 -0
- package/README.md +44 -1
- package/README.pt-BR.md +42 -0
- package/bin/pantheon-init.mjs +178 -21
- package/package.json +13 -10
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/code_mode_server.py +35 -20
- package/scripts/doctor.mjs +178 -9
- package/scripts/install/cli-ui.mjs +17 -11
- package/scripts/install/config-migration.mjs +4 -6
- package/scripts/install/opencode.mjs +95 -35
- package/scripts/install/shared.mjs +43 -2
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +1 -0
- package/scripts/release-beta-version.mjs +2 -7
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +50 -4
- package/src/mcp/code_mode_server.py +35 -20
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/shared-board.ts +37 -0
- package/src/plugin.ts +6 -8
- package/src/plugins/pantheon-hooks.ts +51 -0
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +40 -8
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +253 -97
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +188 -16
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +188 -16
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,67 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
18
18
|
|
|
19
19
|
## β
Closed Issues
|
|
20
20
|
|
|
21
|
+
## [v1.5.0-beta.6] - 2026-09-12
|
|
22
|
+
|
|
23
|
+
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
|
24
|
+
move this section to a versioned entry and reset the template below. -->
|
|
25
|
+
|
|
26
|
+
## π What's New
|
|
27
|
+
|
|
28
|
+
- **Delegation panel v2:** live tool activity per running delegation
|
|
29
|
+
(`β³ bash npm test` β what the child is doing right now), a paginated
|
|
30
|
+
`Archived (n)` section for terminal reports beyond the recent 8, and a
|
|
31
|
+
distinct `retry` state (β³ marker, RETRYING label) instead of lumping
|
|
32
|
+
retries into running.
|
|
33
|
+
|
|
34
|
+
## π Fixed
|
|
35
|
+
|
|
36
|
+
- **Stale plugin refs from old installs:** the installer now prunes
|
|
37
|
+
`pantheon-opencode` plugin references pointing at other installed copies of
|
|
38
|
+
the package (older nvm prefixes, npx cache hashes). Previously every init
|
|
39
|
+
accumulated refs from past installs and the oldest β without the newest
|
|
40
|
+
fixes β won the plugin load race, silently disabling native-task
|
|
41
|
+
mirroring. Third-party plugins sharing the same file names remain
|
|
42
|
+
protected.
|
|
43
|
+
- **Docs:** `@beta` dist-tag guidance on all npx commands β plain
|
|
44
|
+
`npx pantheon-opencode` resolves the stable release and silently downgrades
|
|
45
|
+
the installed TUI copy.
|
|
46
|
+
## [v1.5.0-beta.5] - 2026-09-12
|
|
47
|
+
|
|
48
|
+
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
|
49
|
+
move this section to a versioned entry and reset the template below. -->
|
|
50
|
+
|
|
51
|
+
## π What's New
|
|
52
|
+
|
|
53
|
+
- **Native background tasks in the delegation panel:** tasks dispatched via
|
|
54
|
+
the native `task(background=true)` tool are now mirrored onto the shared
|
|
55
|
+
BackgroundJobBoard, so the TUI delegation panel tracks them while running
|
|
56
|
+
and the existing finalize path writes their terminal report.
|
|
57
|
+
- **`pantheon-opencode update`:** one command updates the global package
|
|
58
|
+
(beta or stable channel) and re-runs init to refresh config, venv and MCP
|
|
59
|
+
entries. The installer stamps the installed version and `doctor` warns when
|
|
60
|
+
the package is newer than the last sync.
|
|
61
|
+
- **Always-fresh artifacts:** the package postinstall now refreshes all
|
|
62
|
+
copy-only artifacts (agents, skills, AGENTS.md, commands, MCP scripts,
|
|
63
|
+
code-mode payload) into existing installations on every `npm install`; a CI
|
|
64
|
+
gate rebuilds the TUI bundle and fails if the committed `dist/` is stale.
|
|
65
|
+
- **More resilient installer:** preflight checks for python3/npm before any
|
|
66
|
+
file is written, atomic `opencode.json` writes with a `.bak` backup,
|
|
67
|
+
non-fatal Python runtime setup (a venv failure now omits MCP entries with a
|
|
68
|
+
warning instead of pointing at a broken interpreter), `--components` /
|
|
69
|
+
`--clean` / `--opencode-version auto` flags, unknown-flag warnings and
|
|
70
|
+
pt-BR/EN message auto-detection via `LANG`.
|
|
71
|
+
- **Roadmap zerado:** o plano ativo nΓ£o tem mais itens pendentes. Sprints
|
|
72
|
+
nunca implementados dos planos v1.0/v1.0+ foram removidos; o contrato atual
|
|
73
|
+
passou a documentar o code-mode (manifest SHA-256, resoluΓ§Γ£o project-first).
|
|
74
|
+
|
|
75
|
+
## π Fixed
|
|
76
|
+
|
|
77
|
+
- **Code-mode project resolution (#112):** the MCP now resolves project code-mode
|
|
78
|
+
scripts before global scripts, honors `PANTHEON_PROJECT`, and fails closed on
|
|
79
|
+
project manifest errors; the installer sets `cwd: "."`.
|
|
80
|
+
- **Code-mode manifest doctor check (#113):** `doctor` validates manifest shape,
|
|
81
|
+
script membership, and SHA-256 hashes without regenerating the manifest.
|
|
21
82
|
## [v1.5.0-beta.4] - 2026-09-11
|
|
22
83
|
|
|
23
84
|
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ way to plan work, make progress, check results, and keep useful project context.
|
|
|
30
30
|
|
|
31
31
|
## Start in 2 minutes
|
|
32
32
|
|
|
33
|
-
Requirements: [OpenCode 1.18.4+](https://opencode.ai/docs/) and Node.js
|
|
33
|
+
Requirements: [OpenCode 1.18.4+](https://opencode.ai/docs/) and Node.js 22+.
|
|
34
34
|
|
|
35
35
|
From the project where you want to use Pantheon:
|
|
36
36
|
|
|
@@ -127,6 +127,14 @@ contract (`OK`, `UNSUPPORTED`, `UNAVAILABLE`, `INVALID_INPUT`, `INVALID_STATE`,
|
|
|
127
127
|
`CONFLICT`, `CORRUPT_DATA`, `TIMEOUT`, `ESCALATE`); with `json_output=true`,
|
|
128
128
|
results carry a `status` field.
|
|
129
129
|
|
|
130
|
+
Code-mode resolves project-first: `.opencode/.pantheon/code-mode` is preferred,
|
|
131
|
+
then `.pantheon/code-mode`. `PANTHEON_PROJECT` overrides the working directory,
|
|
132
|
+
and the installed MCP uses `cwd: "."` so OpenCode supplies the workspace. A
|
|
133
|
+
global directory is used only when no usable project directory is available;
|
|
134
|
+
once a project directory is selected, a missing or corrupt manifest fails
|
|
135
|
+
closed instead of falling back. `doctor` validates the manifest and every
|
|
136
|
+
script's SHA-256 without regenerating it.
|
|
137
|
+
|
|
130
138
|
|
|
131
139
|
## What's new in 1.5.0-beta.2
|
|
132
140
|
|
|
@@ -205,6 +213,41 @@ The installer still writes the compatibility settings required by the selected
|
|
|
205
213
|
OpenCode host, such as `experimental.subagent_depth`; this does not convert a
|
|
206
214
|
V1 plugin into V2 or provide V2 with V1 hooks.
|
|
207
215
|
|
|
216
|
+
## Updating between releases (beta.5+)
|
|
217
|
+
|
|
218
|
+
One command keeps an existing installation current:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# Without a global install, ALWAYS pin the dist-tag β plain `npx
|
|
222
|
+
# pantheon-opencode` resolves `latest`, which is the stable release (1.4.3):
|
|
223
|
+
npx pantheon-opencode@beta update # npm beta channel + config refresh
|
|
224
|
+
npx pantheon-opencode@beta update --stable # stable channel instead
|
|
225
|
+
|
|
226
|
+
# With a global install, use the global bin (defaults to the beta channel):
|
|
227
|
+
pantheon-opencode update
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`update` compares your installed version with the npm dist-tag, runs
|
|
231
|
+
`npm install -g pantheon-opencode@<channel>`, then re-runs `init --yes
|
|
232
|
+
--headless` so config merges, the venv and MCP entries match the new package.
|
|
233
|
+
Two freshness guarantees back it up:
|
|
234
|
+
|
|
235
|
+
- **Postinstall artifact sync** β after any `npm install`, copy-only artifacts
|
|
236
|
+
(agents, skills, AGENTS.md, commands, MCP scripts, code-mode payload) are
|
|
237
|
+
refreshed into the existing config dir automatically; you never have to
|
|
238
|
+
re-run `init` just for file copies.
|
|
239
|
+
- **Drift detection** β the installer stamps the installed version in
|
|
240
|
+
`.pantheon/install-state.json` and `doctor` warns when the package is newer
|
|
241
|
+
than the last sync, pointing at `update`.
|
|
242
|
+
|
|
243
|
+
`init` also gained `--components agents,skills,...` (narrow install),
|
|
244
|
+
`--clean` (alias of `--force`), `--opencode-version auto`, atomic config
|
|
245
|
+
writes with an `opencode.json.bak` backup, preflight checks for python3/npm
|
|
246
|
+
before any file is written, and a non-fatal Python runtime: if the venv fails,
|
|
247
|
+
the install completes but MCP entries are omitted (with a warning) instead of
|
|
248
|
+
pointing at a broken interpreter. Installer messages auto-detect pt-BR via
|
|
249
|
+
`LANG`/`LC_ALL`.
|
|
250
|
+
|
|
208
251
|
## Releases
|
|
209
252
|
|
|
210
253
|
Publication is authorized **only** by an explicit `workflow_dispatch` of the
|
package/README.pt-BR.md
CHANGED
|
@@ -94,6 +94,14 @@ seguem o contrato de nove cΓ³digos (`OK`, `UNSUPPORTED`, `UNAVAILABLE`,
|
|
|
94
94
|
`INVALID_INPUT`, `INVALID_STATE`, `CONFLICT`, `CORRUPT_DATA`, `TIMEOUT`,
|
|
95
95
|
`ESCALATE`) e, com `json_output=true`, incluem o campo `status`.
|
|
96
96
|
|
|
97
|
+
O code-mode resolve primeiro o projeto: `.opencode/.pantheon/code-mode` vem
|
|
98
|
+
antes de `.pantheon/code-mode`. `PANTHEON_PROJECT` sobrescreve o diretΓ³rio de
|
|
99
|
+
trabalho, e o MCP instalado usa `cwd: "."` para receber o workspace do
|
|
100
|
+
OpenCode. O diretΓ³rio global sΓ³ Γ© usado sem um diretΓ³rio de projeto utilizΓ‘vel;
|
|
101
|
+
depois de selecionar um diretΓ³rio de projeto, manifest ausente ou corrompido
|
|
102
|
+
falha fechado, sem fallback silencioso. O `doctor` valida o manifest e o
|
|
103
|
+
SHA-256 de cada script sem regenerΓ‘-lo.
|
|
104
|
+
|
|
97
105
|
|
|
98
106
|
## Novidades da 1.5.0-beta.2
|
|
99
107
|
|
|
@@ -171,6 +179,40 @@ O instalador continua gravando as configuraΓ§Γ΅es de compatibilidade exigidas
|
|
|
171
179
|
pelo host OpenCode selecionado, como `experimental.subagent_depth`; isso nΓ£o
|
|
172
180
|
converte um plugin V1 em V2 nem dΓ‘ hooks V1 ao V2.
|
|
173
181
|
|
|
182
|
+
## Atualizando entre releases (beta.5+)
|
|
183
|
+
|
|
184
|
+
Um comando mantΓ©m uma instalaΓ§Γ£o existente em dia:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Sem instalaΓ§Γ£o global, SEMPRE fixe o dist-tag β `npx pantheon-opencode`
|
|
188
|
+
# puro resolve a tag `latest`, que Γ© o release estΓ‘vel (1.4.3):
|
|
189
|
+
npx pantheon-opencode@beta update # canal beta do npm + refresh do config
|
|
190
|
+
npx pantheon-opencode@beta update --stable # canal estΓ‘vel
|
|
191
|
+
|
|
192
|
+
# Com instalaΓ§Γ£o global, use o bin global (o padrΓ£o Γ© o canal beta):
|
|
193
|
+
pantheon-opencode update
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
O `update` compara a versΓ£o instalada com o dist-tag do npm, roda
|
|
197
|
+
`npm install -g pantheon-opencode@<canal>` e re-executa `init --yes
|
|
198
|
+
--headless` para alinhar merge de config, venv e entradas de MCP com o novo
|
|
199
|
+
pacote. Duas garantias de freshness sustentam isso:
|
|
200
|
+
|
|
201
|
+
- **Sync de artifacts no postinstall** β depois de qualquer `npm install`, os
|
|
202
|
+
artefatos de cΓ³pia (agents, skills, AGENTS.md, commands, scripts MCP,
|
|
203
|
+
payload code-mode) sΓ£o atualizados automaticamente no config dir existente;
|
|
204
|
+
vocΓͺ nunca precisa re-rodar `init` sΓ³ para atualizar arquivos.
|
|
205
|
+
- **DetecΓ§Γ£o de drift** β o instalador grava a versΓ£o instalada em
|
|
206
|
+
`.pantheon/install-state.json` e o `doctor` avisa quando o pacote Γ© mais
|
|
207
|
+
novo que o ΓΊltimo sync, apontando para o `update`.
|
|
208
|
+
|
|
209
|
+
O `init` tambΓ©m ganhou `--components agents,skills,...` (instalaΓ§Γ£o enxuta),
|
|
210
|
+
`--clean` (alias de `--force`), `--opencode-version auto`, escrita atΓ΄mica do
|
|
211
|
+
config com backup em `opencode.json.bak`, prΓ©-checagens de python3/npm antes
|
|
212
|
+
de escrever qualquer arquivo e runtime Python nΓ£o-fatal: se a venv falha, a
|
|
213
|
+
instalaΓ§Γ£o completa mas as entradas MCP ficam de fora (com aviso) em vez de
|
|
214
|
+
apontar para um interpretador quebrado.
|
|
215
|
+
|
|
174
216
|
## Releases
|
|
175
217
|
|
|
176
218
|
A publicaΓ§Γ£o Γ© autorizada **somente** por um `workflow_dispatch` explΓcito do
|
package/bin/pantheon-init.mjs
CHANGED
|
@@ -22,9 +22,10 @@ import { fileURLToPath } from 'node:url'
|
|
|
22
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
23
23
|
const ROOT = path.resolve(__dirname, '..')
|
|
24
24
|
|
|
25
|
-
const REQUIRED_NODE_MAJOR =
|
|
25
|
+
const REQUIRED_NODE_MAJOR = 22
|
|
26
26
|
if (parseInt(process.versions.node.split('.')[0], 10) < REQUIRED_NODE_MAJOR) {
|
|
27
27
|
console.error(`β Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`)
|
|
28
|
+
console.error(' Install a newer Node via nvm: https://github.com/nvm-sh/nvm')
|
|
28
29
|
process.exit(1)
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -50,9 +51,13 @@ function printUsage() {
|
|
|
50
51
|
console.log(' npx pantheon-opencode init --interactive # Force interactive TUI mode')
|
|
51
52
|
console.log(' npx pantheon-opencode init --headless # Force non-interactive mode')
|
|
52
53
|
console.log(' npx pantheon-opencode init -y # Skip confirmations, use defaults')
|
|
54
|
+
console.log(' npx pantheon-opencode init --components agents,skills,instructions')
|
|
53
55
|
console.log(' npx pantheon-opencode init --opencode-version v1|v2|auto')
|
|
54
56
|
console.log(' npx pantheon-opencode init --preset <name> # Install and activate model preset')
|
|
55
57
|
console.log(' npx pantheon-opencode set-tier <name> # Set active model preset (global)')
|
|
58
|
+
console.log(
|
|
59
|
+
' npx pantheon-opencode update [--stable] # Update package + re-run init (default: beta channel)',
|
|
60
|
+
)
|
|
56
61
|
console.log(
|
|
57
62
|
' npx pantheon-opencode set-tier <name> --project # Set active model preset (project)',
|
|
58
63
|
)
|
|
@@ -112,6 +117,78 @@ async function main() {
|
|
|
112
117
|
process.exit(result.status ?? 1)
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
// beta.5: one-command update β check npm dist-tag, install, re-init.
|
|
121
|
+
// Default channel is `beta` (the prerelease line); `--stable` uses `latest`.
|
|
122
|
+
if (command === 'update') {
|
|
123
|
+
const channel = args.includes('--stable') ? 'latest' : 'beta'
|
|
124
|
+
const current = readVersion()
|
|
125
|
+
const { spawnSync } = await import('node:child_process')
|
|
126
|
+
const _S = (await import('../scripts/install/strings.mjs')).strings()
|
|
127
|
+
|
|
128
|
+
console.log(`Pantheon OpenCode v${current} β checking the "${channel}" channel...`)
|
|
129
|
+
const view = spawnSync('npm', ['view', `pantheon-opencode@${channel}`, 'version'], {
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
timeout: 30_000,
|
|
132
|
+
})
|
|
133
|
+
const latest = (view.stdout || '').trim().split('\n').filter(Boolean).pop()
|
|
134
|
+
if (view.status !== 0 || !latest) {
|
|
135
|
+
console.error(
|
|
136
|
+
`β Could not reach the npm registry: ${(view.stderr || view.stdout || '').trim() || 'no version returned'}`,
|
|
137
|
+
)
|
|
138
|
+
process.exit(1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Prerelease-aware compare: core (major.minor.patch) first, then the
|
|
142
|
+
// beta counter; a stable release outranks any beta of the same core.
|
|
143
|
+
const parsePantheonVersion = (v) => {
|
|
144
|
+
const m = String(v)
|
|
145
|
+
.trim()
|
|
146
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/)
|
|
147
|
+
if (!m) return null
|
|
148
|
+
return {
|
|
149
|
+
core: [+m[1], +m[2], +m[3]],
|
|
150
|
+
beta: m[4] === undefined ? Number.POSITIVE_INFINITY : +m[4],
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const cmp = (a, b) => {
|
|
154
|
+
const pa = parsePantheonVersion(a)
|
|
155
|
+
const pb = parsePantheonVersion(b)
|
|
156
|
+
if (!pa || !pb) return 0
|
|
157
|
+
for (let i = 0; i < 3; i++) {
|
|
158
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
|
|
159
|
+
}
|
|
160
|
+
return pa.beta - pb.beta
|
|
161
|
+
}
|
|
162
|
+
if (cmp(latest, current) <= 0) {
|
|
163
|
+
console.log(`β
Already up to date (installed v${current}; ${channel} channel: v${latest}).`)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`Updating v${current} β v${latest} (channel: ${channel})...`)
|
|
168
|
+
const install = spawnSync('npm', ['install', '-g', `pantheon-opencode@${channel}`], {
|
|
169
|
+
stdio: 'inherit',
|
|
170
|
+
})
|
|
171
|
+
if (install.status !== 0) {
|
|
172
|
+
console.error('β npm install failed β the previous installation is untouched.')
|
|
173
|
+
process.exit(install.status ?? 1)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Refresh config/venv/MCP entries with the NEW package (resolved via
|
|
177
|
+
// PATH β the freshly installed global bin).
|
|
178
|
+
const init = spawnSync('pantheon-opencode', ['init', '--yes', '--headless'], {
|
|
179
|
+
stdio: 'inherit',
|
|
180
|
+
shell: process.platform === 'win32',
|
|
181
|
+
})
|
|
182
|
+
if (init.status !== 0 || init.error) {
|
|
183
|
+
console.error(
|
|
184
|
+
'β οΈ Updated, but the config refresh did not complete β run `npx pantheon-opencode init` manually.',
|
|
185
|
+
)
|
|
186
|
+
process.exit(init.status ?? 1)
|
|
187
|
+
}
|
|
188
|
+
console.log(`β
Updated to v${latest}.`)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
115
192
|
if (args.includes('--help')) {
|
|
116
193
|
printUsage()
|
|
117
194
|
process.exit(0)
|
|
@@ -195,11 +272,49 @@ async function main() {
|
|
|
195
272
|
const isProject = args.includes('--project')
|
|
196
273
|
const isDryRun = args.includes('--dry-run')
|
|
197
274
|
const skipMCP = args.includes('--no-mcp')
|
|
198
|
-
|
|
275
|
+
// --clean is an alias of --force (both wipe component dirs + recreate venv).
|
|
276
|
+
const forceReinstall = args.includes('--force') || args.includes('--clean')
|
|
199
277
|
const runDoctor = args.includes('--doctor')
|
|
200
278
|
const forceInteractive = args.includes('--interactive')
|
|
201
279
|
const forceHeadless = args.includes('--headless')
|
|
202
280
|
const autoYes = args.includes('--yes') || args.includes('-y')
|
|
281
|
+
|
|
282
|
+
// beta.5: warn on unrecognized flags instead of silently ignoring them.
|
|
283
|
+
const VALUE_FLAGS = new Set([
|
|
284
|
+
'--preset',
|
|
285
|
+
'--model',
|
|
286
|
+
'--small-model',
|
|
287
|
+
'--version',
|
|
288
|
+
'--opencode-version',
|
|
289
|
+
'--components',
|
|
290
|
+
])
|
|
291
|
+
for (let i = 1; i < args.length; i++) {
|
|
292
|
+
const arg = args[i]
|
|
293
|
+
if (!arg.startsWith('--') || arg === '--help') continue
|
|
294
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
295
|
+
i++ // skip the flag's value
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
const bare = arg.split('=', 1)[0]
|
|
299
|
+
if (
|
|
300
|
+
!VALUE_FLAGS.has(bare) &&
|
|
301
|
+
![
|
|
302
|
+
'--project',
|
|
303
|
+
'--dry-run',
|
|
304
|
+
'--no-mcp',
|
|
305
|
+
'--force',
|
|
306
|
+
'--clean',
|
|
307
|
+
'--doctor',
|
|
308
|
+
'--interactive',
|
|
309
|
+
'--headless',
|
|
310
|
+
'--yes',
|
|
311
|
+
'-y',
|
|
312
|
+
].includes(bare)
|
|
313
|
+
) {
|
|
314
|
+
console.error(`β οΈ Unknown option ignored: ${arg}`)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
203
318
|
const presetIndex = args.indexOf('--preset')
|
|
204
319
|
const presetOpt = presetIndex >= 0 ? (args[presetIndex + 1] ?? null) : null
|
|
205
320
|
// Explicit top-level model overrides for the generated config.
|
|
@@ -207,8 +322,9 @@ async function main() {
|
|
|
207
322
|
const modelOpt = modelIndex >= 0 ? (args[modelIndex + 1] ?? null) : null
|
|
208
323
|
const smallModelIndex = args.indexOf('--small-model')
|
|
209
324
|
const smallModelOpt = smallModelIndex >= 0 ? (args[smallModelIndex + 1] ?? null) : null
|
|
210
|
-
// --version v1|v2 labels the install target
|
|
211
|
-
//
|
|
325
|
+
// --version v1|v2|auto labels the install target. `auto` resolves the
|
|
326
|
+
// generation from the opencode binary at install time (see
|
|
327
|
+
// scripts/install/opencode-version.mjs). Invalid values fail fast.
|
|
212
328
|
const versionIndex = args.indexOf('--version')
|
|
213
329
|
const legacyVersionIndex = args.indexOf('--opencode-version')
|
|
214
330
|
const inlineVersion = args.find((arg) => arg.startsWith('--opencode-version='))
|
|
@@ -218,20 +334,61 @@ async function main() {
|
|
|
218
334
|
: legacyVersionIndex >= 0
|
|
219
335
|
? (args[legacyVersionIndex + 1] ?? null)
|
|
220
336
|
: (inlineVersion?.split('=', 2)[1] ?? null)
|
|
221
|
-
if (
|
|
222
|
-
|
|
337
|
+
if (
|
|
338
|
+
versionOpt !== null &&
|
|
339
|
+
versionOpt !== 'v1' &&
|
|
340
|
+
versionOpt !== 'v2' &&
|
|
341
|
+
versionOpt !== 'auto'
|
|
342
|
+
) {
|
|
343
|
+
console.error(`β Invalid --version "${versionOpt}" β expected v1, v2 or auto`)
|
|
223
344
|
process.exit(1)
|
|
224
345
|
}
|
|
225
346
|
|
|
226
|
-
|
|
227
|
-
|
|
347
|
+
// --components agents,skills,... narrows the install; --no-mcp drops the
|
|
348
|
+
// runtime component. Unknown component names fail fast (typo protection).
|
|
349
|
+
const KNOWN_COMPONENTS = ['agents', 'skills', 'instructions', 'commands', 'plugins', 'runtime']
|
|
350
|
+
const componentsIndex = args.indexOf('--components')
|
|
351
|
+
const inlineComponents = args.find((arg) => arg.startsWith('--components='))
|
|
352
|
+
const componentsOpt =
|
|
353
|
+
componentsIndex >= 0
|
|
354
|
+
? (args[componentsIndex + 1] ?? null)
|
|
355
|
+
: (inlineComponents?.split('=', 2)[1] ?? null)
|
|
356
|
+
let requestedComponents = null
|
|
357
|
+
if (componentsOpt !== null) {
|
|
358
|
+
requestedComponents = componentsOpt
|
|
359
|
+
.split(',')
|
|
360
|
+
.map((c) => c.trim())
|
|
361
|
+
.filter(Boolean)
|
|
362
|
+
const unknown = requestedComponents.filter((c) => !KNOWN_COMPONENTS.includes(c))
|
|
363
|
+
if (requestedComponents.length === 0 || unknown.length > 0) {
|
|
364
|
+
console.error(
|
|
365
|
+
`β Invalid --components "${componentsOpt}" β expected comma-separated names from: ${KNOWN_COMPONENTS.join(', ')}`,
|
|
366
|
+
)
|
|
367
|
+
process.exit(1)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const components = requestedComponents ?? [
|
|
372
|
+
'agents',
|
|
373
|
+
'skills',
|
|
374
|
+
'instructions',
|
|
375
|
+
'commands',
|
|
376
|
+
'plugins',
|
|
377
|
+
]
|
|
378
|
+
if (!skipMCP && !components.includes('runtime')) components.push('runtime')
|
|
379
|
+
if (skipMCP) {
|
|
380
|
+
const runtimeIndex = components.indexOf('runtime')
|
|
381
|
+
if (runtimeIndex >= 0) components.splice(runtimeIndex, 1)
|
|
382
|
+
}
|
|
228
383
|
|
|
229
384
|
// Version info
|
|
230
385
|
const version = readVersion()
|
|
386
|
+
const { strings } = await import('../scripts/install/strings.mjs')
|
|
387
|
+
const S = strings()
|
|
231
388
|
|
|
232
389
|
console.log('')
|
|
233
390
|
if (!forceInteractive || isDryRun) {
|
|
234
|
-
console.log(`Pantheon OpenCode v${version} β ${isDryRun ?
|
|
391
|
+
console.log(`Pantheon OpenCode v${version} β ${isDryRun ? S.dryRun : S.installing}`)
|
|
235
392
|
console.log('')
|
|
236
393
|
}
|
|
237
394
|
|
|
@@ -248,15 +405,17 @@ async function main() {
|
|
|
248
405
|
version: versionOpt ?? 'v1',
|
|
249
406
|
})
|
|
250
407
|
} catch (err) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
408
|
+
if (err?.message === 'Canceled') {
|
|
409
|
+
console.error(S.canceled)
|
|
410
|
+
process.exit(130)
|
|
411
|
+
}
|
|
412
|
+
console.error(`${S.installFailed(err.message)}\n${S.failHintNoMcp}\n${S.failHintForce}`)
|
|
254
413
|
process.exit(1)
|
|
255
414
|
}
|
|
256
415
|
|
|
257
416
|
if (runDoctor && !isDryRun) {
|
|
258
417
|
console.log('')
|
|
259
|
-
console.log(
|
|
418
|
+
console.log(` ${S.runningHealthCheck}`)
|
|
260
419
|
try {
|
|
261
420
|
const { spawnSync } = await import('node:child_process')
|
|
262
421
|
const doctorScript = path.join(ROOT, 'scripts', 'doctor.mjs')
|
|
@@ -268,16 +427,14 @@ async function main() {
|
|
|
268
427
|
|
|
269
428
|
console.log('')
|
|
270
429
|
console.log('ββββββββββββββββββββββββββββββββββββββ')
|
|
271
|
-
console.log(`
|
|
430
|
+
console.log(` ${S.installedTitle(version)}`)
|
|
272
431
|
console.log('ββββββββββββββββββββββββββββββββββββββ')
|
|
273
432
|
console.log('')
|
|
274
|
-
console.log(
|
|
275
|
-
console.log(
|
|
276
|
-
console.log(
|
|
277
|
-
console.log(
|
|
278
|
-
console.log(
|
|
279
|
-
console.log(' 4. For project-local install:')
|
|
280
|
-
console.log(' npx pantheon-opencode init --project')
|
|
433
|
+
console.log(` ${S.nextSteps}`)
|
|
434
|
+
console.log(` ${S.nextVerify}`)
|
|
435
|
+
console.log(` ${S.nextLaunch}`)
|
|
436
|
+
console.log(` ${S.nextAgents}`)
|
|
437
|
+
console.log(` ${S.nextProject}`)
|
|
281
438
|
console.log('')
|
|
282
439
|
|
|
283
440
|
return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.5.0-beta.
|
|
3
|
+
"version": "1.5.0-beta.6",
|
|
4
4
|
"description": "Multi-agent orchestration for OpenCode β 14 specialized agents, TDD, Themis review gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"package:evidence": "node scripts/package-evidence.mjs",
|
|
33
33
|
"package:validate": "node scripts/validate-package.mjs",
|
|
34
34
|
"publish:next": "npm publish --tag next --access public",
|
|
35
|
-
"lint": "
|
|
36
|
-
"typecheck": "
|
|
35
|
+
"lint": "biome ci .",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
37
|
"audit": "npm audit --audit-level=high",
|
|
38
38
|
"secret-scan": "node scripts/secret-scan.mjs",
|
|
39
39
|
"hooks:install": "pre-commit install",
|
|
40
40
|
"hooks:update": "pre-commit autoupdate",
|
|
41
|
-
"test:security": "npm run secret-scan
|
|
41
|
+
"test:security": "npm run secret-scan",
|
|
42
42
|
"test:node": "node --test tests/*.mjs tests/pantheon/*.mjs",
|
|
43
|
-
"test:ts": "for f in tests/pantheon/*.test.ts; do if [ \"$f\" = tests/pantheon/plugin-v2-contract.test.ts ]; then NODE_OPTIONS=--conditions=import
|
|
43
|
+
"test:ts": "for f in tests/pantheon/*.test.ts; do if [ \"$f\" = tests/pantheon/plugin-v2-contract.test.ts ]; then NODE_OPTIONS=--conditions=import tsx \"$f\"; else tsx \"$f\"; fi || exit 1; done",
|
|
44
44
|
"test": "python3 -m pytest tests/ -q 2>&1; status=$?; if [ \"$status\" -eq 5 ]; then echo \"No tests found\"; fi; exit \"$status\"",
|
|
45
45
|
"test:ci": "python3 -m pytest tests/ -q --tb=short 2>&1"
|
|
46
46
|
},
|
|
@@ -88,14 +88,17 @@
|
|
|
88
88
|
"!**/*.pyc"
|
|
89
89
|
],
|
|
90
90
|
"dependencies": {
|
|
91
|
-
"@opencode-ai/plugin": "1.18.
|
|
91
|
+
"@opencode-ai/plugin": "1.18.30",
|
|
92
92
|
"js-yaml": "^4.3.2",
|
|
93
93
|
"zod": "4.1.8"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
|
-
"@
|
|
97
|
-
"@commitlint/
|
|
98
|
-
"
|
|
96
|
+
"@biomejs/biome": "2.5.13",
|
|
97
|
+
"@commitlint/cli": "21.2.2",
|
|
98
|
+
"@commitlint/config-conventional": "21.2.2",
|
|
99
|
+
"husky": "^9.1.7",
|
|
100
|
+
"tsx": "4.23.13",
|
|
101
|
+
"typescript": "6.0.3"
|
|
99
102
|
},
|
|
100
103
|
"repository": {
|
|
101
104
|
"type": "git",
|
|
@@ -116,7 +119,7 @@
|
|
|
116
119
|
"author": "ils15",
|
|
117
120
|
"license": "MIT",
|
|
118
121
|
"engines": {
|
|
119
|
-
"node": ">=
|
|
122
|
+
"node": ">=22"
|
|
120
123
|
},
|
|
121
124
|
"preferGlobal": true
|
|
122
125
|
}
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon",
|
|
3
3
|
"publisher": "ils15",
|
|
4
|
-
"version": "1.5.0-beta.
|
|
4
|
+
"version": "1.5.0-beta.6",
|
|
5
5
|
"displayName": "Pantheon",
|
|
6
6
|
"description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
|
|
7
7
|
"agents": "./src/agents",
|
package/pyproject.toml
CHANGED
|
@@ -157,12 +157,9 @@ def _detect_prlimit() -> str | None:
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
# ββ Scripts Directory Resolution βββββββββββββββββββββββββββββββββββββββββββββ
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
# 3. /.pantheon/code-mode/ (global fallback)
|
|
164
|
-
# 4. .pantheon/code-mode/ shipped inside the installed package (tarball
|
|
165
|
-
# fallback β package.json `files` includes .pantheon/code-mode/**)
|
|
160
|
+
# Project directories are selected before global directories. Once a usable
|
|
161
|
+
# project directory is selected, manifest errors are deliberately fail-closed;
|
|
162
|
+
# execution must never silently cross the project/global trust boundary.
|
|
166
163
|
|
|
167
164
|
|
|
168
165
|
def _is_within(path: Path, root: Path) -> bool:
|
|
@@ -229,25 +226,38 @@ def _packaged_scripts_dir() -> Path | None:
|
|
|
229
226
|
return None
|
|
230
227
|
|
|
231
228
|
|
|
232
|
-
def _resolve_scripts_dir(candidates: list[Path]) -> Path | None:
|
|
233
|
-
"""Select the first usable candidate,
|
|
229
|
+
def _resolve_scripts_dir(candidates: list[Path], *, allow_packaged: bool = True) -> Path | None:
|
|
230
|
+
"""Select the first usable candidate, optionally trying the package copy."""
|
|
234
231
|
for candidate in candidates:
|
|
235
232
|
if _has_usable_scripts(candidate):
|
|
236
233
|
return candidate
|
|
237
|
-
return _packaged_scripts_dir()
|
|
234
|
+
return _packaged_scripts_dir() if allow_packaged else None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _resolve_code_mode_dir() -> Path:
|
|
238
|
+
"""Resolve code-mode scripts project-first, with a global-only fallback.
|
|
239
|
+
|
|
240
|
+
A project directory is a trust boundary: if one is selected, callers use
|
|
241
|
+
its manifest even when that manifest is missing or corrupt. Global users
|
|
242
|
+
retain the packaged/global fallback when no usable project directory is
|
|
243
|
+
available.
|
|
244
|
+
"""
|
|
245
|
+
project = pantheon_project()
|
|
246
|
+
if project is not None:
|
|
247
|
+
project_candidates = [
|
|
248
|
+
project / ".opencode" / ".pantheon" / "code-mode",
|
|
249
|
+
project / ".pantheon" / "code-mode",
|
|
250
|
+
]
|
|
251
|
+
selected = _resolve_scripts_dir(project_candidates, allow_packaged=False)
|
|
252
|
+
if selected is not None:
|
|
253
|
+
return selected
|
|
254
|
+
|
|
255
|
+
global_dir = pantheon_home() / ".pantheon" / "code-mode"
|
|
256
|
+
return _resolve_scripts_dir([global_dir]) or global_dir
|
|
238
257
|
|
|
239
258
|
|
|
240
259
|
_PANTHEON_HOME: Path = pantheon_home()
|
|
241
|
-
|
|
242
|
-
_proj = pantheon_project()
|
|
243
|
-
if _proj is not None:
|
|
244
|
-
_SCRIPTS_DIR_CANDIDATES.append(_proj / ".opencode" / ".pantheon" / "code-mode")
|
|
245
|
-
_SCRIPTS_DIR_CANDIDATES.append(_proj / ".pantheon" / "code-mode")
|
|
246
|
-
_SCRIPTS_DIR_CANDIDATES.append(_PANTHEON_HOME / ".pantheon" / "code-mode")
|
|
247
|
-
|
|
248
|
-
SCRIPTS_DIR: Path = _resolve_scripts_dir(_SCRIPTS_DIR_CANDIDATES) or (
|
|
249
|
-
_PANTHEON_HOME / ".pantheon" / "code-mode"
|
|
250
|
-
)
|
|
260
|
+
SCRIPTS_DIR: Path = _resolve_code_mode_dir()
|
|
251
261
|
|
|
252
262
|
# ββ FastMCP App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
253
263
|
mcp = FastMCP(
|
|
@@ -371,6 +381,11 @@ def _load_manifest(scripts_dir: Path | None = None) -> dict[str, str]:
|
|
|
371
381
|
"CORRUPT_DATA", "Code-mode manifest must be a JSON object."
|
|
372
382
|
)
|
|
373
383
|
|
|
384
|
+
if data.get("version") != 1:
|
|
385
|
+
raise ManifestError(
|
|
386
|
+
"CORRUPT_DATA", "Code-mode manifest malformed: version must be 1."
|
|
387
|
+
)
|
|
388
|
+
|
|
374
389
|
scripts = data.get("scripts")
|
|
375
390
|
if not isinstance(scripts, dict):
|
|
376
391
|
raise ManifestError(
|
|
@@ -384,6 +399,7 @@ def _load_manifest(scripts_dir: Path | None = None) -> dict[str, str]:
|
|
|
384
399
|
not isinstance(name, str)
|
|
385
400
|
or not isinstance(digest, str)
|
|
386
401
|
or len(digest) != _SHA256_HEX_LEN
|
|
402
|
+
or any(char not in "0123456789abcdefABCDEF" for char in digest)
|
|
387
403
|
):
|
|
388
404
|
raise ManifestError(
|
|
389
405
|
"CORRUPT_DATA",
|
|
@@ -877,4 +893,3 @@ def _cli() -> int:
|
|
|
877
893
|
|
|
878
894
|
if __name__ == "__main__":
|
|
879
895
|
raise SystemExit(_cli())
|
|
880
|
-
|