pantheon-opencode 1.5.0-beta.4 β†’ 1.5.0-beta.5

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 CHANGED
@@ -18,6 +18,42 @@ 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.5] - 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
+ - **Native background tasks in the delegation panel:** tasks dispatched via
29
+ the native `task(background=true)` tool are now mirrored onto the shared
30
+ BackgroundJobBoard, so the TUI delegation panel tracks them while running
31
+ and the existing finalize path writes their terminal report.
32
+ - **`pantheon-opencode update`:** one command updates the global package
33
+ (beta or stable channel) and re-runs init to refresh config, venv and MCP
34
+ entries. The installer stamps the installed version and `doctor` warns when
35
+ the package is newer than the last sync.
36
+ - **Always-fresh artifacts:** the package postinstall now refreshes all
37
+ copy-only artifacts (agents, skills, AGENTS.md, commands, MCP scripts,
38
+ code-mode payload) into existing installations on every `npm install`; a CI
39
+ gate rebuilds the TUI bundle and fails if the committed `dist/` is stale.
40
+ - **More resilient installer:** preflight checks for python3/npm before any
41
+ file is written, atomic `opencode.json` writes with a `.bak` backup,
42
+ non-fatal Python runtime setup (a venv failure now omits MCP entries with a
43
+ warning instead of pointing at a broken interpreter), `--components` /
44
+ `--clean` / `--opencode-version auto` flags, unknown-flag warnings and
45
+ pt-BR/EN message auto-detection via `LANG`.
46
+ - **Roadmap zerado:** o plano ativo nΓ£o tem mais itens pendentes. Sprints
47
+ nunca implementados dos planos v1.0/v1.0+ foram removidos; o contrato atual
48
+ passou a documentar o code-mode (manifest SHA-256, resoluΓ§Γ£o project-first).
49
+
50
+ ## 🐞 Fixed
51
+
52
+ - **Code-mode project resolution (#112):** the MCP now resolves project code-mode
53
+ scripts before global scripts, honors `PANTHEON_PROJECT`, and fails closed on
54
+ project manifest errors; the installer sets `cwd: "."`.
55
+ - **Code-mode manifest doctor check (#113):** `doctor` validates manifest shape,
56
+ script membership, and SHA-256 hashes without regenerating the manifest.
21
57
  ## [v1.5.0-beta.4] - 2026-09-11
22
58
 
23
59
  <!-- 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 18+.
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,36 @@ 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
+ npx pantheon-opencode update # npm beta channel + config refresh
222
+ npx pantheon-opencode update --stable # stable channel instead
223
+ ```
224
+
225
+ `update` compares your installed version with the npm dist-tag, runs
226
+ `npm install -g pantheon-opencode@<channel>`, then re-runs `init --yes
227
+ --headless` so config merges, the venv and MCP entries match the new package.
228
+ Two freshness guarantees back it up:
229
+
230
+ - **Postinstall artifact sync** β€” after any `npm install`, copy-only artifacts
231
+ (agents, skills, AGENTS.md, commands, MCP scripts, code-mode payload) are
232
+ refreshed into the existing config dir automatically; you never have to
233
+ re-run `init` just for file copies.
234
+ - **Drift detection** β€” the installer stamps the installed version in
235
+ `.pantheon/install-state.json` and `doctor` warns when the package is newer
236
+ than the last sync, pointing at `update`.
237
+
238
+ `init` also gained `--components agents,skills,...` (narrow install),
239
+ `--clean` (alias of `--force`), `--opencode-version auto`, atomic config
240
+ writes with an `opencode.json.bak` backup, preflight checks for python3/npm
241
+ before any file is written, and a non-fatal Python runtime: if the venv fails,
242
+ the install completes but MCP entries are omitted (with a warning) instead of
243
+ pointing at a broken interpreter. Installer messages auto-detect pt-BR via
244
+ `LANG`/`LC_ALL`.
245
+
208
246
  ## Releases
209
247
 
210
248
  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,35 @@ 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
+ npx pantheon-opencode update # canal beta do npm + refresh do config
188
+ npx pantheon-opencode update --stable # canal estΓ‘vel
189
+ ```
190
+
191
+ O `update` compara a versΓ£o instalada com o dist-tag do npm, roda
192
+ `npm install -g pantheon-opencode@<canal>` e re-executa `init --yes
193
+ --headless` para alinhar merge de config, venv e entradas de MCP com o novo
194
+ pacote. Duas garantias de freshness sustentam isso:
195
+
196
+ - **Sync de artifacts no postinstall** β€” depois de qualquer `npm install`, os
197
+ artefatos de cΓ³pia (agents, skills, AGENTS.md, commands, scripts MCP,
198
+ payload code-mode) sΓ£o atualizados automaticamente no config dir existente;
199
+ vocΓͺ nunca precisa re-rodar `init` sΓ³ para atualizar arquivos.
200
+ - **DetecΓ§Γ£o de drift** β€” o instalador grava a versΓ£o instalada em
201
+ `.pantheon/install-state.json` e o `doctor` avisa quando o pacote Γ© mais
202
+ novo que o ΓΊltimo sync, apontando para o `update`.
203
+
204
+ O `init` tambΓ©m ganhou `--components agents,skills,...` (instalaΓ§Γ£o enxuta),
205
+ `--clean` (alias de `--force`), `--opencode-version auto`, escrita atΓ΄mica do
206
+ config com backup em `opencode.json.bak`, prΓ©-checagens de python3/npm antes
207
+ de escrever qualquer arquivo e runtime Python nΓ£o-fatal: se a venv falha, a
208
+ instalaΓ§Γ£o completa mas as entradas MCP ficam de fora (com aviso) em vez de
209
+ apontar para um interpretador quebrado.
210
+
174
211
  ## Releases
175
212
 
176
213
  A publicaΓ§Γ£o Γ© autorizada **somente** por um `workflow_dispatch` explΓ­cito do
@@ -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 = 18
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
- const forceReinstall = args.includes('--force')
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 (informational; the config is
211
- // shared and V1-shaped under both versions). Invalid values fail fast.
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 (versionOpt !== null && versionOpt !== 'v1' && versionOpt !== 'v2') {
222
- console.error(`❌ Invalid --version "${versionOpt}" β€” expected v1 or v2`)
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
- const components = ['agents', 'skills', 'instructions', 'commands', 'plugins']
227
- if (!skipMCP) components.push('runtime')
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 ? 'DRY RUN' : 'Installing...'}`)
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
- console.error(
252
- `❌ 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`,
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(' Running health check...')
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(` βœ… Pantheon OpenCode v${version} installed!`)
430
+ console.log(` ${S.installedTitle(version)}`)
272
431
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
273
432
  console.log('')
274
- console.log(' Next steps:')
275
- console.log(' 1. Verify installation:')
276
- console.log(' npx pantheon-opencode doctor')
277
- console.log(' 2. Launch OpenCode')
278
- console.log(' 3. Invoke agents with @agent-name in chat')
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.4",
3
+ "version": "1.5.0-beta.5",
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": "npx biome ci .",
36
- "typecheck": "npx tsc --noEmit",
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 && node tests/test_install_mcp_vision.mjs && node tests/test_doctor_layers.mjs",
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 npx tsx \"$f\"; else npx tsx \"$f\"; fi || exit 1; done",
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.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
- "@commitlint/cli": "^19.8.0",
97
- "@commitlint/config-conventional": "^19.8.0",
98
- "husky": "^9.1.7"
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": ">=18"
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",
4
+ "version": "1.5.0-beta.5",
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
4
4
 
5
5
  [project]
6
6
  name = "pantheon"
7
- version = "1.5.0-beta.4"
7
+ version = "1.5.0-beta.5"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []
@@ -157,12 +157,9 @@ def _detect_prlimit() -> str | None:
157
157
 
158
158
 
159
159
  # ── Scripts Directory Resolution ─────────────────────────────────────────────
160
- # Priority:
161
- # 1. /.opencode/.pantheon/code-mode/ (project install)
162
- # 2. /.pantheon/code-mode/ (legacy fallback)
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, then try the packaged fallback."""
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
- _SCRIPTS_DIR_CANDIDATES: list[Path] = []
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
-