pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -15
- package/README.pt-BR.md +39 -12
- package/package.json +7 -3
- package/plugin.json +1 -1
- package/pyproject.toml +57 -0
- package/scripts/code_mode_server.py +101 -36
- package/scripts/cost.mjs +30 -14
- package/scripts/install/opencode.mjs +6 -0
- package/scripts/install/plugin.mjs +3 -33
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +518 -128
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/probe-context-rehydrate.mjs +240 -0
- package/scripts/probe-pantheon-cost.mjs +304 -0
- package/scripts/release-beta-version.mjs +61 -60
- package/scripts/sync-tui.mjs +5 -27
- package/scripts/test-opencode-v1-v2-sandbox.sh +367 -96
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +131 -88
- package/src/mcp/code_mode_server.py +101 -36
- package/src/mcp/mcp_persistence_server.py +518 -128
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/cost-command.ts +49 -32
- package/src/pantheon/delegate-manager.ts +285 -0
- package/src/pantheon/provider-capabilities.ts +221 -0
- package/src/pantheon/provider-stream-tracker.ts +182 -0
- package/src/pantheon/token-opt.ts +237 -11
- package/src/plugin-v2.ts +2 -14
- package/src/plugin.ts +3 -8
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +0 -11
- package/src/pantheon/delegate-task-adapter.ts +0 -444
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ want useful structure without giving up control of their code.
|
|
|
8
8
|
[Português (Brasil)](README.pt-BR.md) ·
|
|
9
9
|
[Repository](https://github.com/ils15/pantheon-opencode) · [MIT License](LICENSE)
|
|
10
10
|
|
|
11
|
-
[](https://github.com/ils15/pantheon-opencode/releases/
|
|
11
|
+
[](https://github.com/ils15/pantheon-opencode/releases/latest)
|
|
12
12
|
[](https://github.com/ils15/pantheon-opencode/actions)
|
|
13
13
|
[](https://doi.org/10.5281/zenodo.22306637)
|
|
14
14
|
|
|
@@ -70,13 +70,14 @@ clear handoff between stages of work.
|
|
|
70
70
|
|
|
71
71
|
## Status
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
Operational checkout version: **v1.5.0-beta.2** (candidate; publication is not
|
|
74
|
+
asserted here). Pantheon is designed for OpenCode and depends on the
|
|
75
|
+
availability and configuration of OpenCode and any optional services you choose
|
|
76
|
+
to use. Check the [releases](https://github.com/ils15/pantheon-opencode/releases)
|
|
77
|
+
and [changelog](CHANGELOG.md) for the latest published changes.
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
## What's new in 1.5.0
|
|
80
|
+
## What's new in 1.5.0-beta.2
|
|
80
81
|
|
|
81
82
|
- OpenCode-only installer: platform guides consolidated into a single
|
|
82
83
|
[OpenCode guide](docs/platforms/opencode.md).
|
|
@@ -90,9 +91,12 @@ and [changelog](CHANGELOG.md) for the latest changes.
|
|
|
90
91
|
- Sandbox validator for global installs (`scripts/test-opencode-v1-v2-sandbox.sh`)
|
|
91
92
|
covering OpenCode V1/V2 side by side — see
|
|
92
93
|
[Sandbox validation](#sandbox-validation-v1v2).
|
|
94
|
+
- Beta2 agent-economy policy: direct native delegation, bounded compaction
|
|
95
|
+
carry-forward, compact context encoding, and quality floors — see
|
|
96
|
+
[Beta2 agent economy policy](docs/ws3-token-opt-measurements.md#beta2-agent-economy-policy).
|
|
93
97
|
- A `--prompts` installer flag is planned for a future release.
|
|
94
98
|
|
|
95
|
-
## OpenCode V1/V2 — Dual Version (1.5.0)
|
|
99
|
+
## OpenCode V1/V2 — Dual Version (1.5.0-beta.2)
|
|
96
100
|
|
|
97
101
|
Pantheon has two **exclusive** OpenCode plugin contracts. Ordinary OpenCode
|
|
98
102
|
configuration may be shared, but the Pantheon plugin registration is selected
|
|
@@ -150,9 +154,21 @@ The installer still writes the compatibility settings required by the selected
|
|
|
150
154
|
OpenCode host, such as `experimental.subagent_depth`; this does not convert a
|
|
151
155
|
V1 plugin into V2 or provide V2 with V1 hooks.
|
|
152
156
|
|
|
153
|
-
##
|
|
157
|
+
## Releases
|
|
154
158
|
|
|
155
|
-
|
|
159
|
+
Publication is authorized **only** by an explicit `workflow_dispatch` of the
|
|
160
|
+
`Release` workflow (`release_channel` input selects beta or stable). PR labels,
|
|
161
|
+
pushes, merges, and tags never publish anything, and every validation gate is
|
|
162
|
+
fail-closed: only an explicit PASS authorizes release evidence. See
|
|
163
|
+
[docs/RELEASING.md](docs/RELEASING.md) for validation and recovery details.
|
|
164
|
+
|
|
165
|
+
Release validation keeps each manifest with its lockfile: the root
|
|
166
|
+
`package.json` + `package-lock.json` and the TUI
|
|
167
|
+
`src/plugins/tui/package.json` + `src/plugins/tui/package-lock.json`. Both use
|
|
168
|
+
`npm ci --ignore-scripts`; an `npm ci` failure blocks the run and there is no
|
|
169
|
+
`npm install` fallback. A release carries one `.tgz` tarball, computes the
|
|
170
|
+
SHA-256 of that same artifact, and binds the tarball and GitHub release to the
|
|
171
|
+
full `TARGET_SHA`; a second pack is not interchangeable.
|
|
156
172
|
|
|
157
173
|
## Sandbox validation (V1/V2)
|
|
158
174
|
|
|
@@ -161,9 +177,9 @@ package as a real user inside an isolated sandbox (own `HOME`, npm prefix and
|
|
|
161
177
|
venv) — never the dev environment. It checks OpenCode V1 (`opencode`) and V2
|
|
162
178
|
(`opencode2`) side by side: binaries, MCP connectivity, `doctor`, and — with
|
|
163
179
|
`--prompts` — a prompt battery covering the `pantheon://agents` resource,
|
|
164
|
-
memory store/recall, filesystem writes and agent delegation.
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
memory store/recall, filesystem writes and agent delegation. The gate is
|
|
181
|
+
fail-closed: every required check must return an explicit PASS; timeouts,
|
|
182
|
+
auth/network/provider failures and missing prerequisites block the run.
|
|
167
183
|
|
|
168
184
|
```bash
|
|
169
185
|
scripts/test-opencode-v1-v2-sandbox.sh --prepare # tarball + install + init in the sandbox
|
|
@@ -177,6 +193,9 @@ Modes are combinable (e.g. `--prepare --run v1 --prompts`). Binaries are
|
|
|
177
193
|
resolved strictly inside the sandbox npm prefix — a non-prepared sandbox fails
|
|
178
194
|
fast instead of silently testing the host installation.
|
|
179
195
|
|
|
196
|
+
This validates the prepared isolated sandbox only. A PASS is not proof of
|
|
197
|
+
support for every real host or for host configurations that were not exercised.
|
|
198
|
+
|
|
180
199
|
Env overrides:
|
|
181
200
|
|
|
182
201
|
| Variable | Default | Purpose |
|
|
@@ -190,6 +209,15 @@ Env overrides:
|
|
|
190
209
|
Exit codes: `0` no real failures · `1` real failure (see `prompts-report.md`
|
|
191
210
|
in the sandbox root) · `2` usage error · `3` sandbox not prepared.
|
|
192
211
|
|
|
212
|
+
### Intentional memory MCP divergence
|
|
213
|
+
|
|
214
|
+
`scripts/memory_mcp_server.py` and `src/mcp/memory_mcp_server.py` are
|
|
215
|
+
intentionally different. The standalone `scripts/` copy keeps the lightweight
|
|
216
|
+
`memory_*` contract; the installed `src/mcp/` copy additionally exposes the
|
|
217
|
+
optional codemap schema and `code_index`, `code_query`, and `code_neighbors`.
|
|
218
|
+
The other shared MCP copies remain identical. Do not overwrite one memory copy
|
|
219
|
+
with the other.
|
|
220
|
+
|
|
193
221
|
## Documentation
|
|
194
222
|
|
|
195
223
|
- [Installation](docs/INSTALLATION.md) · [Quick start](docs/QUICKSTART.md)
|
|
@@ -207,9 +235,10 @@ or pull request.
|
|
|
207
235
|
|
|
208
236
|
## Citation and DOI
|
|
209
237
|
|
|
210
|
-
Pantheon is released under the [MIT License](LICENSE). For the
|
|
211
|
-
record, use the [Zenodo DOI](https://doi.org/10.5281/zenodo.22306637);
|
|
212
|
-
metadata is also available
|
|
238
|
+
Pantheon is released under the [MIT License](LICENSE). For the historical
|
|
239
|
+
published v1.4.3 record only, use the [Zenodo DOI](https://doi.org/10.5281/zenodo.22306637);
|
|
240
|
+
it is not the current operational version. Citation metadata is also available
|
|
241
|
+
in [CITATION.cff](CITATION.cff).
|
|
213
242
|
|
|
214
243
|
Canonical repository: <https://github.com/ils15/pantheon-opencode>
|
|
215
244
|
|
package/README.pt-BR.md
CHANGED
|
@@ -8,7 +8,7 @@ e equipes que querem mais estrutura sem perder o controle do próprio código.
|
|
|
8
8
|
[English](README.md) ·
|
|
9
9
|
[Repositório](https://github.com/ils15/pantheon-opencode) · [Licença MIT](LICENSE)
|
|
10
10
|
|
|
11
|
-
[](https://github.com/ils15/pantheon-opencode/releases/
|
|
11
|
+
[](https://github.com/ils15/pantheon-opencode/releases/latest)
|
|
12
12
|
[](https://github.com/ils15/pantheon-opencode/actions)
|
|
13
13
|
[](https://doi.org/10.5281/zenodo.22306637)
|
|
14
14
|
|
|
@@ -71,13 +71,14 @@ verificações repetíveis e passagens claras entre etapas do trabalho.
|
|
|
71
71
|
|
|
72
72
|
## Status
|
|
73
73
|
|
|
74
|
-
Versão
|
|
74
|
+
Versão operacional neste checkout: **v1.5.0-beta.2** (candidata; esta página
|
|
75
|
+
não afirma publicação). O Pantheon foi feito para OpenCode e depende da
|
|
75
76
|
disponibilidade e da configuração do OpenCode e dos serviços opcionais que você
|
|
76
77
|
escolher. Veja as [releases](https://github.com/ils15/pantheon-opencode/releases)
|
|
77
|
-
e o [changelog](CHANGELOG.md) para acompanhar as mudanças.
|
|
78
|
+
e o [changelog](CHANGELOG.md) para acompanhar as mudanças publicadas.
|
|
78
79
|
|
|
79
80
|
|
|
80
|
-
## Novidades da 1.5.0
|
|
81
|
+
## Novidades da 1.5.0-beta.2
|
|
81
82
|
|
|
82
83
|
- Instalador exclusivo para OpenCode: guias de plataformas consolidados em um
|
|
83
84
|
único [guia OpenCode](docs/platforms/opencode.md).
|
|
@@ -93,7 +94,7 @@ e o [changelog](CHANGELOG.md) para acompanhar as mudanças.
|
|
|
93
94
|
lado — veja [Validação em sandbox](#validação-em-sandbox-v1v2).
|
|
94
95
|
- A flag `--prompts` do instalador está planejada para uma release futura.
|
|
95
96
|
|
|
96
|
-
## OpenCode V1/V2 — Versão dupla (1.5.0)
|
|
97
|
+
## OpenCode V1/V2 — Versão dupla (1.5.0-beta.2)
|
|
97
98
|
|
|
98
99
|
O Pantheon tem dois contratos de plugin OpenCode **exclusivos**. A configuração
|
|
99
100
|
comum do OpenCode pode ser compartilhada, mas o registro do plugin Pantheon é
|
|
@@ -153,9 +154,21 @@ O instalador continua gravando as configurações de compatibilidade exigidas
|
|
|
153
154
|
pelo host OpenCode selecionado, como `experimental.subagent_depth`; isso não
|
|
154
155
|
converte um plugin V1 em V2 nem dá hooks V1 ao V2.
|
|
155
156
|
|
|
156
|
-
## Releases
|
|
157
|
+
## Releases
|
|
157
158
|
|
|
158
|
-
|
|
159
|
+
A publicação é autorizada **somente** por um `workflow_dispatch` explícito do
|
|
160
|
+
workflow `Release` (o input `release_channel` escolhe beta ou stable). Labels
|
|
161
|
+
de PR, push, merge e tag nunca publicam nada, e todos os gates de validação são
|
|
162
|
+
fail-closed: somente um PASS explícito autoriza release evidence. Consulte
|
|
163
|
+
[docs/RELEASING.md](docs/RELEASING.md) para detalhes de validação e recuperação.
|
|
164
|
+
|
|
165
|
+
A validação mantém cada manifest junto do seu lockfile: o root
|
|
166
|
+
`package.json` + `package-lock.json` e o TUI
|
|
167
|
+
`src/plugins/tui/package.json` + `src/plugins/tui/package-lock.json`. Ambos usam
|
|
168
|
+
`npm ci --ignore-scripts`; uma falha em `npm ci` bloqueia a execução e não há
|
|
169
|
+
fallback para `npm install`. Uma release carrega um único tarball `.tgz`, calcula
|
|
170
|
+
o SHA-256 desse mesmo artefato e vincula o tarball e o GitHub Release ao
|
|
171
|
+
`TARGET_SHA` completo; um segundo pack não é intercambiável.
|
|
159
172
|
|
|
160
173
|
## Validação em sandbox (V1/V2)
|
|
161
174
|
|
|
@@ -165,8 +178,9 @@ prefix npm e venv próprios) — nunca o ambiente de desenvolvimento. Ele verifi
|
|
|
165
178
|
OpenCode V1 (`opencode`) e V2 (`opencode2`) lado a lado: binários, conectividade
|
|
166
179
|
MCP, `doctor` e — com `--prompts` — uma bateria de prompts cobrindo o recurso
|
|
167
180
|
`pantheon://agents`, memory store/recall, escrita no filesystem e delegação de
|
|
168
|
-
agente.
|
|
169
|
-
|
|
181
|
+
agente. O gate é fail-closed: todo check obrigatório precisa terminar em PASS
|
|
182
|
+
explícito; timeout, falha de auth/rede/provider e pré-requisitos ausentes
|
|
183
|
+
bloqueiam a execução.
|
|
170
184
|
|
|
171
185
|
```bash
|
|
172
186
|
scripts/test-opencode-v1-v2-sandbox.sh --prepare # tarball + install + init no sandbox
|
|
@@ -180,6 +194,9 @@ Os modos são combináveis (ex.: `--prepare --run v1 --prompts`). Os binários s
|
|
|
180
194
|
resolvidos estritamente dentro do prefix npm do sandbox — um sandbox não
|
|
181
195
|
preparado falha rápido em vez de testar silenciosamente a instalação do host.
|
|
182
196
|
|
|
197
|
+
Isso valida somente o sandbox isolado e preparado. Um PASS não prova suporte
|
|
198
|
+
para todo host real nem para configurações de host que não foram exercitadas.
|
|
199
|
+
|
|
183
200
|
Variáveis de ambiente:
|
|
184
201
|
|
|
185
202
|
| Variável | Padrão | Finalidade |
|
|
@@ -194,6 +211,15 @@ Códigos de saída: `0` sem falhas reais · `1` falha real (veja
|
|
|
194
211
|
`prompts-report.md` na raiz do sandbox) · `2` erro de uso · `3` sandbox não
|
|
195
212
|
preparado.
|
|
196
213
|
|
|
214
|
+
### Divergência intencional do memory MCP
|
|
215
|
+
|
|
216
|
+
`scripts/memory_mcp_server.py` e `src/mcp/memory_mcp_server.py` são
|
|
217
|
+
intencionalmente diferentes. A cópia independente em `scripts/` mantém o
|
|
218
|
+
contrato leve de `memory_*`; a cópia instalada em `src/mcp/` também expõe o
|
|
219
|
+
schema opcional de codemap e `code_index`, `code_query` e `code_neighbors`. As
|
|
220
|
+
outras cópias compartilhadas permanecem idênticas. Não sobrescreva uma cópia de
|
|
221
|
+
memória com a outra.
|
|
222
|
+
|
|
197
223
|
## Documentação
|
|
198
224
|
|
|
199
225
|
- [Instalação](docs/INSTALLATION.md) · [Início rápido](docs/QUICKSTART.md)
|
|
@@ -211,9 +237,10 @@ uma issue ou pull request.
|
|
|
211
237
|
|
|
212
238
|
## Citação e DOI
|
|
213
239
|
|
|
214
|
-
O Pantheon é distribuído sob a [Licença MIT](LICENSE). Para o registro
|
|
215
|
-
da v1.4.3, use o [DOI do Zenodo](https://doi.org/10.5281/zenodo.22306637);
|
|
216
|
-
metadados de citação também estão em
|
|
240
|
+
O Pantheon é distribuído sob a [Licença MIT](LICENSE). Para o registro histórico
|
|
241
|
+
publicado da v1.4.3 apenas, use o [DOI do Zenodo](https://doi.org/10.5281/zenodo.22306637);
|
|
242
|
+
essa não é a versão operacional atual. Os metadados de citação também estão em
|
|
243
|
+
[CITATION.cff](CITATION.cff).
|
|
217
244
|
|
|
218
245
|
Repositório canônico: <https://github.com/ils15/pantheon-opencode>
|
|
219
246
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.5.0-beta.
|
|
3
|
+
"version": "1.5.0-beta.3",
|
|
4
4
|
"description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"version:status": "node scripts/versioning.mjs status",
|
|
29
29
|
"version:apply": "node scripts/versioning.mjs apply",
|
|
30
30
|
"version:check": "node scripts/version-check.mjs",
|
|
31
|
-
"release:dry-run": "node scripts/versioning.mjs status && node scripts/release-notes.mjs &&
|
|
31
|
+
"release:dry-run": "node scripts/versioning.mjs status && node scripts/release-notes.mjs && node scripts/package-evidence.mjs",
|
|
32
|
+
"package:evidence": "node scripts/package-evidence.mjs",
|
|
33
|
+
"package:validate": "node scripts/validate-package.mjs",
|
|
32
34
|
"publish:next": "npm publish --tag next --access public",
|
|
33
35
|
"lint": "npx biome ci .",
|
|
34
36
|
"typecheck": "npx tsc --noEmit",
|
|
@@ -47,7 +49,9 @@
|
|
|
47
49
|
},
|
|
48
50
|
"files": [
|
|
49
51
|
"LICENSE",
|
|
52
|
+
"package-lock.json",
|
|
50
53
|
"plugin.json",
|
|
54
|
+
"pyproject.toml",
|
|
51
55
|
"scripts/install/*.mjs",
|
|
52
56
|
"scripts/*.py",
|
|
53
57
|
"src/plugin.ts",
|
|
@@ -84,7 +88,7 @@
|
|
|
84
88
|
],
|
|
85
89
|
"dependencies": {
|
|
86
90
|
"@opencode-ai/plugin": "1.18.18",
|
|
87
|
-
"js-yaml": "^4.3.
|
|
91
|
+
"js-yaml": "^4.3.2",
|
|
88
92
|
"zod": "4.1.8"
|
|
89
93
|
},
|
|
90
94
|
"devDependencies": {
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon",
|
|
3
3
|
"publisher": "ils15",
|
|
4
|
-
"version": "1.5.0-beta.
|
|
4
|
+
"version": "1.5.0-beta.3",
|
|
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
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.backends._legacy:_Backend"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pantheon"
|
|
7
|
+
version = "1.5.0-beta.3"
|
|
8
|
+
description = "Multi-agent orchestration framework"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[tool.pytest.ini_options]
|
|
13
|
+
testpaths = ["tests"]
|
|
14
|
+
asyncio_mode = "auto"
|
|
15
|
+
addopts = "-v --tb=short"
|
|
16
|
+
|
|
17
|
+
[tool.ruff]
|
|
18
|
+
line-length = 88
|
|
19
|
+
target-version = "py311"
|
|
20
|
+
|
|
21
|
+
[tool.ruff.lint]
|
|
22
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "PL", "RUF", "C901"]
|
|
23
|
+
ignore = ["E501"]
|
|
24
|
+
|
|
25
|
+
[tool.ruff.lint.per-file-ignores]
|
|
26
|
+
"__init__.py" = ["F401"]
|
|
27
|
+
# Legacy command-line scripts intentionally use hyphenated filenames.
|
|
28
|
+
"scripts/ci-validate-yaml.py" = ["N999"]
|
|
29
|
+
"scripts/scrub-secrets.py" = ["N999"]
|
|
30
|
+
# These modules defer optional/heavy imports until the corresponding command is used.
|
|
31
|
+
"scripts/vector_memory/*.py" = ["PLC0415"]
|
|
32
|
+
"tests/test_code_mode_args.py" = ["PLC0415"]
|
|
33
|
+
"tests/test_code_mode_sandbox.py" = ["PLC0415", "N806"]
|
|
34
|
+
"tests/test_code_mode_server.py" = ["PLC0415"]
|
|
35
|
+
"tests/test_eval_store.py" = ["PLC0415", "PLR2004"]
|
|
36
|
+
"tests/test_memory_mcp_server.py" = ["PLC0415", "PLR2004"]
|
|
37
|
+
# Existing public APIs and test fixtures are intentionally broad/simple; refactoring
|
|
38
|
+
# them would be behavior-affecting rather than a mechanical lint correction.
|
|
39
|
+
"scripts/code_mode_server.py" = ["PLR0913"]
|
|
40
|
+
"src/mcp/code_mode_server.py" = ["PLR0913"]
|
|
41
|
+
"scripts/mcp_persistence_server.py" = ["PLR0913"]
|
|
42
|
+
"src/mcp/mcp_persistence_server.py" = ["PLR0913"]
|
|
43
|
+
"scripts/memory_mcp_server.py" = ["C901"]
|
|
44
|
+
"scripts/vector_memory/index.py" = ["C901", "PLR0913", "PLR2004", "PLW0603", "PLW2901"]
|
|
45
|
+
"scripts/vector_memory/cli.py" = ["PLR2004"]
|
|
46
|
+
"scripts/vector_memory/query.py" = ["PLR0913"]
|
|
47
|
+
"src/mcp/eval_store.py" = ["PLR2004"]
|
|
48
|
+
"tests/test_code_mode_frontmatter.py" = ["PLR2004"]
|
|
49
|
+
"tests/test_eval_judge.py" = ["PLR2004"]
|
|
50
|
+
"tests/test_eval_static.py" = ["PLR2004"]
|
|
51
|
+
"tests/test_mcp_persistence_server.py" = ["PLR2004"]
|
|
52
|
+
|
|
53
|
+
[tool.mypy]
|
|
54
|
+
python_version = "3.11"
|
|
55
|
+
strict = true
|
|
56
|
+
ignore_missing_imports = true
|
|
57
|
+
disallow_untyped_defs = true
|
|
@@ -41,18 +41,20 @@ MAX_SCRIPT_TIMEOUT: int = 300
|
|
|
41
41
|
# ── Env Allowlist ──────────────────────────────────────────────────────────────
|
|
42
42
|
# Only these variables are passed to script subprocesses. Everything else
|
|
43
43
|
# (secrets, cloud credentials, etc.) is stripped to limit blast radius.
|
|
44
|
-
_CODE_MODE_ENV_ALLOWLIST: frozenset[str] = frozenset(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
_CODE_MODE_ENV_ALLOWLIST: frozenset[str] = frozenset(
|
|
45
|
+
{
|
|
46
|
+
"PATH",
|
|
47
|
+
"HOME",
|
|
48
|
+
"LANG",
|
|
49
|
+
"PANTHEON_HOME",
|
|
50
|
+
"PANTHEON_PROJECT",
|
|
51
|
+
"OPENAI_API_KEY",
|
|
52
|
+
"OPENAI_BASE_URL",
|
|
53
|
+
"EVAL_JUDGE_MODEL",
|
|
54
|
+
"SHELL",
|
|
55
|
+
"USER",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
56
58
|
|
|
57
59
|
# Optional extra vars that callers may opt-in to pass through. Populate
|
|
58
60
|
# this set at startup if needed (e.g. ``CODE_MODE_EXTRA_ENV.add("MY_VAR")``).
|
|
@@ -127,6 +129,7 @@ def _detect_prlimit() -> str | None:
|
|
|
127
129
|
return None
|
|
128
130
|
return shutil.which("prlimit")
|
|
129
131
|
|
|
132
|
+
|
|
130
133
|
# ── Scripts Directory Resolution ─────────────────────────────────────────────
|
|
131
134
|
# Priority:
|
|
132
135
|
# 1. /.opencode/.pantheon/code-mode/ (project install)
|
|
@@ -136,6 +139,49 @@ def _detect_prlimit() -> str | None:
|
|
|
136
139
|
# fallback — package.json `files` includes .pantheon/code-mode/**)
|
|
137
140
|
|
|
138
141
|
|
|
142
|
+
def _is_within(path: Path, root: Path) -> bool:
|
|
143
|
+
"""Return whether a resolved *path* is contained by a resolved *root*."""
|
|
144
|
+
try:
|
|
145
|
+
return os.path.commonpath((str(root), str(path))) == str(root)
|
|
146
|
+
except (OSError, ValueError):
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _has_usable_scripts(scripts_dir: Path) -> bool:
|
|
151
|
+
"""Return whether *scripts_dir* contains an executable code-mode target.
|
|
152
|
+
|
|
153
|
+
A directory is only a valid resolver candidate when it contains a regular
|
|
154
|
+
``.py`` or ``.sh`` file that can pass the same direct-child and path
|
|
155
|
+
containment rules used by script execution. This prevents an empty
|
|
156
|
+
project overlay (or a directory containing only metadata) from masking a
|
|
157
|
+
lower-priority installation.
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
if scripts_dir.is_symlink():
|
|
161
|
+
return False
|
|
162
|
+
resolved_dir = scripts_dir.resolve(strict=True)
|
|
163
|
+
if not resolved_dir.is_dir():
|
|
164
|
+
return False
|
|
165
|
+
for script_path in scripts_dir.iterdir():
|
|
166
|
+
if script_path.name.startswith("."):
|
|
167
|
+
continue
|
|
168
|
+
if script_path.suffix.lower() not in ALLOWED_EXTENSIONS:
|
|
169
|
+
continue
|
|
170
|
+
try:
|
|
171
|
+
resolved_script = script_path.resolve(strict=True)
|
|
172
|
+
except (OSError, RuntimeError):
|
|
173
|
+
continue
|
|
174
|
+
if (
|
|
175
|
+
resolved_script.is_file()
|
|
176
|
+
and resolved_script.parent == resolved_dir
|
|
177
|
+
and _is_within(resolved_script, resolved_dir)
|
|
178
|
+
):
|
|
179
|
+
return True
|
|
180
|
+
except (OSError, RuntimeError):
|
|
181
|
+
return False
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
|
|
139
185
|
def _packaged_scripts_dir() -> Path | None:
|
|
140
186
|
"""Return the code-mode dir shipped inside the installed package, if any.
|
|
141
187
|
|
|
@@ -144,22 +190,27 @@ def _packaged_scripts_dir() -> Path | None:
|
|
|
144
190
|
Returns None when no packaged copy exists.
|
|
145
191
|
"""
|
|
146
192
|
try:
|
|
147
|
-
here = Path(__file__).resolve()
|
|
148
|
-
except
|
|
193
|
+
here = Path(__file__).resolve(strict=True)
|
|
194
|
+
except (OSError, RuntimeError):
|
|
149
195
|
return None
|
|
150
196
|
for parent in [here.parent, *here.parents]:
|
|
151
197
|
try:
|
|
152
198
|
candidate = parent / ".pantheon" / "code-mode"
|
|
153
199
|
except Exception:
|
|
154
200
|
continue
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return candidate
|
|
158
|
-
except OSError:
|
|
159
|
-
continue
|
|
201
|
+
if _has_usable_scripts(candidate):
|
|
202
|
+
return candidate
|
|
160
203
|
return None
|
|
161
204
|
|
|
162
205
|
|
|
206
|
+
def _resolve_scripts_dir(candidates: list[Path]) -> Path | None:
|
|
207
|
+
"""Select the first usable candidate, then try the packaged fallback."""
|
|
208
|
+
for candidate in candidates:
|
|
209
|
+
if _has_usable_scripts(candidate):
|
|
210
|
+
return candidate
|
|
211
|
+
return _packaged_scripts_dir()
|
|
212
|
+
|
|
213
|
+
|
|
163
214
|
_PANTHEON_HOME: Path = pantheon_home()
|
|
164
215
|
_SCRIPTS_DIR_CANDIDATES: list[Path] = []
|
|
165
216
|
_proj = pantheon_project()
|
|
@@ -168,15 +219,9 @@ if _proj is not None:
|
|
|
168
219
|
_SCRIPTS_DIR_CANDIDATES.append(_proj / ".pantheon" / "code-mode")
|
|
169
220
|
_SCRIPTS_DIR_CANDIDATES.append(_PANTHEON_HOME / ".pantheon" / "code-mode")
|
|
170
221
|
|
|
171
|
-
SCRIPTS_DIR: Path =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
SCRIPTS_DIR = _candidate
|
|
175
|
-
break
|
|
176
|
-
else:
|
|
177
|
-
_packaged = _packaged_scripts_dir()
|
|
178
|
-
if _packaged is not None:
|
|
179
|
-
SCRIPTS_DIR = _packaged
|
|
222
|
+
SCRIPTS_DIR: Path = _resolve_scripts_dir(_SCRIPTS_DIR_CANDIDATES) or (
|
|
223
|
+
_PANTHEON_HOME / ".pantheon" / "code-mode"
|
|
224
|
+
)
|
|
180
225
|
|
|
181
226
|
# ── FastMCP App ───────────────────────────────────────────────────────────────
|
|
182
227
|
mcp = FastMCP(
|
|
@@ -208,11 +253,21 @@ def _validate_script_name(script_name: str) -> Path:
|
|
|
208
253
|
allowed = ", ".join(sorted(ALLOWED_EXTENSIONS))
|
|
209
254
|
raise ValueError(f"Extension '{ext}' not allowed. Allowed: {allowed}")
|
|
210
255
|
|
|
211
|
-
|
|
212
|
-
|
|
256
|
+
if SCRIPTS_DIR.is_symlink():
|
|
257
|
+
raise ValueError(f"Invalid script name: '{script_name}'")
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
scripts_root = SCRIPTS_DIR.resolve(strict=True)
|
|
261
|
+
script_path = (SCRIPTS_DIR / name).resolve(strict=True)
|
|
262
|
+
except (FileNotFoundError, OSError, RuntimeError):
|
|
263
|
+
raise ValueError(f"Script '{script_name}' not found") from None
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
not _is_within(script_path, scripts_root)
|
|
267
|
+
or script_path.parent != scripts_root
|
|
268
|
+
or not script_path.is_file()
|
|
269
|
+
):
|
|
213
270
|
raise ValueError(f"Invalid script name: '{script_name}'")
|
|
214
|
-
if not script_path.exists():
|
|
215
|
-
raise ValueError(f"Script '{script_name}' not found")
|
|
216
271
|
|
|
217
272
|
return script_path
|
|
218
273
|
|
|
@@ -286,7 +341,9 @@ def _parse_frontmatter(script_path: Path) -> dict[str, Any]:
|
|
|
286
341
|
break
|
|
287
342
|
if not stripped.startswith("#"):
|
|
288
343
|
return {} # non-comment line inside the block → not frontmatter
|
|
289
|
-
body_lines.append(
|
|
344
|
+
body_lines.append(
|
|
345
|
+
stripped[1:].lstrip() if stripped.startswith("# ") else stripped[1:]
|
|
346
|
+
)
|
|
290
347
|
else:
|
|
291
348
|
return {} # no closing delimiter
|
|
292
349
|
|
|
@@ -378,7 +435,9 @@ async def list_code_mode_scripts() -> str:
|
|
|
378
435
|
|
|
379
436
|
@mcp.resource(
|
|
380
437
|
"pantheon://code-mode/scripts/{script_name}",
|
|
381
|
-
description=
|
|
438
|
+
description=(
|
|
439
|
+
"Content of a code-mode script by name, with parsed frontmatter metadata"
|
|
440
|
+
),
|
|
382
441
|
)
|
|
383
442
|
async def get_code_mode_script(script_name: str) -> str:
|
|
384
443
|
"""Return the source content of a code-mode script plus its metadata."""
|
|
@@ -486,8 +545,14 @@ async def execute_code_script(
|
|
|
486
545
|
await proc.wait()
|
|
487
546
|
duration_ms = int((time.monotonic() - started) * 1000)
|
|
488
547
|
return _build_result(
|
|
489
|
-
"",
|
|
490
|
-
|
|
548
|
+
"",
|
|
549
|
+
"",
|
|
550
|
+
-1,
|
|
551
|
+
timed_out=True,
|
|
552
|
+
duration_ms=duration_ms,
|
|
553
|
+
metadata=metadata,
|
|
554
|
+
json_output=json_output,
|
|
555
|
+
timeout_s=timeout_s,
|
|
491
556
|
)
|
|
492
557
|
except FileNotFoundError:
|
|
493
558
|
return _build_result(
|
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))
|
|
@@ -1207,4 +1207,10 @@ export async function installOpenCode(
|
|
|
1207
1207
|
const { writeActivePreset } = await import('./model-picker.mjs')
|
|
1208
1208
|
writeActivePreset(target, opts.preset, { source: 'cli' })
|
|
1209
1209
|
}
|
|
1210
|
+
|
|
1211
|
+
// Fail-closed: accumulated stats.errors (bad sources, missing requirements,
|
|
1212
|
+
// failed health checks) must never masquerade as a successful installation.
|
|
1213
|
+
if (stats.errors > 0) {
|
|
1214
|
+
throw new Error(`installation completed with ${stats.errors} error(s); see output above`)
|
|
1215
|
+
}
|
|
1210
1216
|
}
|