pantheon-opencode 1.5.0-beta.2 → 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 +41 -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/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 +516 -163
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/release-beta-version.mjs +61 -60
- package/scripts/sync-tui.mjs +5 -27
- package/scripts/test-opencode-v1-v2-sandbox.sh +79 -161
- 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 +516 -163
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/provider-capabilities.ts +221 -0
- package/src/pantheon/provider-stream-tracker.ts +182 -0
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
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).
|
|
@@ -95,7 +96,7 @@ and [changelog](CHANGELOG.md) for the latest changes.
|
|
|
95
96
|
[Beta2 agent economy policy](docs/ws3-token-opt-measurements.md#beta2-agent-economy-policy).
|
|
96
97
|
- A `--prompts` installer flag is planned for a future release.
|
|
97
98
|
|
|
98
|
-
## OpenCode V1/V2 — Dual Version (1.5.0)
|
|
99
|
+
## OpenCode V1/V2 — Dual Version (1.5.0-beta.2)
|
|
99
100
|
|
|
100
101
|
Pantheon has two **exclusive** OpenCode plugin contracts. Ordinary OpenCode
|
|
101
102
|
configuration may be shared, but the Pantheon plugin registration is selected
|
|
@@ -153,9 +154,21 @@ The installer still writes the compatibility settings required by the selected
|
|
|
153
154
|
OpenCode host, such as `experimental.subagent_depth`; this does not convert a
|
|
154
155
|
V1 plugin into V2 or provide V2 with V1 hooks.
|
|
155
156
|
|
|
156
|
-
##
|
|
157
|
+
## Releases
|
|
157
158
|
|
|
158
|
-
|
|
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.
|
|
159
172
|
|
|
160
173
|
## Sandbox validation (V1/V2)
|
|
161
174
|
|
|
@@ -164,9 +177,9 @@ package as a real user inside an isolated sandbox (own `HOME`, npm prefix and
|
|
|
164
177
|
venv) — never the dev environment. It checks OpenCode V1 (`opencode`) and V2
|
|
165
178
|
(`opencode2`) side by side: binaries, MCP connectivity, `doctor`, and — with
|
|
166
179
|
`--prompts` — a prompt battery covering the `pantheon://agents` resource,
|
|
167
|
-
memory store/recall, filesystem writes and agent delegation.
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
170
183
|
|
|
171
184
|
```bash
|
|
172
185
|
scripts/test-opencode-v1-v2-sandbox.sh --prepare # tarball + install + init in the sandbox
|
|
@@ -180,6 +193,9 @@ Modes are combinable (e.g. `--prepare --run v1 --prompts`). Binaries are
|
|
|
180
193
|
resolved strictly inside the sandbox npm prefix — a non-prepared sandbox fails
|
|
181
194
|
fast instead of silently testing the host installation.
|
|
182
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
|
+
|
|
183
199
|
Env overrides:
|
|
184
200
|
|
|
185
201
|
| Variable | Default | Purpose |
|
|
@@ -193,6 +209,15 @@ Env overrides:
|
|
|
193
209
|
Exit codes: `0` no real failures · `1` real failure (see `prompts-report.md`
|
|
194
210
|
in the sandbox root) · `2` usage error · `3` sandbox not prepared.
|
|
195
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
|
+
|
|
196
221
|
## Documentation
|
|
197
222
|
|
|
198
223
|
- [Installation](docs/INSTALLATION.md) · [Quick start](docs/QUICKSTART.md)
|
|
@@ -210,9 +235,10 @@ or pull request.
|
|
|
210
235
|
|
|
211
236
|
## Citation and DOI
|
|
212
237
|
|
|
213
|
-
Pantheon is released under the [MIT License](LICENSE). For the
|
|
214
|
-
record, use the [Zenodo DOI](https://doi.org/10.5281/zenodo.22306637);
|
|
215
|
-
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).
|
|
216
242
|
|
|
217
243
|
Canonical repository: <https://github.com/ils15/pantheon-opencode>
|
|
218
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",
|
|
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(
|
|
@@ -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
|
}
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { execSync } from 'node:child_process'
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
7
7
|
import { isAbsolute, join, resolve } from 'node:path'
|
|
8
|
-
import { warning } from './cli-ui.mjs'
|
|
9
8
|
import { ROOT, writeIfChanged } from './shared.mjs'
|
|
10
9
|
|
|
11
10
|
/**
|
|
@@ -95,31 +94,9 @@ export function installPlugin(srcDir, dstDir, { dryRun = false, clean = false }
|
|
|
95
94
|
result.skipped += copyResult.skipped
|
|
96
95
|
|
|
97
96
|
// Install plugin dependencies (@opentui/core, @opentui/solid, solid-js).
|
|
98
|
-
//
|
|
99
|
-
// fallback is opt-in because npm install may rewrite the dependency graph.
|
|
97
|
+
// Lockfile installation is the only accepted dependency path.
|
|
100
98
|
if (!dryRun) {
|
|
101
|
-
|
|
102
|
-
execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: dstDir, stdio: 'pipe' })
|
|
103
|
-
} catch (e) {
|
|
104
|
-
if (process.env.PANTHEON_ALLOW_NPM_INSTALL_FALLBACK === '1') {
|
|
105
|
-
warning(`npm ci failed; explicit fallback enabled: ${e.message}`)
|
|
106
|
-
try {
|
|
107
|
-
execSync('npm install --omit=dev --no-audit --no-fund --package-lock=false', {
|
|
108
|
-
cwd: dstDir,
|
|
109
|
-
stdio: 'pipe',
|
|
110
|
-
})
|
|
111
|
-
} catch (fallbackError) {
|
|
112
|
-
warning(`Failed to install TUI plugin dependencies: ${fallbackError.message}`)
|
|
113
|
-
result.errors++
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
warning(
|
|
117
|
-
`npm ci failed; dependencies were not changed. Set ` +
|
|
118
|
-
`PANTHEON_ALLOW_NPM_INSTALL_FALLBACK=1 to explicitly allow npm install: ${e.message}`,
|
|
119
|
-
)
|
|
120
|
-
result.errors++
|
|
121
|
-
}
|
|
122
|
-
}
|
|
99
|
+
execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: dstDir, stdio: 'pipe' })
|
|
123
100
|
}
|
|
124
101
|
|
|
125
102
|
return result
|
|
@@ -133,14 +110,7 @@ export function installPlugin(srcDir, dstDir, { dryRun = false, clean = false }
|
|
|
133
110
|
*/
|
|
134
111
|
export function installPluginDependencies(pluginDir, { dryRun = false } = {}) {
|
|
135
112
|
if (dryRun) return
|
|
136
|
-
|
|
137
|
-
execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: pluginDir, stdio: 'pipe' })
|
|
138
|
-
} catch (e) {
|
|
139
|
-
warning(
|
|
140
|
-
`npm ci failed; dependencies were not changed. Set ` +
|
|
141
|
-
`PANTHEON_ALLOW_NPM_INSTALL_FALLBACK=1 to explicitly allow npm install: ${e.message}`,
|
|
142
|
-
)
|
|
143
|
-
}
|
|
113
|
+
execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: pluginDir, stdio: 'pipe' })
|
|
144
114
|
}
|
|
145
115
|
|
|
146
116
|
/**
|