pantheon-opencode 1.5.0-beta.9 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -63
- package/CHANGELOG.md +326 -0
- package/README.md +77 -29
- package/README.pt-BR.md +5 -6
- package/commands/pantheon.md +1 -35
- package/package.json +2 -3
- package/plugin.json +1 -1
- package/pyproject.toml +3 -5
- package/scripts/doctor.mjs +260 -5
- package/scripts/install/plugin.mjs +9 -12
- package/scripts/mcp_persistence_server.py +1 -1
- package/scripts/sync-tui.mjs +8 -11
- package/src/agents/apollo.md +1 -0
- package/src/agents/gaia.md +1 -0
- package/src/agents/mnemosyne.md +11 -1
- package/src/agents/zeus.md +10 -52
- package/src/instructions/memory-protocol.instructions.md +6 -3
- package/src/instructions/zeus-anti-stall.instructions.md +2 -4
- package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
- package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
- package/src/mcp/mcp_persistence_server.py +1 -1
- package/src/mcp/memory_mcp_server.py +81 -27
- package/src/pantheon/background-job-board.ts +4 -4
- package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
- package/src/pantheon/cost-command.ts +1 -2
- package/src/pantheon/delegation-enforce.ts +59 -14
- package/src/pantheon/goal-loop.ts +1 -1
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/idle-continuation.ts +4 -6
- package/src/pantheon/model-command.ts +1 -2
- package/src/pantheon/native-task-status.ts +2 -2
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/pantheon/session-guard.ts +2 -2
- package/src/pantheon/session-id.ts +30 -0
- package/src/pantheon/task-result-guard.ts +2 -3
- package/src/pantheon/todo-enforcer.ts +3 -3
- package/src/pantheon/tool-context.ts +23 -0
- package/src/pantheon/v2-bridge.ts +1 -4
- package/src/plugin-v2.ts +8 -21
- package/src/plugin.ts +61 -245
- package/src/plugins/pantheon-hooks.ts +2 -2
- package/src/plugins/tui/dist/tui.d.ts +229 -103
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +495 -333
- package/src/plugins/tui/package-lock.json +5 -2
- package/src/plugins/tui/package.json +5 -2
- package/src/plugins/tui/src/index.tsx +710 -409
- package/src/routing.yml +0 -16
- package/src/skills/context-compression/SKILL.md +1 -1
- package/scripts/generate-preset-docs.mjs +0 -300
- package/scripts/generate-prompts.sh +0 -222
- package/scripts/hash_verify.py +0 -192
- package/scripts/install/component.mjs +0 -24
- package/scripts/install/migrate-config.mjs +0 -302
- package/scripts/test-context-compression.sh +0 -169
- package/src/mcp/toon_codec.py +0 -491
- package/src/pantheon/auto-wake.ts +0 -85
- package/src/pantheon/chat-reminders.ts +0 -9
- package/src/pantheon/comment-checker.ts +0 -48
- package/src/pantheon/compaction-assert.ts +0 -160
- package/src/pantheon/compaction-handoff.ts +0 -304
- package/src/pantheon/cost-tracker.ts +0 -108
- package/src/pantheon/delegate-manager.ts +0 -766
- package/src/pantheon/delegation-classifier.ts +0 -307
- package/src/pantheon/delegation-finalize.ts +0 -406
- package/src/pantheon/delegation-notify.ts +0 -212
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1337
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/native-probe.ts +0 -234
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/preemptive-compact.ts +0 -76
- package/src/pantheon/provider-capabilities.ts +0 -221
- package/src/pantheon/provider-stream-tracker.ts +0 -182
- package/src/pantheon/retry-policy.ts +0 -212
- package/src/pantheon/step-cap.ts +0 -110
- package/src/pantheon/token-opt.ts +0 -689
- package/src/pantheon/toon-codec.ts +0 -481
- package/src/pantheon/v2-events.ts +0 -148
- package/src/pantheon/v2-hooks.ts +0 -287
- package/src/pantheon/v2-tool-definitions.ts +0 -331
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
- package/src/plugins/tui/dist/tui.tsx +0 -2853
package/README.pt-BR.md
CHANGED
|
@@ -31,7 +31,7 @@ de planejar o trabalho, avançar, conferir resultados e preservar o contexto
|
|
|
31
31
|
|
|
32
32
|
## Comece em 2 minutos
|
|
33
33
|
|
|
34
|
-
Requisitos: [OpenCode 1.18.4+](https://opencode.ai/docs/) e Node.js
|
|
34
|
+
Requisitos: [OpenCode 1.18.4+](https://opencode.ai/docs/) e Node.js 22+.
|
|
35
35
|
|
|
36
36
|
No projeto em que você quer usar o Pantheon:
|
|
37
37
|
|
|
@@ -130,16 +130,15 @@ registrados juntos.
|
|
|
130
130
|
|---|---|---|
|
|
131
131
|
| Chave de config do OpenCode | `plugin` singular | `plugins` plural |
|
|
132
132
|
| Registro Pantheon | `src/plugin.ts` mais `src/plugins/pantheon-hooks.ts` | `pantheon-opencode/plugin-v2` (`src/plugin-v2.ts`) |
|
|
133
|
-
| Contrato de runtime | Plugin Pantheon
|
|
133
|
+
| Contrato de runtime | Plugin Pantheon V1: 6 ferramentas (`hashline_edit`, as 3 ferramentas de goal, `pantheon_cost`, `pantheon_model`), hooks de evento/ferramenta e tratamento de compactação V1 | Plugin V2 completo: 6 ferramentas de orquestração, 4 assinaturas de eventos, session hooks (`prompt`, `context`), tool hooks (`execute.before`/`after`), além de transforms de configuração |
|
|
134
134
|
| APIs V1 | Registradas | Definições próprias de ferramentas via `ctx.tool.transform()` — não pelo caminho do plugin V1 |
|
|
135
135
|
|
|
136
|
-
O plugin V2 fornece
|
|
137
|
-
`pantheon_delegation_read`, `pantheon_delegation_list`, `hashline_edit`,
|
|
136
|
+
O plugin V2 fornece 6 ferramentas de orquestração (`hashline_edit`,
|
|
138
137
|
`pantheon_goal_create`, `pantheon_goal_get`, `pantheon_goal_update`,
|
|
139
138
|
`pantheon_cost`, `pantheon_model`), 4 assinaturas de eventos (`session.created`,
|
|
140
139
|
`session.idle`, `session.error`, `session.compacted`), session hooks (`prompt`,
|
|
141
140
|
`context`) e tool hooks (`execute.before`, `execute.after`). O único recurso V2
|
|
142
|
-
sem suporte é `legacy-hooks` (a superfície de
|
|
141
|
+
sem suporte é `legacy-hooks` (a superfície de hooks específica do V1).
|
|
143
142
|
|
|
144
143
|
O pacote expõe os dois contratos como exports importáveis:
|
|
145
144
|
`pantheon-opencode/plugin` (V1), `pantheon-opencode/plugin-v2` (V2) e
|
|
@@ -147,7 +146,7 @@ O pacote expõe os dois contratos como exports importáveis:
|
|
|
147
146
|
explicitamente o contrato desejado.
|
|
148
147
|
|
|
149
148
|
A ponte V1→V2 (`src/pantheon/v2-bridge.ts`) habilita interop opcional:
|
|
150
|
-
singletons de infraestrutura V1 (BackgroundJobBoard,
|
|
149
|
+
singletons de infraestrutura V1 (BackgroundJobBoard,
|
|
151
150
|
GoalStore, TodoEnforcer, VisionHandler) são repassados via `ctx.options` do V2.
|
|
152
151
|
A ponte é opcional — o V2 funciona standalone com degradação graciosa.
|
|
153
152
|
|
package/commands/pantheon.md
CHANGED
|
@@ -9,41 +9,7 @@ agent: zeus
|
|
|
9
9
|
**When:** Architecture trade-offs, technology selection, security assessments, multi-stakeholder concerns
|
|
10
10
|
**Returns:** Structured synthesis with recommendation, confidence level, resolved divergences
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Choose 2-3 agents based on domain:
|
|
15
|
-
|
|
16
|
-
| Domain | Specialists |
|
|
17
|
-
|--------|-------------|
|
|
18
|
-
| Architecture | hermes, demeter, themis, athena |
|
|
19
|
-
| Security | themis, hermes, prometheus, nyx |
|
|
20
|
-
| Database | demeter, hermes, prometheus |
|
|
21
|
-
| AI/RAG | hephaestus, nyx |
|
|
22
|
-
| Infrastructure | prometheus, hermes, themis |
|
|
23
|
-
| Frontend/UX | aphrodite, themis, hermes |
|
|
24
|
-
| Observability | nyx, hermes |
|
|
25
|
-
| General | athena, themis, hermes |
|
|
26
|
-
|
|
27
|
-
Send ALL `task()` calls in a single message. Each must return: Recommendation · Reasoning · Trade-offs · Risks · Confidence.
|
|
28
|
-
|
|
29
|
-
## Synthesis Structure
|
|
30
|
-
|
|
31
|
-
After ALL responses arrive, produce EXACTLY this structure:
|
|
32
|
-
|
|
33
|
-
### 📋 Individual Assessments
|
|
34
|
-
For each specialist: Recommendation, Reasoning, Trade-offs, Risks, Confidence
|
|
35
|
-
|
|
36
|
-
### ✅ Agreements
|
|
37
|
-
Where 2+ specialists converge
|
|
38
|
-
|
|
39
|
-
### ⚡ Divergences
|
|
40
|
-
Where they disagree — resolve each one with reasoning
|
|
41
|
-
|
|
42
|
-
### 🔥 Total Divergence
|
|
43
|
-
If ALL specialists disagree on a point, call this out explicitly
|
|
44
|
-
|
|
45
|
-
### 🏆 Recommendation
|
|
46
|
-
Decisive conclusion with confidence level
|
|
12
|
+
Dispatch, specialist selection, and synthesis follow the canonical `## Zeus Council Synthesis` protocol (Steps 0–9: precedent fast-path, dispatch, confidence cross-validation, rebuttal, tie-break, Themis audit, persist). Send ALL `task()` calls in a single message; each specialist returns the `## specialist_response` structure from `## Agent Return Format`.
|
|
47
13
|
|
|
48
14
|
## Question:
|
|
49
15
|
$ARGUMENTS
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"release:dry-run": "node scripts/versioning.mjs status && node scripts/release-notes.mjs && node scripts/package-evidence.mjs",
|
|
32
32
|
"package:evidence": "node scripts/package-evidence.mjs",
|
|
33
33
|
"package:validate": "node scripts/validate-package.mjs",
|
|
34
|
-
"publish:next": "npm publish --tag next --access public",
|
|
35
34
|
"lint": "biome ci .",
|
|
36
35
|
"typecheck": "tsc --noEmit",
|
|
37
36
|
"audit": "npm audit --audit-level=high",
|
|
@@ -120,7 +119,7 @@
|
|
|
120
119
|
"author": "ils15",
|
|
121
120
|
"license": "MIT",
|
|
122
121
|
"engines": {
|
|
123
|
-
"node": ">=
|
|
122
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
124
123
|
},
|
|
125
124
|
"preferGlobal": true
|
|
126
125
|
}
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon",
|
|
3
3
|
"publisher": "ils15",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.1",
|
|
5
5
|
"displayName": "Pantheon",
|
|
6
6
|
"description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
|
|
7
7
|
"agents": "./src/agents",
|
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pantheon"
|
|
7
|
-
version = "1.5.
|
|
7
|
+
version = "1.5.1"
|
|
8
8
|
description = "Multi-agent orchestration framework"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = []
|
|
@@ -37,10 +37,9 @@ ignore = ["E501"]
|
|
|
37
37
|
"scripts/scrub-secrets.py" = ["N999"]
|
|
38
38
|
# These modules defer optional/heavy imports until the corresponding command is used.
|
|
39
39
|
"scripts/vector_memory/*.py" = ["PLC0415"]
|
|
40
|
-
"tests/test_code_mode_args.py" = ["PLC0415"]
|
|
41
40
|
"tests/test_code_mode_sandbox.py" = ["PLC0415", "N806"]
|
|
42
41
|
"tests/test_code_mode_server.py" = ["PLC0415"]
|
|
43
|
-
"tests/
|
|
42
|
+
"tests/test_codemap_mcp.py" = ["PLC0415"]
|
|
44
43
|
"tests/test_memory_mcp_server.py" = ["PLC0415", "PLR2004"]
|
|
45
44
|
# Existing public APIs and test fixtures are intentionally broad/simple; refactoring
|
|
46
45
|
# them would be behavior-affecting rather than a mechanical lint correction.
|
|
@@ -54,8 +53,7 @@ ignore = ["E501"]
|
|
|
54
53
|
"scripts/vector_memory/query.py" = ["PLR0913"]
|
|
55
54
|
"src/mcp/eval_store.py" = ["PLR2004"]
|
|
56
55
|
"tests/test_code_mode_frontmatter.py" = ["PLR2004"]
|
|
57
|
-
"tests/
|
|
58
|
-
"tests/test_eval_static.py" = ["PLR2004"]
|
|
56
|
+
"tests/test_eval.py" = ["PLR2004"]
|
|
59
57
|
"tests/test_mcp_persistence_server.py" = ["PLR2004"]
|
|
60
58
|
|
|
61
59
|
[tool.mypy]
|
package/scripts/doctor.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - Permission/frontmatter mismatches
|
|
10
10
|
* - OpenCode artifact freshness
|
|
11
11
|
* - Git status
|
|
12
|
+
* - Node runtime (node:sqlite availability for pantheon_cost)
|
|
12
13
|
*
|
|
13
14
|
* Usage:
|
|
14
15
|
* node scripts/doctor.mjs # auto-detect, cwd
|
|
@@ -23,9 +24,9 @@
|
|
|
23
24
|
|
|
24
25
|
import { spawn as spawnAsync, spawnSync } from 'node:child_process'
|
|
25
26
|
import { createHash } from 'node:crypto'
|
|
26
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
27
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
27
28
|
import { homedir } from 'node:os'
|
|
28
|
-
import { basename, dirname, join, resolve } from 'node:path'
|
|
29
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
29
30
|
import { fileURLToPath } from 'node:url'
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
@@ -159,6 +160,8 @@ function showHelp() {
|
|
|
159
160
|
D. OpenCode Status — OpenCode artifact layout
|
|
160
161
|
E. Git Status — uncommitted changes
|
|
161
162
|
F. Runtime Layer — venv python + pinned pip dependencies
|
|
163
|
+
H3. Plugin Drift — registered plugin version vs this package
|
|
164
|
+
K. Node Runtime — node:sqlite availability for pantheon_cost
|
|
162
165
|
|
|
163
166
|
Exit codes:
|
|
164
167
|
0 — no blocking errors (warnings are advisory)
|
|
@@ -959,6 +962,35 @@ function checkVenvLayer(args) {
|
|
|
959
962
|
// Check F.2: Code-Mode Scripts Directory
|
|
960
963
|
// ---------------------------------------------------------------------------
|
|
961
964
|
|
|
965
|
+
/**
|
|
966
|
+
* Return whether *dir* contains an executable code-mode target.
|
|
967
|
+
*
|
|
968
|
+
* Mirrors `_has_usable_scripts` in `scripts/code_mode_server.py`: a directory
|
|
969
|
+
* is only a valid resolver candidate when it is a real (non-symlink) directory
|
|
970
|
+
* containing a regular `.py`/`.sh` direct child. This prevents an empty project
|
|
971
|
+
* overlay from masking a lower-priority installation that actually ships
|
|
972
|
+
* scripts. Keep both implementations in sync (see tests/test_doctor_layers.mjs).
|
|
973
|
+
*/
|
|
974
|
+
function hasUsableScripts(dir) {
|
|
975
|
+
try {
|
|
976
|
+
if (!existsSync(dir)) return false
|
|
977
|
+
const stat = lstatSync(dir)
|
|
978
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return false
|
|
979
|
+
return readdirSync(dir).some((name) => {
|
|
980
|
+
if (name.startsWith('.')) return false
|
|
981
|
+
const lower = name.toLowerCase()
|
|
982
|
+
if (!lower.endsWith('.py') && !lower.endsWith('.sh')) return false
|
|
983
|
+
try {
|
|
984
|
+
return statSync(join(dir, name)).isFile()
|
|
985
|
+
} catch {
|
|
986
|
+
return false
|
|
987
|
+
}
|
|
988
|
+
})
|
|
989
|
+
} catch {
|
|
990
|
+
return false
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
962
994
|
/**
|
|
963
995
|
* Verify the code-mode scripts directory exists for the resolved runtime
|
|
964
996
|
* root (project → <target>/.opencode, otherwise the global config dir).
|
|
@@ -977,7 +1009,7 @@ export function checkCodeModeDir(args) {
|
|
|
977
1009
|
// install (.opencode present) gets its runtime dir created when missing.
|
|
978
1010
|
// Only targets with no project layout fall back to the global config dir.
|
|
979
1011
|
const codeModeDir =
|
|
980
|
-
projectCandidates.find(
|
|
1012
|
+
projectCandidates.find(hasUsableScripts) ??
|
|
981
1013
|
(existsSync(join(target, '.opencode'))
|
|
982
1014
|
? projectCandidates[0]
|
|
983
1015
|
: join(resolveOpenCodeConfigDir(env), '.pantheon', 'code-mode'))
|
|
@@ -1003,7 +1035,7 @@ export function resolveCodeModeDir(args) {
|
|
|
1003
1035
|
join(target, '.opencode', '.pantheon', 'code-mode'),
|
|
1004
1036
|
join(target, '.pantheon', 'code-mode'),
|
|
1005
1037
|
]
|
|
1006
|
-
const projectDir = projectCandidates.find(
|
|
1038
|
+
const projectDir = projectCandidates.find(hasUsableScripts)
|
|
1007
1039
|
if (projectDir) return projectDir
|
|
1008
1040
|
return join(resolveOpenCodeConfigDir(env), '.pantheon', 'code-mode')
|
|
1009
1041
|
}
|
|
@@ -1132,7 +1164,10 @@ function comparePantheonVersions(a, b) {
|
|
|
1132
1164
|
for (let i = 0; i < 3; i++) {
|
|
1133
1165
|
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
|
|
1134
1166
|
}
|
|
1135
|
-
|
|
1167
|
+
// Infinity - Infinity is NaN, which would make equal stable versions compare
|
|
1168
|
+
// as drift for any caller using a strict === 0 check.
|
|
1169
|
+
if (pa.beta === pb.beta) return 0
|
|
1170
|
+
return pa.beta < pb.beta ? -1 : 1
|
|
1136
1171
|
}
|
|
1137
1172
|
|
|
1138
1173
|
/**
|
|
@@ -1185,6 +1220,159 @@ export function checkInstallVersionDrift(args) {
|
|
|
1185
1220
|
return 'sync'
|
|
1186
1221
|
}
|
|
1187
1222
|
|
|
1223
|
+
// ---------------------------------------------------------------------------
|
|
1224
|
+
// Check H3: Plugin version drift (issue #158)
|
|
1225
|
+
// ---------------------------------------------------------------------------
|
|
1226
|
+
// npm installs are lockfile-authoritative: a package-lock.json pinned to an
|
|
1227
|
+
// older pantheon-opencode (e.g. 1.4.1 satisfying ^1.4.1) is never re-resolved,
|
|
1228
|
+
// so an installed copy under node_modules/pantheon-opencode can lag several
|
|
1229
|
+
// releases behind the running package. The opencode.json keeps registering
|
|
1230
|
+
// that stale copy's plugin path and the user silently runs an obsolete tool
|
|
1231
|
+
// surface — 1.5.0 removed the pantheon_delegate tool, so a drifted install
|
|
1232
|
+
// still exposes it. This layer compares the version of the package a
|
|
1233
|
+
// registered plugin points into against this package's own version. Without
|
|
1234
|
+
// network access the "latest" version IS the running package's version.
|
|
1235
|
+
|
|
1236
|
+
/** Locates an installed copy of this package inside a dependency tree. */
|
|
1237
|
+
const INSTALLED_PACKAGE_MARKER = '/node_modules/pantheon-opencode/'
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Return the installed-package root containing a plugin path, or null when the
|
|
1241
|
+
* path is not inside a `node_modules/pantheon-opencode` tree.
|
|
1242
|
+
*
|
|
1243
|
+
* Only installed copies are compared: a dev checkout (e.g. <dev>/pantheon)
|
|
1244
|
+
* and unrelated directories named pantheon-opencode are deliberately not
|
|
1245
|
+
* flagged — the drift signal must never fire on a path the installer does not
|
|
1246
|
+
* own (see the third-party guards in scripts/install/opencode.mjs).
|
|
1247
|
+
*
|
|
1248
|
+
* @param {string|null|undefined} pluginPath
|
|
1249
|
+
* @returns {string|null}
|
|
1250
|
+
*/
|
|
1251
|
+
export function resolveInstalledPackageRoot(pluginPath) {
|
|
1252
|
+
if (typeof pluginPath !== 'string') return null
|
|
1253
|
+
const normalized = pluginPath.replaceAll('\\', '/')
|
|
1254
|
+
const index = normalized.toLowerCase().lastIndexOf(INSTALLED_PACKAGE_MARKER.toLowerCase())
|
|
1255
|
+
if (index === -1) return null
|
|
1256
|
+
return normalized.slice(0, index + INSTALLED_PACKAGE_MARKER.length - 1)
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Read the version of the installed pantheon-opencode package a registered
|
|
1261
|
+
* plugin path points into.
|
|
1262
|
+
* @param {string} pluginPath - absolute or config-relative plugin ref
|
|
1263
|
+
* @returns {{root: string, version: string}|null} null when not an installed copy
|
|
1264
|
+
*/
|
|
1265
|
+
function readInstalledPluginVersion(pluginPath) {
|
|
1266
|
+
const root = resolveInstalledPackageRoot(pluginPath)
|
|
1267
|
+
if (!root) return null
|
|
1268
|
+
const pkg = readJson(join(root, 'package.json'))
|
|
1269
|
+
if (!pkg || typeof pkg.version !== 'string') return null
|
|
1270
|
+
return { root, version: pkg.version }
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Classify plugin version drift between the package a registered plugin points
|
|
1275
|
+
* into and the running doctor's own package. Uses the prerelease-aware
|
|
1276
|
+
* comparator so a stable release outranks its own betas (1.5.0 > 1.5.0-beta.9).
|
|
1277
|
+
* Unreadable versions classify as "unknown" rather than drift, so a broken
|
|
1278
|
+
* package.json never produces a false warning.
|
|
1279
|
+
* @param {string|null} registeredVersion
|
|
1280
|
+
* @param {string|null} packageVersion
|
|
1281
|
+
* @returns {'sync'|'drift'|'unknown'}
|
|
1282
|
+
*/
|
|
1283
|
+
export function classifyPluginVersionDrift(registeredVersion, packageVersion) {
|
|
1284
|
+
if (!registeredVersion || !packageVersion) return 'unknown'
|
|
1285
|
+
return comparePantheonVersions(registeredVersion, packageVersion) === 0 ? 'sync' : 'drift'
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Collect the plugin refs registered across opencode.json configs, from both
|
|
1290
|
+
* the V1 `plugin` and V2 `plugins` lists. Each entry keeps the config it came
|
|
1291
|
+
* from so relative refs resolve against the config directory (as the loader
|
|
1292
|
+
* does), never against the doctor's cwd.
|
|
1293
|
+
* @param {{path: string, data: object}[]} configs
|
|
1294
|
+
* @returns {{path: string, configPath: string, configLabel: string}[]}
|
|
1295
|
+
*/
|
|
1296
|
+
export function collectRegisteredPluginRefs(configs) {
|
|
1297
|
+
const refs = []
|
|
1298
|
+
for (const cfg of configs) {
|
|
1299
|
+
for (const key of ['plugin', 'plugins']) {
|
|
1300
|
+
const list = cfg.data?.[key]
|
|
1301
|
+
if (!Array.isArray(list)) continue
|
|
1302
|
+
for (const ref of list) {
|
|
1303
|
+
if (typeof ref === 'string') {
|
|
1304
|
+
refs.push({ path: ref, configPath: cfg.path, configLabel: cfg.label })
|
|
1305
|
+
} else if (ref && typeof ref === 'object') {
|
|
1306
|
+
// Object-shaped entries carry their path under `path`/`source`.
|
|
1307
|
+
const objectPath = ['path', 'source', 'id', 'name'].find(
|
|
1308
|
+
(k) => typeof ref[k] === 'string',
|
|
1309
|
+
)
|
|
1310
|
+
if (objectPath) {
|
|
1311
|
+
refs.push({ path: ref[objectPath], configPath: cfg.path, configLabel: cfg.label })
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return refs
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Compare the version of the package each registered plugin points into (when
|
|
1322
|
+
* it is an installed copy under node_modules/pantheon-opencode) against this
|
|
1323
|
+
* package's own version, and warn on divergence. Advisory only — a drift
|
|
1324
|
+
* warning never changes the exit code, and a healthy install stays silent.
|
|
1325
|
+
* @param {object} args
|
|
1326
|
+
* @returns {'drift'|'sync'|'skip'|'no-plugin'|'no-version'}
|
|
1327
|
+
*/
|
|
1328
|
+
export function checkPluginVersionDrift(args) {
|
|
1329
|
+
section('H3. Plugin Version Drift')
|
|
1330
|
+
|
|
1331
|
+
const pkg = readJson(join(ROOT, 'package.json'))
|
|
1332
|
+
const packageVersion = pkg?.version
|
|
1333
|
+
if (!packageVersion) {
|
|
1334
|
+
info('Plugin version drift skipped — this package has no readable version')
|
|
1335
|
+
return 'no-version'
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const refs = collectRegisteredPluginRefs(collectMcpConfigs(args))
|
|
1339
|
+
if (refs.length === 0) {
|
|
1340
|
+
info('No plugin registrations found in opencode.json — version drift skipped')
|
|
1341
|
+
return 'no-plugin'
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const seen = new Set()
|
|
1345
|
+
let compared = 0
|
|
1346
|
+
let driftReported = false
|
|
1347
|
+
for (const ref of refs) {
|
|
1348
|
+
const absolute = isAbsolute(ref.path) ? ref.path : resolve(dirname(ref.configPath), ref.path)
|
|
1349
|
+
const installed = readInstalledPluginVersion(absolute)
|
|
1350
|
+
if (!installed) continue
|
|
1351
|
+
// The doctor running from the very tree the plugin points at is the
|
|
1352
|
+
// healthy global-install case — trivially in sync, never flagged.
|
|
1353
|
+
if (resolve(installed.root) === resolve(ROOT)) continue
|
|
1354
|
+
if (seen.has(installed.root)) continue
|
|
1355
|
+
seen.add(installed.root)
|
|
1356
|
+
compared++
|
|
1357
|
+
|
|
1358
|
+
if (classifyPluginVersionDrift(installed.version, packageVersion) !== 'drift') continue
|
|
1359
|
+
driftReported = true
|
|
1360
|
+
warn(
|
|
1361
|
+
`${ref.configLabel}: plugin is registered inside pantheon-opencode v${installed.version}, but this package is v${packageVersion} — the registered tool surface is stale (pantheon_delegate was removed in 1.5.0). Run \`npx pantheon-opencode init\`, or update the package, to realign the registered plugin path`,
|
|
1362
|
+
)
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (driftReported) return 'drift'
|
|
1366
|
+
if (compared === 0) {
|
|
1367
|
+
info(
|
|
1368
|
+
'No registered plugin points into an installed pantheon-opencode copy — version drift skipped',
|
|
1369
|
+
)
|
|
1370
|
+
return 'skip'
|
|
1371
|
+
}
|
|
1372
|
+
pass(`All registered plugins point at package v${packageVersion}`)
|
|
1373
|
+
return 'sync'
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1188
1376
|
// ---------------------------------------------------------------------------
|
|
1189
1377
|
// Check G: AGENTS.md freshness
|
|
1190
1378
|
// ---------------------------------------------------------------------------
|
|
@@ -1395,6 +1583,71 @@ export function classifyPermissionTaskCheck(profile, installedCount, missingCoun
|
|
|
1395
1583
|
return missingCount === 0 ? 'pass' : 'error'
|
|
1396
1584
|
}
|
|
1397
1585
|
|
|
1586
|
+
// ---------------------------------------------------------------------------
|
|
1587
|
+
// Check K: Node Runtime — node:sqlite availability (issue #114)
|
|
1588
|
+
// ---------------------------------------------------------------------------
|
|
1589
|
+
|
|
1590
|
+
// pantheon_cost uses node:sqlite as its read-only backend; below this Node
|
|
1591
|
+
// version the tool reports UNSUPPORTED (see the "Cost tool backend" README
|
|
1592
|
+
// section). Matches the floor documented for the PANTHEON_NODE fallback.
|
|
1593
|
+
const NODE_SQLITE_MIN = '22.5'
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* Probe whether the running Node.js can load `node:sqlite` — the read-only
|
|
1597
|
+
* backend used by the `pantheon_cost` tool (issue #114). Runs the import in a
|
|
1598
|
+
* `--no-warnings` child so the experimental-feature notice never pollutes the
|
|
1599
|
+
* doctor output, and so a probe crash can never take down the doctor itself.
|
|
1600
|
+
*
|
|
1601
|
+
* @param {{ execPath?: string; module?: string }} [options]
|
|
1602
|
+
* @returns {{ available: boolean; version: string; reason?: string }}
|
|
1603
|
+
*/
|
|
1604
|
+
export function probeNodeSqlite(options = {}) {
|
|
1605
|
+
const execPath = options.execPath ?? process.execPath
|
|
1606
|
+
const target = options.module ?? 'node:sqlite'
|
|
1607
|
+
const result = spawn(execPath, [
|
|
1608
|
+
'--no-warnings',
|
|
1609
|
+
'--input-type=module',
|
|
1610
|
+
'-e',
|
|
1611
|
+
`import '${target}'`,
|
|
1612
|
+
])
|
|
1613
|
+
if (result.status === 0) return { available: true, version: process.version }
|
|
1614
|
+
const reason = (result.stderr || result.stdout || 'import failed').split('\n')[0]
|
|
1615
|
+
return { available: false, version: process.version, reason }
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Classify a node:sqlite probe result for the doctor. Kept free of I/O so the
|
|
1620
|
+
* contract is exercised deterministically (see tests/test_doctor_layers.mjs).
|
|
1621
|
+
*
|
|
1622
|
+
* @param {{ available: boolean; version: string }} probe
|
|
1623
|
+
* @returns {{ status: 'ok' | 'unsupported'; message: string }}
|
|
1624
|
+
*/
|
|
1625
|
+
export function classifyNodeSqliteProbe(probe) {
|
|
1626
|
+
if (probe.available) {
|
|
1627
|
+
return {
|
|
1628
|
+
status: 'ok',
|
|
1629
|
+
message: `node:sqlite available on ${probe.version} — pantheon_cost read-only backend supported`,
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return {
|
|
1633
|
+
status: 'unsupported',
|
|
1634
|
+
message: `pantheon_cost will report UNSUPPORTED on Node ${probe.version} — node:sqlite requires Node >= ${NODE_SQLITE_MIN}`,
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function checkNodeRuntime() {
|
|
1639
|
+
section('K. Node Runtime')
|
|
1640
|
+
const outcome = classifyNodeSqliteProbe(probeNodeSqlite())
|
|
1641
|
+
if (outcome.status === 'ok') {
|
|
1642
|
+
pass(outcome.message)
|
|
1643
|
+
} else {
|
|
1644
|
+
warn(outcome.message)
|
|
1645
|
+
info(
|
|
1646
|
+
`Upgrade to Node >= ${NODE_SQLITE_MIN} (or a runtime with node:sqlite) to enable pantheon_cost`,
|
|
1647
|
+
)
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1398
1651
|
/**
|
|
1399
1652
|
* Return the final doctor status message without masking blocking errors.
|
|
1400
1653
|
* @param {{ error: number; warn: number }} summaryCounts
|
|
@@ -1477,12 +1730,14 @@ async function main() {
|
|
|
1477
1730
|
existsSync(join(args.target, 'scripts', 'doctor.mjs')) === false
|
|
1478
1731
|
|
|
1479
1732
|
// Run checks (layer order: Config → Venv → spawn paths → runtime smoke)
|
|
1733
|
+
checkNodeRuntime(args)
|
|
1480
1734
|
checkAgentFiles(args)
|
|
1481
1735
|
checkMcpConfig(args)
|
|
1482
1736
|
checkVenvLayer(args)
|
|
1483
1737
|
checkCodeModeDir(args)
|
|
1484
1738
|
checkCodeModeManifest(args)
|
|
1485
1739
|
checkInstallVersionDrift(args)
|
|
1740
|
+
checkPluginVersionDrift(args)
|
|
1486
1741
|
await checkMcpRuntimeSmoke(args)
|
|
1487
1742
|
checkPermissionMismatches(args)
|
|
1488
1743
|
checkSyncStatus(args)
|
|
@@ -8,8 +8,10 @@ import { isAbsolute, join, resolve } from 'node:path'
|
|
|
8
8
|
import { ROOT, writeIfChanged } from './shared.mjs'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Copy a plugin's runtime payload (
|
|
12
|
-
* package
|
|
11
|
+
* Copy a plugin's runtime payload (the bundled dist/*, package.json and
|
|
12
|
+
* package-lock.json) from source to destination. Only the bundle is copied —
|
|
13
|
+
* the raw TSX source is never shipped to the destination, so the plugin can
|
|
14
|
+
* be modularized without the loader needing relative imports.
|
|
13
15
|
* @param {string} srcDir - Source plugin directory (e.g. ROOT/src/plugins/tui)
|
|
14
16
|
* @param {string} dstDir - Destination plugin directory
|
|
15
17
|
* @param {object} [options]
|
|
@@ -22,16 +24,11 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
|
|
|
22
24
|
if (!dryRun) mkdirSync(dstDir, { recursive: true })
|
|
23
25
|
if (!dryRun) mkdirSync(join(dstDir, 'dist'), { recursive: true })
|
|
24
26
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
readFileSync(srcIdx, 'utf8'),
|
|
31
|
-
dryRun,
|
|
32
|
-
)
|
|
33
|
-
if (idxStatus === 'created') result.created++
|
|
34
|
-
else result.skipped++
|
|
27
|
+
// Drop legacy raw copies from previous installs (root index.tsx + dist/tui.tsx).
|
|
28
|
+
// The loader consumes only the bundle, so stale raws must not linger.
|
|
29
|
+
if (!dryRun) {
|
|
30
|
+
rmSync(join(dstDir, 'index.tsx'), { force: true })
|
|
31
|
+
rmSync(join(dstDir, 'dist', 'tui.tsx'), { force: true })
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
// Copy dist/* files
|
|
@@ -1323,7 +1323,7 @@ def build_rehydration_blocks(checkpoint: dict) -> list[str] | None:
|
|
|
1323
1323
|
"""Build deterministic rehydration blocks from a checkpoint dict.
|
|
1324
1324
|
|
|
1325
1325
|
Pure string templating — no LLM, no embeddings. Mirrors the
|
|
1326
|
-
``<mission_context>`` convention of
|
|
1326
|
+
``<mission_context>`` convention of the compaction-context builder: only a
|
|
1327
1327
|
non-done goal is re-emitted. Returns None when nothing critical remains.
|
|
1328
1328
|
"""
|
|
1329
1329
|
if not isinstance(checkpoint, dict) or not checkpoint:
|
package/scripts/sync-tui.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* If the user hasn't initialized yet → do nothing (silent exit).
|
|
11
11
|
*/
|
|
12
12
|
import { execSync } from 'node:child_process'
|
|
13
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
14
14
|
import { homedir } from 'node:os'
|
|
15
15
|
import { join } from 'node:path'
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
@@ -58,9 +58,9 @@ function resolveConfigDir() {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* Copy plugin runtime files (dist/*, package.json,
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* Copy plugin runtime files (the bundled dist/*, package.json,
|
|
62
|
+
* package-lock.json) from srcDir to dstDir. Skips files that are byte-identical.
|
|
63
|
+
* The raw TSX source is never copied — the loader consumes only the bundle.
|
|
64
64
|
* @returns {{ created: number, skipped: number }}
|
|
65
65
|
*/
|
|
66
66
|
function copyPluginFiles(srcDir, dstDir) {
|
|
@@ -69,13 +69,10 @@ function copyPluginFiles(srcDir, dstDir) {
|
|
|
69
69
|
mkdirSync(dstDir, { recursive: true })
|
|
70
70
|
mkdirSync(join(dstDir, 'dist'), { recursive: true })
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (writeIfChanged(join(dstDir, 'index.tsx'), content)) result.created++
|
|
77
|
-
else result.skipped++
|
|
78
|
-
}
|
|
72
|
+
// Drop legacy raw copies from previous installs (root index.tsx + dist/tui.tsx).
|
|
73
|
+
// The loader consumes only the bundle, so stale raws must not linger.
|
|
74
|
+
rmSync(join(dstDir, 'index.tsx'), { force: true })
|
|
75
|
+
rmSync(join(dstDir, 'dist', 'tui.tsx'), { force: true })
|
|
79
76
|
|
|
80
77
|
// dist/*
|
|
81
78
|
const distSrc = join(srcDir, 'dist')
|
package/src/agents/apollo.md
CHANGED
package/src/agents/gaia.md
CHANGED
package/src/agents/mnemosyne.md
CHANGED
|
@@ -34,7 +34,17 @@ permission:
|
|
|
34
34
|
bash: deny
|
|
35
35
|
read: allow
|
|
36
36
|
grep: allow
|
|
37
|
-
edit:
|
|
37
|
+
# Scoped write/edit: mnemosyne may create and modify files ONLY inside the
|
|
38
|
+
# memory-bank and deepwork directories. Everything else stays denied, keeping
|
|
39
|
+
# the SCOPE BOUNDARY below enforceable at the permission layer (issue #111).
|
|
40
|
+
write:
|
|
41
|
+
"*": deny
|
|
42
|
+
".pantheon/memory-bank/**": allow
|
|
43
|
+
".pantheon/deepwork/**": allow
|
|
44
|
+
edit:
|
|
45
|
+
"*": deny
|
|
46
|
+
".pantheon/memory-bank/**": allow
|
|
47
|
+
".pantheon/deepwork/**": allow
|
|
38
48
|
task:
|
|
39
49
|
"*": deny
|
|
40
50
|
---
|