pantheon-opencode 1.6.0-beta.4 → 1.6.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/AGENTS.md +24 -5
- package/CHANGELOG.md +27 -0
- package/README.md +31 -11
- package/README.pt-BR.md +15 -6
- package/bin/pantheon-init.mjs +26 -3
- package/package.json +1 -2
- package/plugin.json +1 -1
- package/pyproject.toml +6 -6
- package/scripts/{code_mode_server.py → code_mode.py} +2 -2
- package/scripts/doctor.mjs +151 -1
- package/scripts/install/config-migration.mjs +80 -2
- package/scripts/install/health-check.mjs +12 -17
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +250 -119
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +1 -1
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +1 -1
- package/scripts/probe-context-rehydrate.mjs +2 -2
- package/scripts/sync-artifacts.mjs +5 -5
- package/scripts/test-opencode-v2-sandbox.sh +21 -21
- package/skills-lock.json +1 -1
- package/src/agents/apollo.md +7 -2
- package/src/agents/zeus.md +29 -1
- package/src/instructions/memory-protocol.instructions.md +15 -4
- package/src/instructions/zeus-anti-stall.instructions.md +9 -1
- package/src/mcp/{code_mode_server.py → code_mode.py} +2 -2
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +1 -1
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +1 -1
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +113 -0
- package/src/plugin-v2.ts +149 -53
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/skills/context-compression/SKILL.md +1 -1
- package/opencode.json +0 -58
- /package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +0 -0
package/AGENTS.md
CHANGED
|
@@ -275,8 +275,14 @@ Para otimizar decisoes de delegacao e reduzir gasto de tokens:
|
|
|
275
275
|
2. Se score > 0.85 → reutiliza agente + background_mode do cache
|
|
276
276
|
3. Se score ≤ 0.85 → aplica regras estaticas e memory_store() com:
|
|
277
277
|
- key: deleg:<task_type>
|
|
278
|
-
- value: {agent, background, pattern}
|
|
279
|
-
- metadata: {type: "decision", score: N}
|
|
278
|
+
- value: JSON.stringify({agent, background, pattern})
|
|
279
|
+
- metadata: JSON.stringify({type: "decision", score: N})
|
|
280
|
+
|
|
281
|
+
`value` E `metadata` sao JSON-encoded strings — o runtime exige que o
|
|
282
|
+
chamador serialize os dois. Objeto cru em `metadata` e rejeitado pelo
|
|
283
|
+
`BeforeValidator` do servidor com `metadata must be a JSON object encoded
|
|
284
|
+
as a string (got object). json.dumps it first. Example: metadata='{"type":
|
|
285
|
+
"decision", "score": 0.9}'`.
|
|
280
286
|
|
|
281
287
|
4. **kv_store("deleg:<pattern>", ...)** para padroes recorrentes de delegacao
|
|
282
288
|
5. **kv_get("deleg:<pattern>")** para reusar decisoes ja tomadas
|
|
@@ -305,14 +311,19 @@ memory_store({
|
|
|
305
311
|
precedent_used: false,
|
|
306
312
|
timestamp: "<ISO-8601>"
|
|
307
313
|
},
|
|
308
|
-
metadata: {
|
|
314
|
+
metadata: JSON.stringify({
|
|
309
315
|
type: "council_decision",
|
|
310
316
|
specialist_count: N,
|
|
311
317
|
model_tier_used: "premium|default|fast"
|
|
312
|
-
}
|
|
318
|
+
})
|
|
313
319
|
})
|
|
314
320
|
```
|
|
315
321
|
`value` is JSON-serialized before storing (the MCP `memory_store.value` argument is a string).
|
|
322
|
+
`metadata` works the same way — it is a JSON object encoded as a **string**, so pass
|
|
323
|
+
`JSON.stringify({...})`. A raw object is rejected with `metadata must be a JSON object
|
|
324
|
+
encoded as a string (got object). json.dumps it first. Example: metadata='{"type":
|
|
325
|
+
"decision", "score": 0.9}'` (enforced by a `BeforeValidator` in
|
|
326
|
+
`memory_mcp.py`). **Both `value` and `metadata` must be serialized by the caller.**
|
|
316
327
|
|
|
317
328
|
### Read Path (Precedent Fast-Path)
|
|
318
329
|
Before dispatching a new council, Zeus runs:
|
|
@@ -425,9 +436,17 @@ No file I/O, no checkpoint_session.py — TTL (4h) handles cleanup automatically
|
|
|
425
436
|
Before ANY delegate dispatch, save a checkpoint:
|
|
426
437
|
```
|
|
427
438
|
context_save(slug, "phase:N", json({
|
|
428
|
-
"phase":
|
|
439
|
+
"phase": {"current": N, "total": M, "name": "..."},
|
|
440
|
+
"turn_count": N, "agent": "...", "summary": "..."
|
|
429
441
|
}), session_id=SESSION_ID)
|
|
430
442
|
```
|
|
443
|
+
**`content.phase` must be an OBJECT, not a bare number.** `{"phase": 1}` is rejected with
|
|
444
|
+
`context_save: content.phase must be an object (got number)`. The server reads `current`
|
|
445
|
+
and `total` (non-negative ints) and `name` (string, max 256 **bytes** — the guard
|
|
446
|
+
measures `len(value.encode())`, so multi-byte characters count as more than one). That
|
|
447
|
+
set is not closed: any other key inside `phase` is accepted and ignored, never an
|
|
448
|
+
error. The `"phase:N"` slot KEY above is unrelated to `content.phase` and stays a plain
|
|
449
|
+
string.
|
|
431
450
|
All checkpoints auto-expire after 4h (TTL=14400).
|
|
432
451
|
|
|
433
452
|
### Gatilho de Pré-Compactação (Anti-perda de estado)
|
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,33 @@ 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.6.0-beta.5] - 2026-10-05
|
|
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
|
+
- **O gate de geração do plugin passa a ler a versão do host — e a superfície de ferramentas deixa de desaparecer num host OpenCode 2.x.** O seletor de geração tinha três defaults `'v1'` (`bin/pantheon-init.mjs`, `scripts/install/opencode.mjs` e a assinatura do resolver em `scripts/install/opencode-version.mjs`). Nesse ramo, o installer grava caminhos de arquivo `.ts` na chave singular `plugin`; um host 2.x rejeita caminhos de arquivo ali (`configured plugin path must be a directory`), **descarta** as entradas e, ao fazer isso, **remove** a entrada de diretório V2 que já estava registrada e funcionando. O sintoma era um host aparentemente saudável — a frota MCP subia normalmente — com um único WARN no log e **nenhuma** ferramenta do plugin no catálogo: sem `pantheon_cost`, sem `hashline_edit`, sem `pantheon_model`, sem BackgroundJobBoard e sem hooks. Os três defaults passam a `'auto'`, e `auto` consulta a versão do host. Precedência: flag explícita > `OPENCODE_VERSION` > basename `opencode2` > probe de `--version` > aviso e fallback `v1`. Isto restaura as **3** ferramentas que o contrato V2 registra (`hashline_edit`, `pantheon_cost`, `pantheon_model`); não é paridade com o V1 — ver **Issues Conhecidas**.
|
|
29
|
+
- **Uma instalação na geração V2 e o `doctor` agora declaram a superfície reduzida em vez de deixá-la ser descoberta.** A lista de recursos não suportados no V2 (`getUnsupportedFeatures()`) não tinha nenhum consumidor em produção — só testes a liam. O installer passa a imprimi-la ao lado do aviso de destino V2, e o `doctor` a reporta como um achado próprio, sem alterar o código de saída (uma instalação V2 é uma configuração suportada, não quebrada). A V2 registra 3 ferramentas, não 6, e não tem BackgroundJobBoard; as 3 ferramentas de goal não são registradas na V2 por desenho. O texto agora é único, vindo de `src/pantheon/v2-unsupported.mjs`.
|
|
30
|
+
- **Renomeação dos entrypoints MCP (issue #198).** Os cinco entrypoints Python deixaram de compartilhar o token `server.py`, então um `pkill -f server.py` amplo não derruba mais a frota inteira como efeito colateral. Inclui uma migração em tempo de instalação para que as instalações existentes não fiquem órfãs.
|
|
31
|
+
- **O pacote npm deixou de versionar e distribuir a configuração pessoal de desenvolvimento do OpenCode.** `opencode.json` e `.opencode/` (comandos, plugins e skills) saíram do controle de versão: a config carregava um endpoint MCP privado e overrides de permissão por desenvolvedor, e o harness V1 de comandos/plugins foi substituído por `src/plugin-v2.ts`. O `init` continua gerando a config do projeto do usuário, então instalações não são afetadas; para contribuidores, um `opencode.json` pessoal deve ir em `.git/info/exclude`.
|
|
32
|
+
- **Documentação: `ROADMAP.md` reescrito e um novo documento de fronteira.** O roadmap foi reescrito com as causas-raiz verificadas, e um novo documento — `docs/PERMISSIONS-QUALITY-DELEGATION.md` — mapeia os gates de qualidade, as quatro camadas de permissão e a delegação num só lugar, marcando explicitamente o que ainda não foi verificado.
|
|
33
|
+
|
|
34
|
+
## 🐞 Fixed
|
|
35
|
+
|
|
36
|
+
- **A leitura do banner de versão do host era quadrática.** Um banner longo custava **11,1 s** para ser interpretado, contra o timeout de **5 s** do spawn de `--version`; o timeout guarda o processo, não a leitura, então um host verboso conseguia estourá-lo. A interpretação passou a ser **linear**.
|
|
37
|
+
- **O `readOnlyRegistry` do V2 não era podado no `session.deleted`.** O registro é global ao processo e nunca era limpo quando uma sessão do V2 terminava, então entradas de sessões encerradas acumulavam. O `session.deleted` do V2 passa a podar o registro, como a V1 já fazia.
|
|
38
|
+
|
|
39
|
+
## ⚠️ Known Issues
|
|
40
|
+
|
|
41
|
+
- **Ação necessária: re-executar o `init` numa instalação afetada.** O `postinstall` **não** roda o `init`. Quem teve a configuração corrompida por uma instalação `v1` anterior **não** é auto-corrigido apenas por atualizar o pacote — é preciso rodar o installer de novo (`npx pantheon-opencode init`). Uma escolha correta de geração **auto-corrige** a configuração (o ramo V2 já remove as referências `.ts` do V1), mas só quando o installer roda. O mesmo vale para a renomeação dos entrypoints: instalações existentes continuam funcionando porque os arquivos antigos são mantidos, mas os caminhos só são corrigidos numa nova execução do installer; até lá, esses usuários permanecem nos entrypoints antigos e sujeitos ao efeito colateral do `server.py`.
|
|
42
|
+
- **A superfície V2 é menor que a V1 por desenho.** A V2 registra 3 ferramentas (`hashline_edit`, `pantheon_cost`, `pantheon_model`), não 6, e não tem BackgroundJobBoard; as 3 `pantheon_goal_*` são V1-only e a matriz de delegação caller→target não é replicada. O installer e o `doctor` agora **dizem** isso (ver What's New), mas a funcionalidade ausente não é restaurada.
|
|
43
|
+
- **O fallback continua a ser `v1`.** Quando o probe do host falha, não consegue ser interpretado, ou traz versões contraditórias, o resolver avisa **uma vez** e cai para `v1`, em vez de arriscar um host 1.x com uma entrada `plugins` que ele ignoraria. É um erro alto e visível, mas ainda é uma queda para a configuração V1.
|
|
44
|
+
|
|
45
|
+
## ✅ Closed Issues
|
|
46
|
+
|
|
47
|
+
- **A justificativa da matriz de delegação V1→V2 foi corrigida.** O texto dizia que o V2 "não expõe esse caminho de semeadura"; os tipos do SDK refutam isso. A causa real da matriz não replicada não é a ausência de um caminho de semeadura (`session.created` do V2 traz `properties.info: Session` com `parentID?`, o mesmo campo que a V1 semeia), mas a *segunda* fonte da V1 — o seed de `client.session.list()` para sessões anteriores ao carregamento do plugin — porque o `PluginContext` do V2 não expõe `client`. Se um host 2.0.22 ao vivo popula `info.parentID` nesse evento continua **não verificado** (nenhum canário lê payload de evento). A linha publicada em `v1.6.0-beta.4` fica como registro.
|
|
21
48
|
## [v1.6.0-beta.4] - 2026-10-03
|
|
22
49
|
|
|
23
50
|
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
package/README.md
CHANGED
|
@@ -188,12 +188,13 @@ per installation; V1 and V2 Pantheon plugins must never be registered together.
|
|
|
188
188
|
|---|---|---|
|
|
189
189
|
| OpenCode config key | singular `plugin` | plural `plugins` |
|
|
190
190
|
| Pantheon registration | `src/plugin.ts` plus `src/plugins/pantheon-hooks.ts` | `<installed>/src/plugin-v2` directory (`index.ts` re-exports `src/plugin-v2.ts`) |
|
|
191
|
-
| Runtime contract | Pantheon V1 plugin: 6 tools (`hashline_edit`, the 3 goal tools, `pantheon_cost`, `pantheon_model`), event/tool hooks and V1 compaction handling | Full V2 plugin: 3 tools (`hashline_edit`, `pantheon_cost`, `pantheon_model`),
|
|
191
|
+
| Runtime contract | Pantheon V1 plugin: 6 tools (`hashline_edit`, the 3 goal tools, `pantheon_cost`, `pantheon_model`), event/tool hooks and V1 compaction handling | Full V2 plugin: 3 tools (`hashline_edit`, `pantheon_cost`, `pantheon_model`), 5 event subscriptions, session hooks (`prompt`, `context`), a read-only-enforcing tool `execute.before` hook, plus configuration transforms |
|
|
192
192
|
| V1 APIs | Registered | Own tool definitions via `ctx.tool.transform()` — not the V1 plugin path |
|
|
193
193
|
|
|
194
194
|
The V2 plugin provides 3 orchestration tools (`hashline_edit`, `pantheon_cost`,
|
|
195
|
-
`pantheon_model`),
|
|
196
|
-
`session.error`, `session.compacted`), session hooks
|
|
195
|
+
`pantheon_model`), 5 event subscriptions (`session.created`, `session.idle`,
|
|
196
|
+
`session.deleted`, `session.error`, `session.compacted`), session hooks
|
|
197
|
+
(`prompt`, `context`),
|
|
197
198
|
and a tool `execute.before` hook that enforces read-only sessions. The 3 goal
|
|
198
199
|
tools are **not**
|
|
199
200
|
registered on V2: the goal loop needs a `GoalStore`, a `GoalLoopClient` and a
|
|
@@ -236,13 +237,22 @@ npx pantheon-opencode init --opencode-version v2
|
|
|
236
237
|
npx pantheon-opencode init --opencode-version auto
|
|
237
238
|
```
|
|
238
239
|
|
|
239
|
-
`--version v1|v2|auto` is accepted as the older
|
|
240
|
-
after `init`. `auto`
|
|
241
|
-
`OPENCODE_VERSION=v1|v2` wins; otherwise an
|
|
242
|
-
`
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
`auto` is the default. `--version v1|v2|auto` is accepted as the older
|
|
241
|
+
selector spelling when used after `init`. `auto` resolves the generation from
|
|
242
|
+
the host in this order: an explicit `OPENCODE_VERSION=v1|v2` wins; otherwise an
|
|
243
|
+
`OPENCODE_BIN` path ending in `opencode2` selects V2; otherwise the host
|
|
244
|
+
binary is asked for its `--version` and a major of 2 or more selects V2. Every
|
|
245
|
+
other case — an unreadable probe, an unparseable banner, or a banner whose
|
|
246
|
+
version-like tokens contradict each other with no tool name to break the tie —
|
|
247
|
+
warns once and falls back to V1, because a plural `plugins` directory entry on
|
|
248
|
+
an unknown 1.x host loses the plugin entirely.
|
|
249
|
+
|
|
250
|
+
The probe prefers the token that follows the tool name, so a runtime token
|
|
251
|
+
ahead of it (`node v22.1.0 (opencode 1.18.33)`) or a trailing build date
|
|
252
|
+
(`opencode v1.18.33 built 2026.10.04`) cannot flip the generation. The
|
|
253
|
+
installer removes Pantheon references from both config shapes before writing
|
|
254
|
+
only the selected Pantheon registration. Third-party entries are not converted
|
|
255
|
+
or claimed by this rule.
|
|
246
256
|
|
|
247
257
|
The `pantheon_cost` report resolves its database by PATH, in this order: an
|
|
248
258
|
explicit `dbPath` supplied by the tool caller, then
|
|
@@ -450,7 +460,7 @@ in the sandbox root) · `2` usage error · `3` sandbox not prepared.
|
|
|
450
460
|
|
|
451
461
|
### Intentional memory MCP divergence
|
|
452
462
|
|
|
453
|
-
`scripts/
|
|
463
|
+
`scripts/memory_mcp.py` and `src/mcp/memory_mcp.py` are
|
|
454
464
|
intentionally different. The standalone `scripts/` copy keeps the lightweight
|
|
455
465
|
`memory_*` contract; the installed `src/mcp/` copy additionally exposes the
|
|
456
466
|
optional codemap schema and `code_index`, `code_query`, and `code_neighbors`.
|
|
@@ -499,6 +509,16 @@ Ideas, bug reports, documentation improvements, and code contributions are
|
|
|
499
509
|
welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening an issue
|
|
500
510
|
or pull request.
|
|
501
511
|
|
|
512
|
+
### Project-level `opencode.json`
|
|
513
|
+
|
|
514
|
+
The published package carries no `opencode.json` template. `init` still writes
|
|
515
|
+
your project's `opencode.json` itself, and the few product defaults an install
|
|
516
|
+
seeds — `default_agent: zeus` and `permission.skill` — come from the installer
|
|
517
|
+
code, not from a packaged file. Contributors who keep a project-level
|
|
518
|
+
`opencode.json` for their own OpenCode sessions should leave it untracked —
|
|
519
|
+
`.git/info/exclude` per clone, or a shared `.gitignore` decision — rather than
|
|
520
|
+
versioning it.
|
|
521
|
+
|
|
502
522
|
## Citation and DOI
|
|
503
523
|
|
|
504
524
|
Pantheon is released under the [MIT License](LICENSE). Cite the
|
package/README.pt-BR.md
CHANGED
|
@@ -171,11 +171,20 @@ npx pantheon-opencode init --opencode-version v2
|
|
|
171
171
|
npx pantheon-opencode init --opencode-version auto
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
-
`--version v1|v2|auto` é aceito como a grafia antiga do seletor
|
|
175
|
-
depois de `init`. `auto`
|
|
176
|
-
|
|
177
|
-
terminando em `opencode2` seleciona V2;
|
|
178
|
-
|
|
174
|
+
`auto` é o padrão. `--version v1|v2|auto` é aceito como a grafia antiga do seletor
|
|
175
|
+
quando usado depois de `init`. `auto` resolve a geração a partir do host nesta
|
|
176
|
+
ordem: um `OPENCODE_VERSION=v1|v2` explícito vence; caso contrário, um
|
|
177
|
+
`OPENCODE_BIN` terminando em `opencode2` seleciona V2; caso contrário o binário
|
|
178
|
+
do host é consultado com `--version` e uma major >= 2 seleciona V2. Qualquer
|
|
179
|
+
outro caso — uma sonda ilegível, um banner não interpretável, ou um banner
|
|
180
|
+
cujos tokens de versão se contradizem sem um nome de ferramenta que desfaça o
|
|
181
|
+
empate — avisa uma vez e volta para V1, porque uma entrada de diretório em
|
|
182
|
+
`plugins` plural em um host 1.x desconhecido perde o plugin inteiro.
|
|
183
|
+
|
|
184
|
+
A sonda prefere o token que vem logo após o nome da ferramenta, então um token
|
|
185
|
+
de runtime antes dele (`node v22.1.0 (opencode 1.18.33)`) ou uma data de build
|
|
186
|
+
no final (`opencode v1.18.33 built 2026.10.04`) não conseguem virar a geração.
|
|
187
|
+
O instalador remove referências Pantheon das duas formas de config antes de
|
|
179
188
|
gravar apenas o registro Pantheon selecionado. Entradas de terceiros não são
|
|
180
189
|
convertidas nem reivindicadas por essa regra.
|
|
181
190
|
|
|
@@ -344,7 +353,7 @@ preparado.
|
|
|
344
353
|
|
|
345
354
|
### Divergência intencional do memory MCP
|
|
346
355
|
|
|
347
|
-
`scripts/
|
|
356
|
+
`scripts/memory_mcp.py` e `src/mcp/memory_mcp.py` são
|
|
348
357
|
intencionalmente diferentes. A cópia independente em `scripts/` mantém o
|
|
349
358
|
contrato leve de `memory_*`; a cópia instalada em `src/mcp/` também expõe o
|
|
350
359
|
schema opcional de codemap e `code_index`, `code_query` e `code_neighbors`. As
|
package/bin/pantheon-init.mjs
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* installOpenCode() from scripts/install/opencode.mjs.
|
|
9
9
|
*
|
|
10
10
|
* Dual-version install (Phase 3): the generated config is V1-shaped and valid
|
|
11
|
-
* under BOTH OpenCode V1 (`opencode` 1.18.x) and V2 (`opencode2
|
|
12
|
-
*
|
|
11
|
+
* under BOTH OpenCode V1 (`opencode` 1.18.x) and V2 (`opencode2`, OpenCode 2.x —
|
|
12
|
+
* `opencode2 --version` prints "opencode v2.0.22"; an early 2.x prerelease
|
|
13
|
+
* printed "0.0.0-next-17444", which is history and not a host observation).
|
|
14
|
+
* V2 reads the same config locations and normalizes V1
|
|
13
15
|
* fields in memory — no native V2 conversion. `--version v2` labels the
|
|
14
16
|
* install for the operator; state isolation is a runtime concern
|
|
15
17
|
* (OPENCODE_DB ~/.local/share/opencode/opencode-v2.db, service port 49375).
|
|
@@ -54,6 +56,9 @@ function printUsage() {
|
|
|
54
56
|
console.log(' npx pantheon-opencode init --components agents,skills,instructions')
|
|
55
57
|
console.log(' npx pantheon-opencode init --opencode-version v1|v2|auto')
|
|
56
58
|
console.log(' npx pantheon-opencode init --preset <name> # Install and activate model preset')
|
|
59
|
+
console.log(
|
|
60
|
+
' npx pantheon-opencode init --merge-dev-config <path> # Merge a dev config (off by default)',
|
|
61
|
+
)
|
|
57
62
|
console.log(' npx pantheon-opencode set-tier <name> # Set active model preset (global)')
|
|
58
63
|
console.log(
|
|
59
64
|
' npx pantheon-opencode update [--stable] # Update package + re-run init (default: beta channel)',
|
|
@@ -287,6 +292,7 @@ async function main() {
|
|
|
287
292
|
'--version',
|
|
288
293
|
'--opencode-version',
|
|
289
294
|
'--components',
|
|
295
|
+
'--merge-dev-config',
|
|
290
296
|
])
|
|
291
297
|
for (let i = 1; i < args.length; i++) {
|
|
292
298
|
const arg = args[i]
|
|
@@ -368,6 +374,22 @@ async function main() {
|
|
|
368
374
|
}
|
|
369
375
|
}
|
|
370
376
|
|
|
377
|
+
// --merge-dev-config <path> opts IN to merging a contributor's personal
|
|
378
|
+
// OpenCode config into the generated one. Only third-party plugin/provider/
|
|
379
|
+
// compaction entries are merged; Pantheon-owned entries are always stripped.
|
|
380
|
+
// Absent this flag no repository-root config is read (product defaults come
|
|
381
|
+
// from the installer code, never from a dev checkout).
|
|
382
|
+
const mergeDevConfigIndex = args.indexOf('--merge-dev-config')
|
|
383
|
+
const inlineMergeDevConfig = args.find((arg) => arg.startsWith('--merge-dev-config='))
|
|
384
|
+
const mergeDevConfigOpt =
|
|
385
|
+
mergeDevConfigIndex >= 0
|
|
386
|
+
? (args[mergeDevConfigIndex + 1] ?? null)
|
|
387
|
+
: (inlineMergeDevConfig?.split('=', 2)[1] ?? null)
|
|
388
|
+
if (mergeDevConfigIndex >= 0 && mergeDevConfigOpt === null) {
|
|
389
|
+
console.error('❌ --merge-dev-config requires a file path')
|
|
390
|
+
process.exit(1)
|
|
391
|
+
}
|
|
392
|
+
|
|
371
393
|
const components = requestedComponents ?? [
|
|
372
394
|
'agents',
|
|
373
395
|
'skills',
|
|
@@ -402,7 +424,8 @@ async function main() {
|
|
|
402
424
|
model: modelOpt,
|
|
403
425
|
smallModel: smallModelOpt,
|
|
404
426
|
preset: presetOpt,
|
|
405
|
-
version: versionOpt ?? '
|
|
427
|
+
version: versionOpt ?? 'auto',
|
|
428
|
+
mergeDevConfig: mergeDevConfigOpt,
|
|
406
429
|
})
|
|
407
430
|
} catch (err) {
|
|
408
431
|
if (err?.message === 'Canceled') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.6.0-beta.
|
|
3
|
+
"version": "1.6.0-beta.5",
|
|
4
4
|
"description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -76,7 +76,6 @@
|
|
|
76
76
|
"bin/*.mjs",
|
|
77
77
|
"AGENTS.md",
|
|
78
78
|
"CHANGELOG.md",
|
|
79
|
-
"opencode.json",
|
|
80
79
|
"src/mcp/*.py",
|
|
81
80
|
"src/instructions/*.instructions.md",
|
|
82
81
|
"src/mcp/*.mjs",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon",
|
|
3
3
|
"publisher": "ils15",
|
|
4
|
-
"version": "1.6.0-beta.
|
|
4
|
+
"version": "1.6.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.6.0-beta.
|
|
7
|
+
version = "1.6.0-beta.5"
|
|
8
8
|
description = "Multi-agent orchestration framework"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = []
|
|
@@ -39,11 +39,11 @@ select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "PL", "RUF", "C901"]
|
|
|
39
39
|
"tests/test_memory_mcp_server.py" = ["PLC0415", "PLR2004"]
|
|
40
40
|
# Existing public APIs and test fixtures are intentionally broad/simple; refactoring
|
|
41
41
|
# them would be behavior-affecting rather than a mechanical lint correction.
|
|
42
|
-
"scripts/
|
|
43
|
-
"src/mcp/
|
|
44
|
-
"scripts/
|
|
45
|
-
"src/mcp/
|
|
46
|
-
"scripts/
|
|
42
|
+
"scripts/code_mode.py" = ["PLR0913", "PLR0917"]
|
|
43
|
+
"src/mcp/code_mode.py" = ["PLR0913", "PLR0917"]
|
|
44
|
+
"scripts/mcp_persistence.py" = ["PLR0913", "PLR0917"]
|
|
45
|
+
"src/mcp/mcp_persistence.py" = ["PLR0913", "PLR0917"]
|
|
46
|
+
"scripts/memory_mcp.py" = ["C901"]
|
|
47
47
|
"src/mcp/eval_store.py" = ["PLR2004"]
|
|
48
48
|
"tests/test_code_mode_frontmatter.py" = ["PLR2004"]
|
|
49
49
|
"tests/test_eval.py" = ["PLR2004"]
|
|
@@ -5,12 +5,12 @@ Provides a confined execution environment for orchestration scripts
|
|
|
5
5
|
via MCP tools and resources.
|
|
6
6
|
|
|
7
7
|
Usage:
|
|
8
|
-
python scripts/
|
|
8
|
+
python scripts/code_mode.py
|
|
9
9
|
|
|
10
10
|
Or via MCP client (stdio transport):
|
|
11
11
|
pantheon-code-mode:
|
|
12
12
|
command: python
|
|
13
|
-
args: ["scripts/
|
|
13
|
+
args: ["scripts/code_mode.py"]
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
package/scripts/doctor.mjs
CHANGED
|
@@ -29,6 +29,8 @@ import { homedir } from 'node:os'
|
|
|
29
29
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
30
30
|
import { fileURLToPath } from 'node:url'
|
|
31
31
|
|
|
32
|
+
import { listV2UnsupportedFeatures } from '../src/pantheon/v2-unsupported.mjs'
|
|
33
|
+
|
|
32
34
|
// ---------------------------------------------------------------------------
|
|
33
35
|
// Paths
|
|
34
36
|
// ---------------------------------------------------------------------------
|
|
@@ -1000,7 +1002,7 @@ function checkVenvLayer(args) {
|
|
|
1000
1002
|
/**
|
|
1001
1003
|
* Return whether *dir* contains an executable code-mode target.
|
|
1002
1004
|
*
|
|
1003
|
-
* Mirrors `_has_usable_scripts` in `scripts/
|
|
1005
|
+
* Mirrors `_has_usable_scripts` in `scripts/code_mode.py`: a directory
|
|
1004
1006
|
* is only a valid resolver candidate when it is a real (non-symlink) directory
|
|
1005
1007
|
* containing a regular `.py`/`.sh` direct child. This prevents an empty project
|
|
1006
1008
|
* overlay from masking a lower-priority installation that actually ships
|
|
@@ -1430,6 +1432,153 @@ export function checkPluginVersionDrift(args) {
|
|
|
1430
1432
|
return 'sync'
|
|
1431
1433
|
}
|
|
1432
1434
|
|
|
1435
|
+
// ---------------------------------------------------------------------------
|
|
1436
|
+
// Check H4: V2 feature reduction (the surface a migrating user loses)
|
|
1437
|
+
// ---------------------------------------------------------------------------
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Whether an opencode.json registers the V2 generation of the Pantheon plugin.
|
|
1441
|
+
*
|
|
1442
|
+
* The V2 contract is the PLURAL `plugins` key; V1 is the singular `plugin` key.
|
|
1443
|
+
* So the key alone is not the test — a config can legitimately carry a
|
|
1444
|
+
* third-party entry in `plugins`. What identifies the generation is a Pantheon
|
|
1445
|
+
* V2 entry inside it. Matched on the `plugin-v2` marker so all three real
|
|
1446
|
+
* spellings are recognised: the canonical DIRECTORY (`src/plugin-v2`), an
|
|
1447
|
+
* absolute path to it, and the package export (`pantheon-opencode/plugin-v2`).
|
|
1448
|
+
*
|
|
1449
|
+
* @param {unknown} ref
|
|
1450
|
+
* @returns {boolean}
|
|
1451
|
+
*/
|
|
1452
|
+
function isV2PluginRef(ref) {
|
|
1453
|
+
const candidate =
|
|
1454
|
+
typeof ref === 'string'
|
|
1455
|
+
? ref
|
|
1456
|
+
: ref && typeof ref === 'object'
|
|
1457
|
+
? (ref.path ?? ref.source ?? ref.id ?? ref.name)
|
|
1458
|
+
: undefined
|
|
1459
|
+
return typeof candidate === 'string' && candidate.includes('plugin-v2')
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Whether any reachable config registers the V2 generation of the Pantheon
|
|
1464
|
+
* plugin. Split out of {@link collectV2UnsupportedFeatures} because "is the V2
|
|
1465
|
+
* generation registered" and "does the seed list anything" are TWO facts: the
|
|
1466
|
+
* first is about the user's config, the second about this repository's seed. A
|
|
1467
|
+
* single `features.length === 0` cannot tell them apart, and conflating them
|
|
1468
|
+
* let an emptied seed print "no V2 generation registered — nothing reduced".
|
|
1469
|
+
*
|
|
1470
|
+
* @param {{path:string, data:object}[]} configs
|
|
1471
|
+
* @returns {boolean}
|
|
1472
|
+
*/
|
|
1473
|
+
export function isV2GenerationRegistered(configs) {
|
|
1474
|
+
return configs.some((cfg) => {
|
|
1475
|
+
const list = cfg.data?.plugins
|
|
1476
|
+
return Array.isArray(list) && list.some(isV2PluginRef)
|
|
1477
|
+
})
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* The V2 feature-reduction markers to report for a set of collected configs.
|
|
1482
|
+
*
|
|
1483
|
+
* Returns the shared seed verbatim when ANY reachable config registers the V2
|
|
1484
|
+
* generation, and an empty list otherwise. The strings come from
|
|
1485
|
+
* `src/pantheon/v2-unsupported.mjs` — the same seed `getUnsupportedFeatures()`
|
|
1486
|
+
* is built from — so doctor cannot report a reduction the plugin does not, or
|
|
1487
|
+
* miss one it does. Nothing is reworded here.
|
|
1488
|
+
*
|
|
1489
|
+
* An empty result is ambiguous on its own and deliberately not interpreted
|
|
1490
|
+
* here: ask {@link isV2GenerationRegistered} for the registration fact, or
|
|
1491
|
+
* {@link describeV2Reduction} to get both at once.
|
|
1492
|
+
*
|
|
1493
|
+
* @param {{path:string, data:object}[]} configs
|
|
1494
|
+
* @returns {string[]}
|
|
1495
|
+
*/
|
|
1496
|
+
export function collectV2UnsupportedFeatures(configs) {
|
|
1497
|
+
return isV2GenerationRegistered(configs) ? listV2UnsupportedFeatures() : []
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Decide what H4 prints, from the two distinct facts rather than from one list
|
|
1502
|
+
* length.
|
|
1503
|
+
*
|
|
1504
|
+
* Three cases, and the middle one used to be silently reported as the first:
|
|
1505
|
+
*
|
|
1506
|
+
* - not registered → info. A V1 install is a supported configuration with
|
|
1507
|
+
* nothing to reduce, and saying so plainly is correct.
|
|
1508
|
+
* - registered, seed EMPTY → warn. This is a packaging fault in THIS repo,
|
|
1509
|
+
* not an absence of reduction: the V2 surface really is reduced and the
|
|
1510
|
+
* report is missing. It must never claim "nothing reduced".
|
|
1511
|
+
* - registered, seed populated → warn, listing every marker.
|
|
1512
|
+
*
|
|
1513
|
+
* Pure and exported so the empty-seed branch is testable without stubbing the
|
|
1514
|
+
* frozen seed module. `status` is returned alongside the text so a caller never
|
|
1515
|
+
* has to recover the case by matching on the message.
|
|
1516
|
+
*
|
|
1517
|
+
* @param {{registered:boolean, features:string[]}} input
|
|
1518
|
+
* @returns {{status:'reported'|'empty-seed'|'no-v2-generation', level:'info'|'warn', message:string}}
|
|
1519
|
+
*/
|
|
1520
|
+
export function describeV2Reduction({ registered, features }) {
|
|
1521
|
+
if (!registered) {
|
|
1522
|
+
return {
|
|
1523
|
+
status: 'no-v2-generation',
|
|
1524
|
+
level: 'info',
|
|
1525
|
+
message: 'No V2 plugin generation registered — nothing reduced',
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (features.length === 0) {
|
|
1529
|
+
return {
|
|
1530
|
+
status: 'empty-seed',
|
|
1531
|
+
level: 'warn',
|
|
1532
|
+
message:
|
|
1533
|
+
'Pantheon IS registered on the V2 plugin generation, but the shared ' +
|
|
1534
|
+
'unsupported-feature seed is empty, so the reduction cannot be reported. ' +
|
|
1535
|
+
"That is a fault in this installation's reporting, not an absence of " +
|
|
1536
|
+
'reduction: the V2 surface still drops the 3 goal tools and the ' +
|
|
1537
|
+
'caller/target delegation matrix.',
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
status: 'reported',
|
|
1542
|
+
level: 'warn',
|
|
1543
|
+
message:
|
|
1544
|
+
'Pantheon is registered on the V2 plugin generation, which registers 3 tools ' +
|
|
1545
|
+
"(hashline_edit, pantheon_cost, pantheon_model) instead of the V1 surface's 6 plus " +
|
|
1546
|
+
`the BackgroundJobBoard and the caller/target delegation matrix. Unsupported: ${features.join(', ')}`,
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Report the V2 feature reduction through doctor's own output shape
|
|
1552
|
+
* (section + warn/info), so it lands in the health summary like every other
|
|
1553
|
+
* finding and never changes the exit code: a V2 install is a supported
|
|
1554
|
+
* configuration, not a broken one.
|
|
1555
|
+
*
|
|
1556
|
+
* @param {{target:string, env?:object}} args
|
|
1557
|
+
* @returns {'reported'|'empty-seed'|'no-v2-generation'|'no-config'}
|
|
1558
|
+
*/
|
|
1559
|
+
export function checkV2UnsupportedFeatures(args) {
|
|
1560
|
+
section('H4. V2 Feature Reduction')
|
|
1561
|
+
|
|
1562
|
+
const configs = collectMcpConfigs(args)
|
|
1563
|
+
if (configs.length === 0) {
|
|
1564
|
+
info('No opencode.json was found — V2 feature reduction check skipped')
|
|
1565
|
+
return 'no-config'
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Branch on REGISTRATION, not on the marker count: an empty seed on a
|
|
1569
|
+
// registered V2 generation is a reporting fault and must not be reported as
|
|
1570
|
+
// the reassuring "nothing reduced".
|
|
1571
|
+
const registered = isV2GenerationRegistered(configs)
|
|
1572
|
+
const verdict = describeV2Reduction({
|
|
1573
|
+
registered,
|
|
1574
|
+
features: collectV2UnsupportedFeatures(configs),
|
|
1575
|
+
})
|
|
1576
|
+
if (verdict.level === 'info') info(verdict.message)
|
|
1577
|
+
else warn(verdict.message)
|
|
1578
|
+
|
|
1579
|
+
return verdict.status
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1433
1582
|
// ---------------------------------------------------------------------------
|
|
1434
1583
|
// Check G: AGENTS.md freshness
|
|
1435
1584
|
// ---------------------------------------------------------------------------
|
|
@@ -1802,6 +1951,7 @@ async function main() {
|
|
|
1802
1951
|
checkAgentsMdFreshness(args)
|
|
1803
1952
|
checkSubagentDepthPlacement(args)
|
|
1804
1953
|
checkPermissionTaskPresence(args)
|
|
1954
|
+
checkV2UnsupportedFeatures(args)
|
|
1805
1955
|
|
|
1806
1956
|
printSummary(args.target)
|
|
1807
1957
|
|
|
@@ -229,8 +229,11 @@ function convertProviderV2toV1(_name, config) {
|
|
|
229
229
|
/**
|
|
230
230
|
* Convert V1 MCP config to V2.
|
|
231
231
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
232
|
+
* The server names are arbitrary — a user calls theirs whatever they like, and
|
|
233
|
+
* which MCP servers exist is entirely their configuration, never ours.
|
|
234
|
+
*
|
|
235
|
+
* V1: { myserver: { type: "remote", url: "...", enabled: true, timeout: 30000 } }
|
|
236
|
+
* V2 installer output: { myserver: { type: "remote", url: "...", enabled: true } }
|
|
234
237
|
*
|
|
235
238
|
* OpenCode 1.18.18 does not have an `mcp.servers` wrapper. It interprets
|
|
236
239
|
* `servers` as the name of an MCP server and then rejects it with
|
|
@@ -285,6 +288,81 @@ function convertMcpV2toV1(mcpConfig) {
|
|
|
285
288
|
return convertMcpV1toV2(mcpConfig)
|
|
286
289
|
}
|
|
287
290
|
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// MCP entrypoint path migration
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Entrypoint filename renames: every historical Python MCP entrypoint shared
|
|
297
|
+
* the `server.py` token, so a broad `pkill -f server.py` (a common idiom in
|
|
298
|
+
* agent and scratch tooling) matched the entire fleet and killed it as
|
|
299
|
+
* collateral. The five entrypoints now carry distinct tokens.
|
|
300
|
+
*
|
|
301
|
+
* The rename alone is a BREAKING change for existing installs: their
|
|
302
|
+
* `opencode.json` carries literal paths like `scripts/memory_mcp_server.py`,
|
|
303
|
+
* and a path that no longer resolves fails every MCP server silently (the
|
|
304
|
+
* same class of failure as the dead plugin surface). This map is the single
|
|
305
|
+
* source of truth for the stale → current basename rewrite that repairs them.
|
|
306
|
+
*/
|
|
307
|
+
export const MCP_ENTRYPOINT_RENAMES = Object.freeze({
|
|
308
|
+
'mcp_resources_server.py': 'mcp_resources.py',
|
|
309
|
+
'code_mode_server.py': 'code_mode.py',
|
|
310
|
+
'memory_mcp_server.py': 'memory_mcp.py',
|
|
311
|
+
'mcp_persistence_server.py': 'mcp_persistence.py',
|
|
312
|
+
'pantheon_vision_server.py': 'pantheon_vision.py',
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Rewrite a single path/argument string when its basename is a renamed MCP
|
|
317
|
+
* entrypoint. The directory prefix is preserved verbatim, so both relative
|
|
318
|
+
* (`scripts/memory_mcp_server.py`) and absolute/hand-edited paths heal without
|
|
319
|
+
* discarding a user's chosen location. Non-matching values pass through.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} value
|
|
322
|
+
* @returns {string}
|
|
323
|
+
*/
|
|
324
|
+
function renameMcpEntrypointPath(value) {
|
|
325
|
+
if (typeof value !== 'string') return value
|
|
326
|
+
const slash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'))
|
|
327
|
+
const base = value.slice(slash + 1)
|
|
328
|
+
const renamed = MCP_ENTRYPOINT_RENAMES[base]
|
|
329
|
+
if (!renamed) return value
|
|
330
|
+
return value.slice(0, value.length - base.length) + renamed
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Heal stale MCP entrypoint paths in a flat named-server map, in place.
|
|
335
|
+
*
|
|
336
|
+
* Scans every server — not only the `pantheon-*` keys — because a user who
|
|
337
|
+
* hand-edited their config may have renamed the key while keeping the
|
|
338
|
+
* Pantheon path. Both `command` (installer writes `[python, script]`) and
|
|
339
|
+
* `args` (the standalone installer writes `[script]`) are covered, since
|
|
340
|
+
* which array carries the script differs between installers.
|
|
341
|
+
*
|
|
342
|
+
* @param {object} mcp - the `mcp` map from a config (mutated in place)
|
|
343
|
+
* @returns {number} count of rewritten strings (0 = already current)
|
|
344
|
+
*/
|
|
345
|
+
export function migrateMcpEntrypointPaths(mcp) {
|
|
346
|
+
if (!mcp || typeof mcp !== 'object' || Array.isArray(mcp)) return 0
|
|
347
|
+
|
|
348
|
+
let changed = 0
|
|
349
|
+
for (const server of Object.values(mcp)) {
|
|
350
|
+
if (!server || typeof server !== 'object') continue
|
|
351
|
+
for (const field of ['command', 'args']) {
|
|
352
|
+
const list = server[field]
|
|
353
|
+
if (!Array.isArray(list)) continue
|
|
354
|
+
for (let i = 0; i < list.length; i++) {
|
|
355
|
+
const next = renameMcpEntrypointPath(list[i])
|
|
356
|
+
if (next !== list[i]) {
|
|
357
|
+
list[i] = next
|
|
358
|
+
changed++
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return changed
|
|
364
|
+
}
|
|
365
|
+
|
|
288
366
|
// ---------------------------------------------------------------------------
|
|
289
367
|
// Top-level key mappings
|
|
290
368
|
// ---------------------------------------------------------------------------
|