pantheon-opencode 1.4.3 → 1.5.0-beta.2
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/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/README.md +161 -1101
- package/README.pt-BR.md +222 -0
- package/bin/pantheon-init.mjs +30 -11
- package/opencode.json +1 -0
- package/package.json +14 -3
- package/plugin.json +15 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +33 -2
- package/scripts/cost.mjs +30 -14
- package/scripts/doctor.mjs +135 -127
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +8 -1
- package/scripts/install/config-migration.mjs +401 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +261 -88
- package/scripts/install/plugin.mjs +112 -42
- package/scripts/install/shared.mjs +13 -158
- package/scripts/install-mcp.mjs +44 -232
- package/scripts/mcp_persistence_server.py +382 -16
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/probe-context-rehydrate.mjs +240 -0
- package/scripts/probe-pantheon-cost.mjs +304 -0
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/sync-tui.mjs +32 -7
- package/scripts/test-opencode-v1-v2-sandbox.sh +944 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/agents/zeus.md +25 -14
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +33 -2
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +382 -16
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/background-job-board.ts +67 -1
- package/src/pantheon/cost-command.ts +60 -39
- package/src/pantheon/delegate-manager.ts +669 -0
- package/src/pantheon/delegation-classifier.ts +307 -0
- package/src/pantheon/delegation-enforce.ts +13 -12
- package/src/pantheon/delegation-finalize.ts +374 -0
- package/src/pantheon/delegation-notify.ts +212 -0
- package/src/pantheon/delegation-toast.ts +73 -0
- package/src/pantheon/delegation.ts +1120 -0
- package/src/pantheon/goal-loop.ts +2 -2
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/hashline/tool.ts +2 -2
- package/src/pantheon/model-command.ts +2 -1
- package/src/pantheon/permission-eval.ts +284 -0
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/token-opt.ts +781 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +131 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +824 -0
- package/src/plugin.ts +236 -42
- 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 +10 -7
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +11 -8
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +22 -16
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +28 -24
- package/src/skills/README.md +0 -2
- package/src/pantheon/tool-context.ts +0 -41
package/README.pt-BR.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Pantheon
|
|
2
|
+
|
|
3
|
+
**Uma forma mais clara de trabalhar com OpenCode em projetos reais.** O
|
|
4
|
+
Pantheon reúne planejamento, implementação, revisão e documentação em uma
|
|
5
|
+
experiência guiada. É um plugin e instalador para OpenCode, feito para pessoas
|
|
6
|
+
e equipes que querem mais estrutura sem perder o controle do próprio código.
|
|
7
|
+
|
|
8
|
+
[English](README.md) ·
|
|
9
|
+
[Repositório](https://github.com/ils15/pantheon-opencode) · [Licença MIT](LICENSE)
|
|
10
|
+
|
|
11
|
+
[](https://github.com/ils15/pantheon-opencode/releases/tag/v1.5.0)
|
|
12
|
+
[](https://github.com/ils15/pantheon-opencode/actions)
|
|
13
|
+
[](https://doi.org/10.5281/zenodo.22306637)
|
|
14
|
+
|
|
15
|
+
## O que é?
|
|
16
|
+
|
|
17
|
+
O Pantheon é um companheiro para o [OpenCode](https://opencode.ai/) que ajuda a
|
|
18
|
+
transformar uma ideia em uma alteração revisada. Ele oferece uma forma comum
|
|
19
|
+
de planejar o trabalho, avançar, conferir resultados e preservar o contexto
|
|
20
|
+
útil do projeto.
|
|
21
|
+
|
|
22
|
+
## Por que usar?
|
|
23
|
+
|
|
24
|
+
- **Menos troca de contexto:** planeje e construa no mesmo fluxo.
|
|
25
|
+
- **Alterações mais conscientes:** peça revisões e verificações antes de
|
|
26
|
+
considerar o trabalho concluído.
|
|
27
|
+
- **Um ponto de partida repetível:** use a mesma configuração em projetos e
|
|
28
|
+
com colaboradores diferentes.
|
|
29
|
+
- **Você continua no comando:** o Pantheon apoia suas decisões, mas não
|
|
30
|
+
substitui seu julgamento nem a revisão do código gerado.
|
|
31
|
+
|
|
32
|
+
## Comece em 2 minutos
|
|
33
|
+
|
|
34
|
+
Requisitos: [OpenCode 1.18.4+](https://opencode.ai/docs/) e Node.js 18+.
|
|
35
|
+
|
|
36
|
+
No projeto em que você quer usar o Pantheon:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx pantheon-opencode init
|
|
40
|
+
opencode
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
O instalador orienta você pelas opções disponíveis. Para servidores MCP
|
|
44
|
+
opcionais, instalação local no projeto ou uso sem perguntas, consulte o
|
|
45
|
+
[guia de instalação](docs/INSTALLATION.md).
|
|
46
|
+
|
|
47
|
+
## Um exemplo simples
|
|
48
|
+
|
|
49
|
+
Com o OpenCode em execução, descreva o resultado que você quer:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
/pantheon Adicione exportação CSV à página de relatórios, com testes e revisão.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
O Pantheon ajuda a transformar esse pedido em um plano e em etapas revisadas.
|
|
56
|
+
|
|
57
|
+
## Para quem é?
|
|
58
|
+
|
|
59
|
+
Para desenvolvedores, mantenedores e equipes que usam OpenCode e querem uma
|
|
60
|
+
forma mais consistente de lidar com correções pequenas e mudanças maiores. É
|
|
61
|
+
especialmente útil quando o projeto se beneficia de decisões registradas,
|
|
62
|
+
verificações repetíveis e passagens claras entre etapas do trabalho.
|
|
63
|
+
|
|
64
|
+
## O que inclui?
|
|
65
|
+
|
|
66
|
+
- Um instalador guiado para disponibilizar o Pantheon no OpenCode.
|
|
67
|
+
- Instruções e comandos reutilizáveis para planejar, construir, revisar e
|
|
68
|
+
documentar o trabalho.
|
|
69
|
+
- Memória de projeto para preservar contexto relevante entre sessões.
|
|
70
|
+
- Integrações opcionais para tarefas comuns de desenvolvimento.
|
|
71
|
+
|
|
72
|
+
## Status
|
|
73
|
+
|
|
74
|
+
Versão atual: **v1.5.0**. O Pantheon foi feito para OpenCode e depende da
|
|
75
|
+
disponibilidade e da configuração do OpenCode e dos serviços opcionais que você
|
|
76
|
+
escolher. Veja as [releases](https://github.com/ils15/pantheon-opencode/releases)
|
|
77
|
+
e o [changelog](CHANGELOG.md) para acompanhar as mudanças.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Novidades da 1.5.0
|
|
81
|
+
|
|
82
|
+
- Instalador exclusivo para OpenCode: guias de plataformas consolidados em um
|
|
83
|
+
único [guia OpenCode](docs/platforms/opencode.md).
|
|
84
|
+
- Novo CLI `uninstall` com escopos project/global e checagem de ownership:
|
|
85
|
+
`node scripts/uninstall.mjs --project|--global [--dry-run] [--force]`.
|
|
86
|
+
- Recursos MCP endurecidos: correção do `pantheon://agents` e proteção contra
|
|
87
|
+
symlink/traversal nos caminhos de recursos.
|
|
88
|
+
- Compatibilidade com OpenCode V2: merge de configuração `plugins` /
|
|
89
|
+
`mcp.servers.enabled` e launch stdio MCP com PWD correto.
|
|
90
|
+
- `doctor` e health checks de instalação expandidos.
|
|
91
|
+
- Validador de sandbox para instalações globais
|
|
92
|
+
(`scripts/test-opencode-v1-v2-sandbox.sh`) cobrindo OpenCode V1/V2 lado a
|
|
93
|
+
lado — veja [Validação em sandbox](#validação-em-sandbox-v1v2).
|
|
94
|
+
- A flag `--prompts` do instalador está planejada para uma release futura.
|
|
95
|
+
|
|
96
|
+
## OpenCode V1/V2 — Versão dupla (1.5.0)
|
|
97
|
+
|
|
98
|
+
O Pantheon tem dois contratos de plugin OpenCode **exclusivos**. A configuração
|
|
99
|
+
comum do OpenCode pode ser compartilhada, mas o registro do plugin Pantheon é
|
|
100
|
+
selecionado por instalação; os plugins Pantheon V1 e V2 nunca devem ser
|
|
101
|
+
registrados juntos.
|
|
102
|
+
|
|
103
|
+
| | V1 | V2 |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| Chave de config do OpenCode | `plugin` singular | `plugins` plural |
|
|
106
|
+
| Registro Pantheon | `src/plugin.ts` mais `src/plugins/pantheon-hooks.ts` | `pantheon-opencode/plugin-v2` (`src/plugin-v2.ts`) |
|
|
107
|
+
| Contrato de runtime | Plugin Pantheon legado, incluindo `pantheon_delegate`, ferramentas read/list, hooks de evento/ferramenta e tratamento de compactação V1 | Plugin V2 completo: 9 ferramentas de orquestração, 4 assinaturas de eventos, session hooks (`prompt`, `context`), tool hooks (`execute.before`/`after`), além de transforms de configuração |
|
|
108
|
+
| APIs V1 | Registradas | Definições próprias de ferramentas via `ctx.tool.transform()` — não pelo caminho do plugin V1 |
|
|
109
|
+
|
|
110
|
+
O plugin V2 fornece 9 ferramentas de orquestração (`pantheon_delegate`,
|
|
111
|
+
`pantheon_delegation_read`, `pantheon_delegation_list`, `hashline_edit`,
|
|
112
|
+
`pantheon_goal_create`, `pantheon_goal_get`, `pantheon_goal_update`,
|
|
113
|
+
`pantheon_cost`, `pantheon_model`), 4 assinaturas de eventos (`session.created`,
|
|
114
|
+
`session.idle`, `session.error`, `session.compacted`), session hooks (`prompt`,
|
|
115
|
+
`context`) e tool hooks (`execute.before`, `execute.after`). O único recurso V2
|
|
116
|
+
sem suporte é `legacy-hooks` (a superfície de API de delegate específica do V1).
|
|
117
|
+
|
|
118
|
+
O pacote expõe os dois contratos como exports importáveis:
|
|
119
|
+
`pantheon-opencode/plugin` (V1), `pantheon-opencode/plugin-v2` (V2) e
|
|
120
|
+
`pantheon-opencode/v2-bridge` (interop opcional), para que o host carregue
|
|
121
|
+
explicitamente o contrato desejado.
|
|
122
|
+
|
|
123
|
+
A ponte V1→V2 (`src/pantheon/v2-bridge.ts`) habilita interop opcional:
|
|
124
|
+
singletons de infraestrutura V1 (BackgroundJobBoard, DelegationClient,
|
|
125
|
+
GoalStore, TodoEnforcer, VisionHandler) são repassados via `ctx.options` do V2.
|
|
126
|
+
A ponte é opcional — o V2 funciona standalone com degradação graciosa.
|
|
127
|
+
|
|
128
|
+
Selecione o contrato explicitamente na instalação:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx pantheon-opencode init --opencode-version v1
|
|
132
|
+
npx pantheon-opencode init --opencode-version v2
|
|
133
|
+
npx pantheon-opencode init --opencode-version auto
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`--version v1|v2|auto` é aceito como a grafia antiga do seletor quando usado
|
|
137
|
+
depois de `init`. `auto` é conservador, não uma autodetecção geral de
|
|
138
|
+
plataforma: `OPENCODE_VERSION=v1|v2` vence; caso contrário, um `OPENCODE_BIN`
|
|
139
|
+
terminando em `opencode2` seleciona V2; qualquer outro caso seleciona V1. O
|
|
140
|
+
instalador remove referências Pantheon das duas formas de config antes de
|
|
141
|
+
gravar apenas o registro Pantheon selecionado. Entradas de terceiros não são
|
|
142
|
+
convertidas nem reivindicadas por essa regra.
|
|
143
|
+
|
|
144
|
+
O relatório `pantheon_cost` (apenas V1) pode selecionar o banco com
|
|
145
|
+
`PANTHEON_OPENCODE_VERSION=v1` ou `v2` (`opencode.db` ou `opencode-v2.db`).
|
|
146
|
+
`PANTHEON_COST_DB=/caminho/absoluto/para/opencode.db` tem precedência sobre o
|
|
147
|
+
seletor de versão, e um `dbPath` explícito fornecido pelo chamador da
|
|
148
|
+
ferramenta tem precedência sobre ambos. O resolver nunca consulta o banco da
|
|
149
|
+
outra versão e reporta um erro acionável quando o banco selecionado está
|
|
150
|
+
ausente ou com schema incompatível.
|
|
151
|
+
|
|
152
|
+
O instalador continua gravando as configurações de compatibilidade exigidas
|
|
153
|
+
pelo host OpenCode selecionado, como `experimental.subagent_depth`; isso não
|
|
154
|
+
converte um plugin V1 em V2 nem dá hooks V1 ao V2.
|
|
155
|
+
|
|
156
|
+
## Releases beta
|
|
157
|
+
|
|
158
|
+
Um pull request com o label exato `release:beta` aciona o fluxo de release beta. Consulte [docs/RELEASING.md](docs/RELEASING.md) para detalhes de validação e recuperação.
|
|
159
|
+
|
|
160
|
+
## Validação em sandbox (V1/V2)
|
|
161
|
+
|
|
162
|
+
O `scripts/test-opencode-v1-v2-sandbox.sh` valida o pacote instalado
|
|
163
|
+
globalmente como um usuário real dentro de um sandbox isolado (com `HOME`,
|
|
164
|
+
prefix npm e venv próprios) — nunca o ambiente de desenvolvimento. Ele verifica
|
|
165
|
+
OpenCode V1 (`opencode`) e V2 (`opencode2`) lado a lado: binários, conectividade
|
|
166
|
+
MCP, `doctor` e — com `--prompts` — uma bateria de prompts cobrindo o recurso
|
|
167
|
+
`pantheon://agents`, memory store/recall, escrita no filesystem e delegação de
|
|
168
|
+
agente. Falhas ambientais (rede, auth de provider, Docker) são classificadas
|
|
169
|
+
como `AMBIENTAL` e nunca falham a execução; apenas falhas reais falham.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
scripts/test-opencode-v1-v2-sandbox.sh --prepare # tarball + install + init no sandbox
|
|
173
|
+
scripts/test-opencode-v1-v2-sandbox.sh --run v1 --prompts # validação base + bateria de prompts (V1)
|
|
174
|
+
scripts/test-opencode-v1-v2-sandbox.sh --run v2 # apenas validação base (V2)
|
|
175
|
+
scripts/test-opencode-v1-v2-sandbox.sh --prompts # bateria de prompts para as duas versões
|
|
176
|
+
scripts/test-opencode-v1-v2-sandbox.sh --reset # limpa a raiz do sandbox
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Os modos são combináveis (ex.: `--prepare --run v1 --prompts`). Os binários são
|
|
180
|
+
resolvidos estritamente dentro do prefix npm do sandbox — um sandbox não
|
|
181
|
+
preparado falha rápido em vez de testar silenciosamente a instalação do host.
|
|
182
|
+
|
|
183
|
+
Variáveis de ambiente:
|
|
184
|
+
|
|
185
|
+
| Variável | Padrão | Finalidade |
|
|
186
|
+
|----------|---------|---------|
|
|
187
|
+
| `PANTHEON_SANDBOX_ROOT` | `~/pantheon-sandbox` | Raiz do sandbox (recusada se insegura para `--reset`) |
|
|
188
|
+
| `OPENCODE_V1_SPEC` | `opencode-ai@1.18.18` | Spec npm que fornece o binário `opencode` |
|
|
189
|
+
| `OPENCODE_V2_SPEC` | `@opencode-ai/cli@beta` | Spec npm que fornece o binário `opencode2` |
|
|
190
|
+
| `PANTHEON_SANDBOX_MODEL` | `opencode-go/mimo-v2.5` | Modelo usado pelo init e pelos prompts |
|
|
191
|
+
| `PANTHEON_PROMPT_TIMEOUT` | `300` | Timeout por prompt em segundos |
|
|
192
|
+
|
|
193
|
+
Códigos de saída: `0` sem falhas reais · `1` falha real (veja
|
|
194
|
+
`prompts-report.md` na raiz do sandbox) · `2` erro de uso · `3` sandbox não
|
|
195
|
+
preparado.
|
|
196
|
+
|
|
197
|
+
## Documentação
|
|
198
|
+
|
|
199
|
+
- [Instalação](docs/INSTALLATION.md) · [Início rápido](docs/QUICKSTART.md)
|
|
200
|
+
- [Arquitetura](docs/ARCHITECTURE.md) · [Ferramentas MCP](docs/mcp-tools.md)
|
|
201
|
+
- [Plataformas](docs/PLATFORMS.md) · [Atualização](docs/UPGRADING.pt-BR.md)
|
|
202
|
+
- [Referência de agentes](docs/agents/README.md) · [Referência de skills](src/skills/README.md)
|
|
203
|
+
- [Processo de release](docs/RELEASING.md) · [Contribuição](CONTRIBUTING.md)
|
|
204
|
+
- [Changelog](CHANGELOG.md)
|
|
205
|
+
|
|
206
|
+
## Contribua
|
|
207
|
+
|
|
208
|
+
Ideias, relatos de problemas, melhorias na documentação e contribuições de
|
|
209
|
+
código são bem-vindos. Leia [CONTRIBUTING.md](CONTRIBUTING.md) antes de abrir
|
|
210
|
+
uma issue ou pull request.
|
|
211
|
+
|
|
212
|
+
## Citação e DOI
|
|
213
|
+
|
|
214
|
+
O Pantheon é distribuído sob a [Licença MIT](LICENSE). Para o registro publicado
|
|
215
|
+
da v1.4.3, use o [DOI do Zenodo](https://doi.org/10.5281/zenodo.22306637); os
|
|
216
|
+
metadados de citação também estão em [CITATION.cff](CITATION.cff).
|
|
217
|
+
|
|
218
|
+
Repositório canônico: <https://github.com/ils15/pantheon-opencode>
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
[Read in English](README.md)
|
package/bin/pantheon-init.mjs
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* install for the operator; state isolation is a runtime concern
|
|
15
15
|
* (OPENCODE_DB ~/.local/share/opencode/opencode-v2.db, service port 49375).
|
|
16
16
|
*/
|
|
17
|
-
import fs from 'fs'
|
|
18
|
-
import os from 'os'
|
|
19
|
-
import path from 'path'
|
|
20
|
-
import { fileURLToPath } from 'url'
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import os from 'node:os'
|
|
19
|
+
import path from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
21
|
|
|
22
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
23
23
|
const ROOT = path.resolve(__dirname, '..')
|
|
@@ -50,7 +50,7 @@ function printUsage() {
|
|
|
50
50
|
console.log(' npx pantheon-opencode init --interactive # Force interactive TUI mode')
|
|
51
51
|
console.log(' npx pantheon-opencode init --headless # Force non-interactive mode')
|
|
52
52
|
console.log(' npx pantheon-opencode init -y # Skip confirmations, use defaults')
|
|
53
|
-
console.log(' npx pantheon-opencode init --version v2
|
|
53
|
+
console.log(' npx pantheon-opencode init --opencode-version v1|v2|auto')
|
|
54
54
|
console.log(' npx pantheon-opencode init --preset <name> # Install and activate model preset')
|
|
55
55
|
console.log(' npx pantheon-opencode set-tier <name> # Set active model preset (global)')
|
|
56
56
|
console.log(
|
|
@@ -59,6 +59,8 @@ function printUsage() {
|
|
|
59
59
|
console.log(' npx pantheon-opencode prune # List legacy artifacts (dry-run)')
|
|
60
60
|
console.log(' npx pantheon-opencode prune --apply # Remove stale backups')
|
|
61
61
|
console.log(' npx pantheon-opencode doctor [--profile] # Run health check')
|
|
62
|
+
console.log(' npx pantheon-opencode uninstall --project # Remove project artifacts safely')
|
|
63
|
+
console.log(' npx pantheon-opencode uninstall --global # Remove global artifacts safely')
|
|
62
64
|
console.log(' npx pantheon-opencode --version, -v # Print version and exit')
|
|
63
65
|
console.log(' npx pantheon-opencode --help # Show this help')
|
|
64
66
|
}
|
|
@@ -78,11 +80,21 @@ async function main() {
|
|
|
78
80
|
// prune handles its own --help (delegated to scripts/prune.mjs), so its
|
|
79
81
|
// dispatch must run BEFORE the generic --help check.
|
|
80
82
|
if (command === 'prune') {
|
|
81
|
-
const { spawnSync } = await import('child_process')
|
|
83
|
+
const { spawnSync } = await import('node:child_process')
|
|
82
84
|
const pruneScript = path.join(ROOT, 'scripts', 'prune.mjs')
|
|
83
85
|
const result = spawnSync(process.execPath, [pruneScript, ...args.slice(1)], {
|
|
84
86
|
stdio: 'inherit',
|
|
85
|
-
cwd:
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
})
|
|
89
|
+
process.exit(result.status ?? 1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (command === 'uninstall') {
|
|
93
|
+
const { spawnSync } = await import('node:child_process')
|
|
94
|
+
const uninstallScript = path.join(ROOT, 'scripts', 'uninstall.mjs')
|
|
95
|
+
const result = spawnSync(process.execPath, [uninstallScript, ...args.slice(1)], {
|
|
96
|
+
stdio: 'inherit',
|
|
97
|
+
cwd: process.cwd(),
|
|
86
98
|
})
|
|
87
99
|
process.exit(result.status ?? 1)
|
|
88
100
|
}
|
|
@@ -91,7 +103,7 @@ async function main() {
|
|
|
91
103
|
// Forwarding the FULL args (profile, target, --fix, ...) keeps the packaged
|
|
92
104
|
// CLI a thin wrapper; the doctor banner reports the resolved profile.
|
|
93
105
|
if (command === 'doctor') {
|
|
94
|
-
const { spawnSync } = await import('child_process')
|
|
106
|
+
const { spawnSync } = await import('node:child_process')
|
|
95
107
|
const doctorScript = path.join(ROOT, 'scripts', 'doctor.mjs')
|
|
96
108
|
const result = spawnSync(process.execPath, [doctorScript, ...args.slice(1)], {
|
|
97
109
|
stdio: 'inherit',
|
|
@@ -198,7 +210,14 @@ async function main() {
|
|
|
198
210
|
// --version v1|v2 labels the install target (informational; the config is
|
|
199
211
|
// shared and V1-shaped under both versions). Invalid values fail fast.
|
|
200
212
|
const versionIndex = args.indexOf('--version')
|
|
201
|
-
const
|
|
213
|
+
const legacyVersionIndex = args.indexOf('--opencode-version')
|
|
214
|
+
const inlineVersion = args.find((arg) => arg.startsWith('--opencode-version='))
|
|
215
|
+
const versionOpt =
|
|
216
|
+
versionIndex >= 0
|
|
217
|
+
? (args[versionIndex + 1] ?? null)
|
|
218
|
+
: legacyVersionIndex >= 0
|
|
219
|
+
? (args[legacyVersionIndex + 1] ?? null)
|
|
220
|
+
: (inlineVersion?.split('=', 2)[1] ?? null)
|
|
202
221
|
if (versionOpt !== null && versionOpt !== 'v1' && versionOpt !== 'v2') {
|
|
203
222
|
console.error(`❌ Invalid --version "${versionOpt}" — expected v1 or v2`)
|
|
204
223
|
process.exit(1)
|
|
@@ -239,7 +258,7 @@ async function main() {
|
|
|
239
258
|
console.log('')
|
|
240
259
|
console.log(' Running health check...')
|
|
241
260
|
try {
|
|
242
|
-
const { spawnSync } = await import('child_process')
|
|
261
|
+
const { spawnSync } = await import('node:child_process')
|
|
243
262
|
const doctorScript = path.join(ROOT, 'scripts', 'doctor.mjs')
|
|
244
263
|
spawnSync(process.execPath, [doctorScript], { stdio: 'inherit', cwd: ROOT })
|
|
245
264
|
} catch {
|
|
@@ -255,7 +274,7 @@ async function main() {
|
|
|
255
274
|
console.log(' Next steps:')
|
|
256
275
|
console.log(' 1. Verify installation:')
|
|
257
276
|
console.log(' npx pantheon-opencode doctor')
|
|
258
|
-
console.log(' 2. Launch
|
|
277
|
+
console.log(' 2. Launch OpenCode')
|
|
259
278
|
console.log(' 3. Invoke agents with @agent-name in chat')
|
|
260
279
|
console.log(' 4. For project-local install:')
|
|
261
280
|
console.log(' npx pantheon-opencode init --project')
|
package/opencode.json
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0-beta.2",
|
|
4
4
|
"description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"publishConfig": {
|
|
6
7
|
"access": "public",
|
|
7
8
|
"registry": "https://registry.npmjs.org/"
|
|
8
9
|
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./plugin": "./src/plugin.ts",
|
|
12
|
+
"./plugin-v2": "./src/plugin-v2.ts",
|
|
13
|
+
"./v2-bridge": "./src/pantheon/v2-bridge.ts"
|
|
14
|
+
},
|
|
9
15
|
"scripts": {
|
|
10
16
|
"postinstall": "node scripts/postinstall.mjs && node scripts/sync-tui.mjs",
|
|
11
17
|
"prepare": "husky",
|
|
@@ -32,7 +38,7 @@
|
|
|
32
38
|
"hooks:update": "pre-commit autoupdate",
|
|
33
39
|
"test:security": "npm run secret-scan && node tests/test_install_mcp_vision.mjs && node tests/test_doctor_layers.mjs",
|
|
34
40
|
"test:node": "node --test tests/*.mjs tests/pantheon/*.mjs",
|
|
35
|
-
"test:ts": "for f in tests/pantheon/*.test.ts; do npx tsx \"$f\" || exit 1; done",
|
|
41
|
+
"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",
|
|
36
42
|
"test": "python3 -m pytest tests/ -q 2>&1; status=$?; if [ \"$status\" -eq 5 ]; then echo \"No tests found\"; fi; exit \"$status\"",
|
|
37
43
|
"test:ci": "python3 -m pytest tests/ -q --tb=short 2>&1"
|
|
38
44
|
},
|
|
@@ -41,9 +47,13 @@
|
|
|
41
47
|
},
|
|
42
48
|
"files": [
|
|
43
49
|
"LICENSE",
|
|
50
|
+
"plugin.json",
|
|
44
51
|
"scripts/install/*.mjs",
|
|
45
52
|
"scripts/*.py",
|
|
46
53
|
"src/plugin.ts",
|
|
54
|
+
"src/plugin-v2.ts",
|
|
55
|
+
"src/plugin-v2",
|
|
56
|
+
"src/pantheon/v2-bridge.ts",
|
|
47
57
|
"README.md",
|
|
48
58
|
"src/mcp/*.txt",
|
|
49
59
|
"commands/*.md",
|
|
@@ -62,6 +72,7 @@
|
|
|
62
72
|
"src/mcp/*.sh",
|
|
63
73
|
"src/routing.yml",
|
|
64
74
|
"src/pantheon/**",
|
|
75
|
+
".pantheon/code-mode/**",
|
|
65
76
|
"src/plugins/**",
|
|
66
77
|
"scripts/hooks/**",
|
|
67
78
|
"src/mcp/requirements*",
|
|
@@ -72,13 +83,13 @@
|
|
|
72
83
|
"!**/*.pyc"
|
|
73
84
|
],
|
|
74
85
|
"dependencies": {
|
|
86
|
+
"@opencode-ai/plugin": "1.18.18",
|
|
75
87
|
"js-yaml": "^4.3.0",
|
|
76
88
|
"zod": "4.1.8"
|
|
77
89
|
},
|
|
78
90
|
"devDependencies": {
|
|
79
91
|
"@commitlint/cli": "^19.8.0",
|
|
80
92
|
"@commitlint/config-conventional": "^19.8.0",
|
|
81
|
-
"@opencode-ai/plugin": "1.18.11",
|
|
82
93
|
"husky": "^9.1.7"
|
|
83
94
|
},
|
|
84
95
|
"repository": {
|
package/plugin.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pantheon",
|
|
3
|
+
"publisher": "ils15",
|
|
4
|
+
"version": "1.5.0",
|
|
5
|
+
"displayName": "Pantheon",
|
|
6
|
+
"description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
|
|
7
|
+
"agents": "./src/agents",
|
|
8
|
+
"skills": "./src/skills",
|
|
9
|
+
"instructions": "./src/instructions",
|
|
10
|
+
"prompts": "./prompts",
|
|
11
|
+
"repository": "https://github.com/ils15/pantheon-opencode",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/routing.yml"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -34,11 +34,11 @@ def pantheon_home() -> Path:
|
|
|
34
34
|
"""
|
|
35
35
|
env = os.environ.get("PANTHEON_HOME")
|
|
36
36
|
if env:
|
|
37
|
-
return Path(env).expanduser().
|
|
37
|
+
return Path(env).expanduser().absolute()
|
|
38
38
|
|
|
39
39
|
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
40
40
|
if xdg:
|
|
41
|
-
return Path(xdg).expanduser().
|
|
41
|
+
return Path(xdg).expanduser().absolute() / "opencode"
|
|
42
42
|
|
|
43
43
|
return Path.home() / ".config" / "opencode"
|
|
44
44
|
|
|
@@ -48,7 +48,8 @@ def pantheon_project() -> Path | None:
|
|
|
48
48
|
|
|
49
49
|
Resolution priority:
|
|
50
50
|
1. $PANTHEON_PROJECT env var (explicit override)
|
|
51
|
-
2.
|
|
51
|
+
2. The validated logical ``$PWD`` supplied by the launcher
|
|
52
|
+
3. Current working directory (set by MCP client's cwd in opencode.json)
|
|
52
53
|
|
|
53
54
|
Returns:
|
|
54
55
|
Absolute Path to the project root, or None if neither is available
|
|
@@ -56,10 +57,50 @@ def pantheon_project() -> Path | None:
|
|
|
56
57
|
"""
|
|
57
58
|
env = os.environ.get("PANTHEON_PROJECT")
|
|
58
59
|
if env:
|
|
59
|
-
return Path(env).expanduser().
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
return Path(env).expanduser().absolute()
|
|
61
|
+
|
|
62
|
+
logical_cwd = os.environ.get("PWD")
|
|
63
|
+
try:
|
|
64
|
+
cwd = os.getcwd()
|
|
65
|
+
except OSError:
|
|
66
|
+
cwd = ""
|
|
67
|
+
if logical_cwd and _is_valid_logical_cwd(logical_cwd, cwd):
|
|
68
|
+
return Path(logical_cwd).expanduser().absolute()
|
|
62
69
|
if cwd:
|
|
63
|
-
return Path(cwd).
|
|
70
|
+
return Path(cwd).absolute()
|
|
64
71
|
|
|
65
72
|
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_valid_logical_cwd(logical_cwd: str, physical_cwd: str) -> bool:
|
|
76
|
+
"""Validate a launcher-provided logical cwd without resolving its result.
|
|
77
|
+
|
|
78
|
+
``PWD`` is trusted only when it is an existing directory naming the same
|
|
79
|
+
inode as the process cwd. This preserves symlink spelling while rejecting
|
|
80
|
+
forged or stale environment values.
|
|
81
|
+
"""
|
|
82
|
+
logical = Path(logical_cwd).expanduser()
|
|
83
|
+
if not logical.is_absolute() or not logical.is_dir():
|
|
84
|
+
return False
|
|
85
|
+
try:
|
|
86
|
+
return os.path.samefile(logical, physical_cwd)
|
|
87
|
+
except (OSError, ValueError):
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def has_symlink_component(path: Path) -> bool:
|
|
92
|
+
"""Return whether any existing lexical component of ``path`` is a symlink.
|
|
93
|
+
|
|
94
|
+
The lexical path is intentionally inspected before ``resolve()`` so a
|
|
95
|
+
symlink cannot be hidden by resolving to an otherwise trusted directory.
|
|
96
|
+
Missing trailing components are allowed; this keeps normal project paths
|
|
97
|
+
usable while still checking every existing ancestor.
|
|
98
|
+
"""
|
|
99
|
+
current = Path(path.anchor) if path.is_absolute() else Path.cwd()
|
|
100
|
+
for component in path.parts[1:] if path.is_absolute() else path.parts:
|
|
101
|
+
if component in {"", "."}:
|
|
102
|
+
continue
|
|
103
|
+
current /= component
|
|
104
|
+
if current.is_symlink():
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Validate agent YAML frontmatter in CI."""
|
|
3
|
-
import os
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
4
8
|
|
|
5
9
|
errors = 0
|
|
6
10
|
for f in sorted(os.listdir('src/agents')):
|
|
7
11
|
if not f.endswith('.md'):
|
|
8
12
|
continue
|
|
9
|
-
|
|
13
|
+
with open(f"src/agents/{f}", encoding="utf-8") as agent_file:
|
|
14
|
+
content = agent_file.read()
|
|
10
15
|
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
|
11
16
|
if match:
|
|
12
17
|
try:
|
|
@@ -18,8 +18,8 @@ from __future__ import annotations
|
|
|
18
18
|
import asyncio
|
|
19
19
|
import logging
|
|
20
20
|
import os
|
|
21
|
-
import signal
|
|
22
21
|
import shutil
|
|
22
|
+
import signal
|
|
23
23
|
import stat
|
|
24
24
|
import sys
|
|
25
25
|
import time
|
|
@@ -28,7 +28,6 @@ from pathlib import Path
|
|
|
28
28
|
from typing import Any
|
|
29
29
|
|
|
30
30
|
import yaml
|
|
31
|
-
|
|
32
31
|
from _pantheon_paths import pantheon_home, pantheon_project
|
|
33
32
|
from mcp.server.fastmcp import FastMCP
|
|
34
33
|
|
|
@@ -133,6 +132,34 @@ def _detect_prlimit() -> str | None:
|
|
|
133
132
|
# 1. /.opencode/.pantheon/code-mode/ (project install)
|
|
134
133
|
# 2. /.pantheon/code-mode/ (legacy fallback)
|
|
135
134
|
# 3. /.pantheon/code-mode/ (global fallback)
|
|
135
|
+
# 4. .pantheon/code-mode/ shipped inside the installed package (tarball
|
|
136
|
+
# fallback — package.json `files` includes .pantheon/code-mode/**)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _packaged_scripts_dir() -> Path | None:
|
|
140
|
+
"""Return the code-mode dir shipped inside the installed package, if any.
|
|
141
|
+
|
|
142
|
+
Walks up from this file looking for a `.pantheon/code-mode/` directory
|
|
143
|
+
(e.g. `<pkg>/.pantheon/code-mode` when installed from the tarball).
|
|
144
|
+
Returns None when no packaged copy exists.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
here = Path(__file__).resolve()
|
|
148
|
+
except Exception:
|
|
149
|
+
return None
|
|
150
|
+
for parent in [here.parent, *here.parents]:
|
|
151
|
+
try:
|
|
152
|
+
candidate = parent / ".pantheon" / "code-mode"
|
|
153
|
+
except Exception:
|
|
154
|
+
continue
|
|
155
|
+
try:
|
|
156
|
+
if candidate.is_dir():
|
|
157
|
+
return candidate
|
|
158
|
+
except OSError:
|
|
159
|
+
continue
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
136
163
|
_PANTHEON_HOME: Path = pantheon_home()
|
|
137
164
|
_SCRIPTS_DIR_CANDIDATES: list[Path] = []
|
|
138
165
|
_proj = pantheon_project()
|
|
@@ -146,6 +173,10 @@ for _candidate in _SCRIPTS_DIR_CANDIDATES:
|
|
|
146
173
|
if _candidate.is_dir():
|
|
147
174
|
SCRIPTS_DIR = _candidate
|
|
148
175
|
break
|
|
176
|
+
else:
|
|
177
|
+
_packaged = _packaged_scripts_dir()
|
|
178
|
+
if _packaged is not None:
|
|
179
|
+
SCRIPTS_DIR = _packaged
|
|
149
180
|
|
|
150
181
|
# ── FastMCP App ───────────────────────────────────────────────────────────────
|
|
151
182
|
mcp = FastMCP(
|
package/scripts/cost.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* cost.mjs — read-only
|
|
3
|
+
* cost.mjs — read-only token aggregation from opencode.db.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* the plugin process
|
|
7
|
-
*
|
|
8
|
-
* { ok: true,
|
|
5
|
+
* CLI fallback for the `pantheon_cost` tool when node:sqlite is unavailable
|
|
6
|
+
* in the plugin process. Uses node:sqlite itself and prints a single JSON
|
|
7
|
+
* object to stdout:
|
|
8
|
+
* { ok: true, days, rows: [{agent, phase, tokensInput, tokensOutput, tokensTotal}] }
|
|
9
9
|
* { ok: false, error: "<message>" } (exit code 1)
|
|
10
10
|
*
|
|
11
11
|
* NEVER writes to the database — opened read-only. Malformed rows are
|
|
12
|
-
* skipped; assistant messages carry the authoritative
|
|
12
|
+
* skipped; assistant messages carry the authoritative token usage.
|
|
13
13
|
*
|
|
14
|
-
* Usage: node scripts/cost.mjs <dbPath> <days>
|
|
14
|
+
* Usage: node scripts/cost.mjs --db-path <dbPath> --days <days>
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync } from 'node:fs'
|
|
17
17
|
import { DatabaseSync } from 'node:sqlite'
|
|
@@ -43,8 +43,13 @@ function assertCompatibleSchema(db) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function main() {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
46
|
+
const argv = process.argv.slice(2)
|
|
47
|
+
const valueFor = (name) => {
|
|
48
|
+
const index = argv.indexOf(name)
|
|
49
|
+
return index >= 0 ? argv[index + 1] : undefined
|
|
50
|
+
}
|
|
51
|
+
const dbPath = valueFor('--db-path')
|
|
52
|
+
const days = Number(valueFor('--days') ?? '7')
|
|
48
53
|
|
|
49
54
|
if (!dbPath) return fail('missing <dbPath> argument')
|
|
50
55
|
if (!existsSync(dbPath)) return fail(`database not found: ${dbPath}`)
|
|
@@ -64,19 +69,30 @@ function main() {
|
|
|
64
69
|
if (info.role !== 'assistant') continue
|
|
65
70
|
const agent = typeof info.agent === 'string' && info.agent !== '' ? info.agent : null
|
|
66
71
|
if (!agent) continue
|
|
67
|
-
const
|
|
72
|
+
const metadata = info.metadata
|
|
73
|
+
const phase =
|
|
74
|
+
(typeof info.phase === 'string' && info.phase) ||
|
|
75
|
+
(typeof metadata?.phase === 'string' && metadata.phase) ||
|
|
76
|
+
'unknown'
|
|
68
77
|
const tokensInput = Number(info.tokens?.input) || 0
|
|
69
78
|
const tokensOutput = Number(info.tokens?.output) || 0
|
|
70
|
-
const
|
|
71
|
-
acc.
|
|
79
|
+
const key = `${agent}\u0000${phase}`
|
|
80
|
+
const acc = byAgent.get(key) ?? {
|
|
81
|
+
agent,
|
|
82
|
+
phase,
|
|
83
|
+
tokensInput: 0,
|
|
84
|
+
tokensOutput: 0,
|
|
85
|
+
tokensTotal: 0,
|
|
86
|
+
}
|
|
72
87
|
acc.tokensInput += tokensInput
|
|
73
88
|
acc.tokensOutput += tokensOutput
|
|
74
|
-
|
|
89
|
+
acc.tokensTotal += tokensInput + tokensOutput
|
|
90
|
+
byAgent.set(key, acc)
|
|
75
91
|
} catch {
|
|
76
92
|
// Skip malformed rows — a partial ledger never breaks the report.
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
|
-
const result = [...byAgent.values()].sort((a, b) => b.
|
|
95
|
+
const result = [...byAgent.values()].sort((a, b) => b.tokensTotal - a.tokensTotal)
|
|
80
96
|
process.stdout.write(JSON.stringify({ ok: true, days, rows: result }))
|
|
81
97
|
} catch (err) {
|
|
82
98
|
fail(err instanceof Error ? err.message : String(err))
|