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.
Files changed (84) hide show
  1. package/AGENTS.md +14 -63
  2. package/CHANGELOG.md +326 -0
  3. package/README.md +77 -29
  4. package/README.pt-BR.md +5 -6
  5. package/commands/pantheon.md +1 -35
  6. package/package.json +2 -3
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +3 -5
  9. package/scripts/doctor.mjs +260 -5
  10. package/scripts/install/plugin.mjs +9 -12
  11. package/scripts/mcp_persistence_server.py +1 -1
  12. package/scripts/sync-tui.mjs +8 -11
  13. package/src/agents/apollo.md +1 -0
  14. package/src/agents/gaia.md +1 -0
  15. package/src/agents/mnemosyne.md +11 -1
  16. package/src/agents/zeus.md +10 -52
  17. package/src/instructions/memory-protocol.instructions.md +6 -3
  18. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  19. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  20. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  21. package/src/mcp/mcp_persistence_server.py +1 -1
  22. package/src/mcp/memory_mcp_server.py +81 -27
  23. package/src/pantheon/background-job-board.ts +4 -4
  24. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  25. package/src/pantheon/cost-command.ts +1 -2
  26. package/src/pantheon/delegation-enforce.ts +59 -14
  27. package/src/pantheon/goal-loop.ts +1 -1
  28. package/src/pantheon/goal-store.ts +1 -1
  29. package/src/pantheon/idle-continuation.ts +4 -6
  30. package/src/pantheon/model-command.ts +1 -2
  31. package/src/pantheon/native-task-status.ts +2 -2
  32. package/src/pantheon/presets.d.mts +0 -20
  33. package/src/pantheon/presets.mjs +0 -64
  34. package/src/pantheon/session-guard.ts +2 -2
  35. package/src/pantheon/session-id.ts +30 -0
  36. package/src/pantheon/task-result-guard.ts +2 -3
  37. package/src/pantheon/todo-enforcer.ts +3 -3
  38. package/src/pantheon/tool-context.ts +23 -0
  39. package/src/pantheon/v2-bridge.ts +1 -4
  40. package/src/plugin-v2.ts +8 -21
  41. package/src/plugin.ts +61 -245
  42. package/src/plugins/pantheon-hooks.ts +2 -2
  43. package/src/plugins/tui/dist/tui.d.ts +229 -103
  44. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  45. package/src/plugins/tui/dist/tui.js +495 -333
  46. package/src/plugins/tui/package-lock.json +5 -2
  47. package/src/plugins/tui/package.json +5 -2
  48. package/src/plugins/tui/src/index.tsx +710 -409
  49. package/src/routing.yml +0 -16
  50. package/src/skills/context-compression/SKILL.md +1 -1
  51. package/scripts/generate-preset-docs.mjs +0 -300
  52. package/scripts/generate-prompts.sh +0 -222
  53. package/scripts/hash_verify.py +0 -192
  54. package/scripts/install/component.mjs +0 -24
  55. package/scripts/install/migrate-config.mjs +0 -302
  56. package/scripts/test-context-compression.sh +0 -169
  57. package/src/mcp/toon_codec.py +0 -491
  58. package/src/pantheon/auto-wake.ts +0 -85
  59. package/src/pantheon/chat-reminders.ts +0 -9
  60. package/src/pantheon/comment-checker.ts +0 -48
  61. package/src/pantheon/compaction-assert.ts +0 -160
  62. package/src/pantheon/compaction-handoff.ts +0 -304
  63. package/src/pantheon/cost-tracker.ts +0 -108
  64. package/src/pantheon/delegate-manager.ts +0 -766
  65. package/src/pantheon/delegation-classifier.ts +0 -307
  66. package/src/pantheon/delegation-finalize.ts +0 -406
  67. package/src/pantheon/delegation-notify.ts +0 -212
  68. package/src/pantheon/delegation-toast.ts +0 -73
  69. package/src/pantheon/delegation.ts +0 -1337
  70. package/src/pantheon/dispatch-guard.ts +0 -109
  71. package/src/pantheon/native-probe.ts +0 -234
  72. package/src/pantheon/permission-eval.ts +0 -284
  73. package/src/pantheon/preemptive-compact.ts +0 -76
  74. package/src/pantheon/provider-capabilities.ts +0 -221
  75. package/src/pantheon/provider-stream-tracker.ts +0 -182
  76. package/src/pantheon/retry-policy.ts +0 -212
  77. package/src/pantheon/step-cap.ts +0 -110
  78. package/src/pantheon/token-opt.ts +0 -689
  79. package/src/pantheon/toon-codec.ts +0 -481
  80. package/src/pantheon/v2-events.ts +0 -148
  81. package/src/pantheon/v2-hooks.ts +0 -287
  82. package/src/pantheon/v2-tool-definitions.ts +0 -331
  83. package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
  84. 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 18+.
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 legado, incluindo `pantheon_delegate`, ferramentas read/list, hooks de evento/ferramenta e tratamento de compactação V1 | Plugin V2 completo: 9 ferramentas de orquestração, 4 assinaturas de eventos, session hooks (`prompt`, `context`), tool hooks (`execute.before`/`after`), além de transforms de configuração |
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 9 ferramentas de orquestração (`pantheon_delegate`,
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 API de delegate específica do V1).
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, DelegationClient,
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
 
@@ -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
- ## Dispatch Selection
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.0-beta.9",
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": ">=22"
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.0-beta.9",
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.0-beta.9"
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/test_eval_store.py" = ["PLC0415", "PLR2004"]
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/test_eval_judge.py" = ["PLR2004"]
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]
@@ -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((candidate) => existsSync(candidate)) ??
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((candidate) => existsSync(candidate))
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
- return pa.beta - pb.beta
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 (src/index.tsx → index.tsx, dist/*,
12
- * package.json and package-lock.json) from source to destination.
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
- // Copy src/index.tsx -> index.tsx
26
- const srcIdx = join(srcDir, 'src', 'index.tsx')
27
- if (existsSync(srcIdx)) {
28
- const idxStatus = writeIfChanged(
29
- join(dstDir, 'index.tsx'),
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 delegation-compaction.ts: only a
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:
@@ -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, package-lock.json,
62
- * src/index.tsx) from srcDir
63
- * to dstDir. Skips files that are byte-identical.
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
- // src/index.tsx → index.tsx
73
- const srcIdx = join(srcDir, 'src', 'index.tsx')
74
- if (existsSync(srcIdx)) {
75
- const content = readFileSync(srcIdx, 'utf8')
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')
@@ -12,6 +12,7 @@ permission:
12
12
  glob: allow
13
13
  webfetch: allow
14
14
  edit: deny
15
+ hashline_edit: deny
15
16
  bash: deny
16
17
  task:
17
18
  "*": deny
@@ -11,6 +11,7 @@ permission:
11
11
  grep: allow
12
12
  webfetch: allow
13
13
  edit: deny
14
+ hashline_edit: deny
14
15
  bash: deny
15
16
  task:
16
17
  "*": deny
@@ -34,7 +34,17 @@ permission:
34
34
  bash: deny
35
35
  read: allow
36
36
  grep: allow
37
- edit: allow
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
  ---