pantheon-opencode 1.5.0 → 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/CHANGELOG.md +22 -0
- package/README.md +63 -2
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/pyproject.toml +2 -1
- package/scripts/doctor.mjs +228 -2
- package/src/agents/mnemosyne.md +11 -1
- package/src/agents/zeus.md +1 -0
- package/src/mcp/memory_mcp_server.py +81 -27
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +19 -24
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +22 -81
- package/src/plugins/tui/package-lock.json +5 -2
- package/src/plugins/tui/package.json +4 -1
- package/src/plugins/tui/src/index.tsx +54 -151
- package/src/mcp/toon_codec.py +0 -489
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
18
18
|
|
|
19
19
|
## ✅ Closed Issues
|
|
20
20
|
|
|
21
|
+
## [v1.5.1] - 2026-09-22
|
|
22
|
+
|
|
23
|
+
## 🐞 Fixed
|
|
24
|
+
- **agents** — declare explicit read: allow for zeus
|
|
25
|
+
- **ci** — drop fastembed from CI, keep pytest collection green (#163)
|
|
26
|
+
- **plugin** — detect registered-plugin version drift after pantheon_delegate removal (#169)
|
|
27
|
+
- **config** — correct node floor and warn on unsupported runtime (#168)
|
|
28
|
+
- **mcp** — degrade gracefully when fastembed import fails (#165)
|
|
29
|
+
- **mcp** — drop dead toon_codec module from published package (#167)
|
|
30
|
+
- **tui** — remove dead pantheon_delegate refs after v1.5.0 V1 removal (#166)
|
|
31
|
+
- **mnemosyne** — grant scoped write to memory-bank and deepwork paths (#164)
|
|
32
|
+
|
|
33
|
+
## ✅ Closed Issues
|
|
34
|
+
- #94 - drop fastembed from CI, keep pytest collection green (#163)
|
|
35
|
+
- #158 - detect registered-plugin version drift after pantheon_delegate removal (#169)
|
|
36
|
+
- #114 - correct node floor and warn on unsupported runtime (#168)
|
|
37
|
+
- #160 - correct node floor and warn on unsupported runtime (#168)
|
|
38
|
+
- #159 - degrade gracefully when fastembed import fails (#165)
|
|
39
|
+
- #162 - drop dead toon_codec module from published package (#167)
|
|
40
|
+
- #161 - remove dead pantheon_delegate refs after v1.5.0 V1 removal (#166)
|
|
41
|
+
- #111 - grant scoped write to memory-bank and deepwork paths (#164)
|
|
42
|
+
|
|
21
43
|
## [v1.5.0] - 2026-09-16
|
|
22
44
|
|
|
23
45
|
### 🆕 What's New
|
package/README.md
CHANGED
|
@@ -30,7 +30,15 @@ way to plan work, make progress, check results, and keep useful project context.
|
|
|
30
30
|
|
|
31
31
|
## Start in 2 minutes
|
|
32
32
|
|
|
33
|
-
Requirements: [OpenCode 1.18.4+](https://opencode.ai/docs/) and Node.js
|
|
33
|
+
Requirements: [OpenCode 1.18.4+](https://opencode.ai/docs/) and Node.js
|
|
34
|
+
22.22.2+ (or 24.15.0+ / 26+).
|
|
35
|
+
|
|
36
|
+
Pantheon declares `engines.node` as `^22.22.2 || ^24.15.0 || >=26.0.0`. The
|
|
37
|
+
floor reflects what the dependency tree actually needs — the transitive
|
|
38
|
+
`ini@7` rejects earlier 22.x/24.x builds with `EBADENGINE` — and odd-numbered
|
|
39
|
+
Node releases (23, 25) are out of range. The `pantheon_cost` tool also needs
|
|
40
|
+
`node:sqlite`, which requires Node >= 22.5; `doctor` warns when the running
|
|
41
|
+
runtime cannot load it.
|
|
34
42
|
|
|
35
43
|
From the project where you want to use Pantheon:
|
|
36
44
|
|
|
@@ -246,7 +254,12 @@ Two freshness guarantees back it up:
|
|
|
246
254
|
re-run `init` just for file copies.
|
|
247
255
|
- **Drift detection** — the installer stamps the installed version in
|
|
248
256
|
`.pantheon/install-state.json` and `doctor` warns when the package is newer
|
|
249
|
-
than the last sync, pointing at `update`.
|
|
257
|
+
than the last sync, pointing at `update`. `doctor` also detects **plugin
|
|
258
|
+
version drift** (issue #158): when `opencode.json` registers a plugin path
|
|
259
|
+
inside a `node_modules/pantheon-opencode` copy whose `package.json` version
|
|
260
|
+
differs from the running package, it warns that the registered tool surface
|
|
261
|
+
is stale. Re-running `init`/`update` realigns the registration onto the
|
|
262
|
+
current package.
|
|
250
263
|
|
|
251
264
|
`init` also gained `--components agents,skills,...` (narrow install),
|
|
252
265
|
`--clean` (alias of `--force`), `--opencode-version auto`, atomic config
|
|
@@ -256,6 +269,42 @@ the install completes but MCP entries are omitted (with a warning) instead of
|
|
|
256
269
|
pointing at a broken interpreter. Installer messages auto-detect pt-BR via
|
|
257
270
|
`LANG`/`LC_ALL`.
|
|
258
271
|
|
|
272
|
+
## Migrating to 1.5.x (from 1.4.x)
|
|
273
|
+
|
|
274
|
+
1.5.0 removed the custom `pantheon_delegate` tool (and the V1 delegation
|
|
275
|
+
engine) in favor of OpenCode's native `task()`; see
|
|
276
|
+
[Delegation (native `task()`)](#delegation-native-task). Two things change on
|
|
277
|
+
an existing install:
|
|
278
|
+
|
|
279
|
+
1. **The tool disappears from the plugin surface.** `pantheon_delegate` is no
|
|
280
|
+
longer registered by `src/plugin.ts` or `src/plugin-v2.ts`. Agents now
|
|
281
|
+
delegate through `task()` only — no configuration is needed.
|
|
282
|
+
2. **A lockfile-pinned copy can keep the old tool alive.** `npm install` is
|
|
283
|
+
lockfile-authoritative: a `package-lock.json` pinned to `1.4.1` (which
|
|
284
|
+
satisfies `^1.4.1`) is never re-resolved, so a project's
|
|
285
|
+
`node_modules/pantheon-opencode` can stay on 1.4.x while the published
|
|
286
|
+
package moved on. The plugin path your `opencode.json` registers keeps
|
|
287
|
+
pointing at that stale copy, and you keep running the obsolete tool surface
|
|
288
|
+
— including `pantheon_delegate` — with no warning.
|
|
289
|
+
|
|
290
|
+
The fix is a realignment + a detector:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
# Realign the registered plugin path onto the current package (rewrites any
|
|
294
|
+
# node_modules/pantheon-opencode reference in opencode.json):
|
|
295
|
+
npx pantheon-opencode@latest init --yes --headless
|
|
296
|
+
# or, with a global install:
|
|
297
|
+
pantheon-opencode update
|
|
298
|
+
|
|
299
|
+
# Then verify no drift remains:
|
|
300
|
+
npx pantheon-opencode@latest doctor
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`doctor` now reports a **Plugin Version Drift** warning (section H3) when the
|
|
304
|
+
registered plugin points into an installed copy whose version differs from the
|
|
305
|
+
running package, naming both versions and the removal (`pantheon_delegate` in
|
|
306
|
+
1.5.0). A healthy install reports no warning.
|
|
307
|
+
|
|
259
308
|
## Releases
|
|
260
309
|
|
|
261
310
|
Publication is authorized **only** by an explicit `workflow_dispatch` of the
|
|
@@ -331,6 +380,18 @@ and returns nothing. Prefer `background=true` dispatches with an explicit
|
|
|
331
380
|
`task_status(wait=true)` fan-in so large payloads are collected deterministically.
|
|
332
381
|
|
|
333
382
|
|
|
383
|
+
## Configuration (environment variables)
|
|
384
|
+
|
|
385
|
+
| Variable | Default | Description |
|
|
386
|
+
|---|---|---|
|
|
387
|
+
| `PANTHEON_MEMORY_EMBED` | `on` | `off` (also `0`, `false`, `no`) disables the `pantheon-memory` embedding/vector pipeline — search runs in FTS5-only mode with no model download and no `sqlite-vec` writes. Also forced `off` automatically when `fastembed` fails to import, so a broken embedding install never takes the server down (issue #159). |
|
|
388
|
+
|
|
389
|
+
The memory MCP degrades gracefully: when `fastembed` or `sqlite-vec` are
|
|
390
|
+
unavailable (network failure, incompatible wheel), the server still starts,
|
|
391
|
+
answers the MCP `initialize` handshake, and serves keyword search — only the
|
|
392
|
+
semantic vector ranking is unavailable.
|
|
393
|
+
|
|
394
|
+
|
|
334
395
|
## Documentation
|
|
335
396
|
|
|
336
397
|
- [Installation](docs/INSTALLATION.md) · [Quick start](docs/QUICKSTART.md)
|
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": {
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"author": "ils15",
|
|
120
120
|
"license": "MIT",
|
|
121
121
|
"engines": {
|
|
122
|
-
"node": ">=
|
|
122
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
123
123
|
},
|
|
124
124
|
"preferGlobal": true
|
|
125
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 = []
|
|
@@ -39,6 +39,7 @@ ignore = ["E501"]
|
|
|
39
39
|
"scripts/vector_memory/*.py" = ["PLC0415"]
|
|
40
40
|
"tests/test_code_mode_sandbox.py" = ["PLC0415", "N806"]
|
|
41
41
|
"tests/test_code_mode_server.py" = ["PLC0415"]
|
|
42
|
+
"tests/test_codemap_mcp.py" = ["PLC0415"]
|
|
42
43
|
"tests/test_memory_mcp_server.py" = ["PLC0415", "PLR2004"]
|
|
43
44
|
# Existing public APIs and test fixtures are intentionally broad/simple; refactoring
|
|
44
45
|
# them would be behavior-affecting rather than a mechanical lint correction.
|
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
|
|
@@ -25,7 +26,7 @@ import { spawn as spawnAsync, spawnSync } from 'node:child_process'
|
|
|
25
26
|
import { createHash } from 'node:crypto'
|
|
26
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)
|
|
@@ -1161,7 +1164,10 @@ function comparePantheonVersions(a, b) {
|
|
|
1161
1164
|
for (let i = 0; i < 3; i++) {
|
|
1162
1165
|
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
|
|
1163
1166
|
}
|
|
1164
|
-
|
|
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
|
|
1165
1171
|
}
|
|
1166
1172
|
|
|
1167
1173
|
/**
|
|
@@ -1214,6 +1220,159 @@ export function checkInstallVersionDrift(args) {
|
|
|
1214
1220
|
return 'sync'
|
|
1215
1221
|
}
|
|
1216
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
|
+
|
|
1217
1376
|
// ---------------------------------------------------------------------------
|
|
1218
1377
|
// Check G: AGENTS.md freshness
|
|
1219
1378
|
// ---------------------------------------------------------------------------
|
|
@@ -1424,6 +1583,71 @@ export function classifyPermissionTaskCheck(profile, installedCount, missingCoun
|
|
|
1424
1583
|
return missingCount === 0 ? 'pass' : 'error'
|
|
1425
1584
|
}
|
|
1426
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
|
+
|
|
1427
1651
|
/**
|
|
1428
1652
|
* Return the final doctor status message without masking blocking errors.
|
|
1429
1653
|
* @param {{ error: number; warn: number }} summaryCounts
|
|
@@ -1506,12 +1730,14 @@ async function main() {
|
|
|
1506
1730
|
existsSync(join(args.target, 'scripts', 'doctor.mjs')) === false
|
|
1507
1731
|
|
|
1508
1732
|
// Run checks (layer order: Config → Venv → spawn paths → runtime smoke)
|
|
1733
|
+
checkNodeRuntime(args)
|
|
1509
1734
|
checkAgentFiles(args)
|
|
1510
1735
|
checkMcpConfig(args)
|
|
1511
1736
|
checkVenvLayer(args)
|
|
1512
1737
|
checkCodeModeDir(args)
|
|
1513
1738
|
checkCodeModeManifest(args)
|
|
1514
1739
|
checkInstallVersionDrift(args)
|
|
1740
|
+
checkPluginVersionDrift(args)
|
|
1515
1741
|
await checkMcpRuntimeSmoke(args)
|
|
1516
1742
|
checkPermissionMismatches(args)
|
|
1517
1743
|
checkSyncStatus(args)
|
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
|
---
|
package/src/agents/zeus.md
CHANGED
|
@@ -6,6 +6,10 @@ for semantic memory with hybrid search (vector cosine + FTS5 BM25).
|
|
|
6
6
|
|
|
7
7
|
Total footprint: ~50MB (sqlite-vec + fastembed) vs ~1.4GB (old chromadb).
|
|
8
8
|
|
|
9
|
+
Both embedding backends are optional: if either fails to import the server
|
|
10
|
+
still starts and serves FTS5-only search (``PANTHEON_MEMORY_EMBED=off``
|
|
11
|
+
disables the vector pipeline explicitly).
|
|
12
|
+
|
|
9
13
|
Tools:
|
|
10
14
|
memory_store — Store with automatic embedding
|
|
11
15
|
memory_search — Hybrid vector + FTS5 keyword search
|
|
@@ -30,9 +34,21 @@ from datetime import UTC, datetime
|
|
|
30
34
|
from pathlib import Path
|
|
31
35
|
from typing import Any
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
# Embedding backends are optional (issue #159): a failed fastembed/sqlite_vec
|
|
38
|
+
# install (network error, incompatible wheel) must not kill the server — it
|
|
39
|
+
# degrades to FTS-only search instead of crashing before the MCP handshake.
|
|
40
|
+
try:
|
|
41
|
+
import sqlite_vec
|
|
42
|
+
except ImportError: # pragma: no cover - degraded mode
|
|
43
|
+
sqlite_vec = None # type: ignore[assignment]
|
|
44
|
+
|
|
34
45
|
from _pantheon_paths import pantheon_home
|
|
35
|
-
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from fastembed import TextEmbedding
|
|
49
|
+
except ImportError: # pragma: no cover - degraded mode
|
|
50
|
+
TextEmbedding = None # type: ignore[assignment]
|
|
51
|
+
|
|
36
52
|
from mcp.server.fastmcp import FastMCP
|
|
37
53
|
|
|
38
54
|
try:
|
|
@@ -52,6 +68,12 @@ except ImportError: # pragma: no cover - deployed runtime may need path fix
|
|
|
52
68
|
DB_PATH = pantheon_home() / "memory" / "memory.db"
|
|
53
69
|
EMBED_CACHE = Path.home() / ".cache" / "fastembed"
|
|
54
70
|
|
|
71
|
+
# PANTHEON_MEMORY_EMBED=off disables the embedding/vector pipeline so the
|
|
72
|
+
# server serves FTS-only search (issue #159). Default "on" keeps the current
|
|
73
|
+
# hybrid vector + FTS behavior. The flag is also forced off automatically
|
|
74
|
+
# when fastembed failed to import.
|
|
75
|
+
_EMBED_DISABLED = frozenset({"off", "0", "false", "no"})
|
|
76
|
+
|
|
55
77
|
|
|
56
78
|
_BYTE_UNIT = 1024
|
|
57
79
|
|
|
@@ -81,11 +103,6 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
81
103
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_ns_key
|
|
82
104
|
ON memories(namespace, key);
|
|
83
105
|
|
|
84
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
|
|
85
|
-
id INTEGER PRIMARY KEY,
|
|
86
|
-
embedding float[384]
|
|
87
|
-
);
|
|
88
|
-
|
|
89
106
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
90
107
|
namespace, key, value, metadata,
|
|
91
108
|
content='memories', content_rowid='id',
|
|
@@ -113,6 +130,14 @@ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
|
113
130
|
END;
|
|
114
131
|
"""
|
|
115
132
|
|
|
133
|
+
# Vector store requires the sqlite-vec extension; created only when available.
|
|
134
|
+
VEC_SCHEMA_SQL = """
|
|
135
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
|
|
136
|
+
id INTEGER PRIMARY KEY,
|
|
137
|
+
embedding float[384]
|
|
138
|
+
);
|
|
139
|
+
"""
|
|
140
|
+
|
|
116
141
|
# ── FastMCP App ───────────────────────────────────────────────────────────────
|
|
117
142
|
|
|
118
143
|
mcp = FastMCP(
|
|
@@ -129,7 +154,8 @@ def _get_db() -> sqlite3.Connection:
|
|
|
129
154
|
"""Get or create the SQLite connection singleton.
|
|
130
155
|
|
|
131
156
|
Creates DB directory, applies WAL/performance pragmas, registers
|
|
132
|
-
the sqlite-vec extension, and runs schema init.
|
|
157
|
+
the sqlite-vec extension, and runs schema init. When sqlite-vec is
|
|
158
|
+
unavailable the vector table is skipped — search degrades to FTS-only.
|
|
133
159
|
"""
|
|
134
160
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
135
161
|
conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
|
|
@@ -137,9 +163,10 @@ def _get_db() -> sqlite3.Connection:
|
|
|
137
163
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
138
164
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
139
165
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
140
|
-
# Register sqlite-vec extension
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
# Register sqlite-vec extension (optional; enables vector search)
|
|
167
|
+
if sqlite_vec is not None:
|
|
168
|
+
sqlite_vec.load(conn)
|
|
169
|
+
conn.executescript(VEC_SCHEMA_SQL)
|
|
143
170
|
conn.executescript(SCHEMA_SQL)
|
|
144
171
|
if _codemap is not None:
|
|
145
172
|
with contextlib.suppress(Exception):
|
|
@@ -155,13 +182,30 @@ def _get_conn() -> sqlite3.Connection:
|
|
|
155
182
|
# ── Embedding ─────────────────────────────────────────────────────────────────
|
|
156
183
|
|
|
157
184
|
|
|
185
|
+
def _embeddings_enabled() -> bool:
|
|
186
|
+
"""Whether the embedding/vector pipeline is active.
|
|
187
|
+
|
|
188
|
+
Controlled by ``PANTHEON_MEMORY_EMBED`` (``on`` by default; ``off``,
|
|
189
|
+
``0``, ``false`` or ``no`` disable it). Automatically disabled when
|
|
190
|
+
``fastembed`` failed to import, so the server degrades to FTS-only
|
|
191
|
+
search instead of crashing at startup (issue #159).
|
|
192
|
+
"""
|
|
193
|
+
if TextEmbedding is None:
|
|
194
|
+
return False
|
|
195
|
+
flag = os.environ.get("PANTHEON_MEMORY_EMBED", "on").strip().lower()
|
|
196
|
+
return flag not in _EMBED_DISABLED
|
|
197
|
+
|
|
198
|
+
|
|
158
199
|
@functools.cache
|
|
159
|
-
def _get_embedder() -> TextEmbedding:
|
|
200
|
+
def _get_embedder() -> TextEmbedding | None:
|
|
160
201
|
"""Lazy-load fastembed model (auto-downloads on first call, ~30MB).
|
|
161
202
|
|
|
162
203
|
Uses BAAI/bge-small-en-v1.5 (384-dim, CPU, ONNX) — no PyTorch needed.
|
|
163
|
-
Model is cached in ~/.cache/fastembed/.
|
|
204
|
+
Model is cached in ~/.cache/fastembed/. Returns None when fastembed is
|
|
205
|
+
unavailable.
|
|
164
206
|
"""
|
|
207
|
+
if TextEmbedding is None:
|
|
208
|
+
return None
|
|
165
209
|
os.environ.setdefault("TQDM_DISABLE", "1")
|
|
166
210
|
return TextEmbedding(
|
|
167
211
|
model_name="BAAI/bge-small-en-v1.5",
|
|
@@ -169,9 +213,18 @@ def _get_embedder() -> TextEmbedding:
|
|
|
169
213
|
)
|
|
170
214
|
|
|
171
215
|
|
|
172
|
-
def _embed(text: str) -> list[float]:
|
|
173
|
-
"""Generate a 384-dim embedding vector for the given text.
|
|
174
|
-
|
|
216
|
+
def _embed(text: str) -> list[float] | None:
|
|
217
|
+
"""Generate a 384-dim embedding vector for the given text.
|
|
218
|
+
|
|
219
|
+
Returns None when embeddings are disabled (``PANTHEON_MEMORY_EMBED=off``
|
|
220
|
+
or fastembed missing) — callers fall back to FTS-only search.
|
|
221
|
+
"""
|
|
222
|
+
if not _embeddings_enabled():
|
|
223
|
+
return None
|
|
224
|
+
embedder = _get_embedder()
|
|
225
|
+
if embedder is None:
|
|
226
|
+
return None
|
|
227
|
+
return next(iter(embedder.embed(text))).tolist()
|
|
175
228
|
|
|
176
229
|
|
|
177
230
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -341,14 +394,14 @@ def memory_store(
|
|
|
341
394
|
)
|
|
342
395
|
entry_id = cur.lastrowid
|
|
343
396
|
|
|
344
|
-
# Generate embedding
|
|
397
|
+
# Generate embedding (None when disabled → FTS-only mode)
|
|
345
398
|
embedding = _embed(value)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
399
|
+
if embedding is not None and sqlite_vec is not None:
|
|
400
|
+
embedding_json = json.dumps(embedding)
|
|
401
|
+
db.execute(
|
|
402
|
+
"INSERT INTO vec_memories(id, embedding) VALUES (?, ?)",
|
|
403
|
+
[entry_id, embedding_json],
|
|
404
|
+
)
|
|
352
405
|
db.commit()
|
|
353
406
|
except sqlite3.IntegrityError as e:
|
|
354
407
|
db.rollback()
|
|
@@ -406,8 +459,8 @@ def memory_search( # noqa: C901, PLR0912
|
|
|
406
459
|
vec_ids: list[int] = []
|
|
407
460
|
fts_ids: list[int] = []
|
|
408
461
|
|
|
409
|
-
# 2. Vector search
|
|
410
|
-
if query_vec is not None:
|
|
462
|
+
# 2. Vector search (skipped when embeddings or sqlite-vec are unavailable)
|
|
463
|
+
if query_vec is not None and sqlite_vec is not None:
|
|
411
464
|
nvec = top_k * 2
|
|
412
465
|
try:
|
|
413
466
|
if namespace:
|
|
@@ -564,8 +617,9 @@ def memory_forget(
|
|
|
564
617
|
db = _get_conn()
|
|
565
618
|
try:
|
|
566
619
|
if col == "id":
|
|
567
|
-
# Vec table cascade
|
|
568
|
-
|
|
620
|
+
# Vec table cascade (absent when sqlite-vec is unavailable)
|
|
621
|
+
if sqlite_vec is not None:
|
|
622
|
+
db.execute("DELETE FROM vec_memories WHERE id = ?", [id])
|
|
569
623
|
cur = db.execute("DELETE FROM memories WHERE id = ?", [id])
|
|
570
624
|
else:
|
|
571
625
|
cur = db.execute(sql, params)
|