pantheon-opencode 1.3.5-beta.60.efa533f → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -6
- package/package.json +4 -3
- package/scripts/_pantheon_paths.py +0 -1
- package/scripts/build-agents-md.mjs +4 -2
- package/scripts/code_mode_server.py +307 -21
- package/scripts/doctor.mjs +58 -15
- package/scripts/hooks/format-multi-language.sh +6 -3
- package/scripts/hooks/log-session-start.sh +6 -0
- package/scripts/hooks/on-subagent-delegation-start.sh +22 -6
- package/scripts/hooks/validate-post-conditions.sh +1 -1
- package/scripts/hooks/validate-tool-safety.sh +5 -2
- package/scripts/install/agents-md.mjs +3 -1
- package/scripts/install/cli-ui.mjs +24 -4
- package/scripts/install/interactive.mjs +18 -7
- package/scripts/install/migrate-config.mjs +13 -19
- package/scripts/install/migrate.mjs +8 -5
- package/scripts/install/model-picker.mjs +4 -1
- package/scripts/install/opencode.mjs +8 -2
- package/scripts/install/shared.mjs +8 -4
- package/scripts/install/state.mjs +9 -6
- package/scripts/mcp_persistence_server.py +23 -22
- package/scripts/mcp_resources_server.py +60 -1
- package/scripts/memory_mcp_server.py +80 -4
- package/scripts/postinstall.mjs +27 -25
- package/scripts/prune.mjs +1 -1
- package/scripts/redaction-gate.mjs +3 -1
- package/scripts/uninstall.mjs +0 -2
- package/scripts/validation-policy.mjs +6 -1
- package/scripts/version-check.mjs +9 -4
- package/src/agents/aphrodite.md +1 -1
- package/src/agents/apollo.md +3 -0
- package/src/agents/demeter.md +1 -1
- package/src/agents/gaia.md +3 -0
- package/src/agents/hephaestus.md +1 -1
- package/src/agents/hermes.md +1 -1
- package/src/agents/themis.md +4 -0
- package/src/agents/zeus.md +5 -1
- package/src/mcp/code_mode_server.py +304 -18
- package/src/mcp/eval_store.py +233 -0
- package/src/mcp/mcp_persistence_server.py +23 -22
- package/src/mcp/mcp_resources_server.py +58 -0
- package/src/mcp/memory_mcp_server.py +80 -3
- package/src/pantheon/auto-wake.ts +2 -4
- package/src/pantheon/background-job-board.ts +17 -4
- package/src/pantheon/comment-checker.ts +48 -0
- package/src/pantheon/delegation-classifier.ts +307 -0
- package/src/pantheon/delegation-enforce.ts +67 -3
- package/src/pantheon/delegation-finalize.ts +44 -4
- package/src/pantheon/delegation.ts +413 -30
- package/src/pantheon/dispatch-guard.ts +2 -0
- package/src/pantheon/file-persistence.ts +7 -3
- package/src/pantheon/goal-loop.ts +13 -2
- package/src/pantheon/permission-eval.ts +284 -0
- package/src/pantheon/permission-globs.ts +86 -0
- package/src/pantheon/presets.d.mts +41 -0
- package/src/pantheon/presets.mjs +135 -0
- package/src/pantheon/retry-policy.ts +212 -0
- package/src/pantheon/session-guard.ts +48 -0
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/todo-enforcer.ts +31 -5
- package/src/pantheon/todo-preserve.ts +7 -1
- package/src/pantheon/vision.ts +7 -5
- package/src/pantheon/zeus-delegate-with-retry.ts +401 -0
- package/src/plugin.ts +72 -13
- package/src/plugins/hook-runner.ts +1 -1
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +36 -14
- package/src/plugins/tui/dist/tui.tsx +71 -16
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +71 -16
- package/src/plugins/tui/tsdown.config.ts +1 -1
- package/src/routing.yml +47 -0
- package/src/agents/README.md +0 -26
|
@@ -178,14 +178,15 @@ _global_root = (
|
|
|
178
178
|
_global_db = _init_db(_global_root / "global.db")
|
|
179
179
|
|
|
180
180
|
_project_db_instance = None
|
|
181
|
+
_project_db_path: Path | None = None
|
|
181
182
|
if args.project_db:
|
|
182
|
-
|
|
183
|
+
_project_db_path = Path(args.project_db)
|
|
184
|
+
_project_db_instance = _init_db(_project_db_path)
|
|
183
185
|
else:
|
|
184
186
|
_proj = pantheon_project()
|
|
185
187
|
if _proj:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
188
|
+
_project_db_path = _proj / ".pantheon" / "persistence" / "project.db"
|
|
189
|
+
_project_db_instance = _init_db(_project_db_path)
|
|
189
190
|
_project_db = _project_db_instance
|
|
190
191
|
|
|
191
192
|
|
|
@@ -262,7 +263,7 @@ async def kv_get(
|
|
|
262
263
|
row = conn.execute(
|
|
263
264
|
"SELECT value FROM kv_store "
|
|
264
265
|
"WHERE namespace = ? AND key = ? "
|
|
265
|
-
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
266
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
266
267
|
"AND deleted_at IS NULL",
|
|
267
268
|
(namespace, key),
|
|
268
269
|
).fetchone()
|
|
@@ -289,13 +290,13 @@ async def kv_stats(
|
|
|
289
290
|
total = conn.execute("SELECT COUNT(*) FROM kv_store WHERE deleted_at IS NULL").fetchone()[0]
|
|
290
291
|
expired = conn.execute(
|
|
291
292
|
"SELECT COUNT(*) FROM kv_store "
|
|
292
|
-
"WHERE expires_at IS NOT NULL AND expires_at < datetime('now') "
|
|
293
|
+
"WHERE expires_at IS NOT NULL AND datetime(expires_at) < datetime('now') "
|
|
293
294
|
"AND deleted_at IS NULL"
|
|
294
295
|
).fetchone()[0]
|
|
295
296
|
|
|
296
297
|
ns_rows = conn.execute(
|
|
297
298
|
"SELECT namespace, COUNT(*) as cnt, "
|
|
298
|
-
"SUM(CASE WHEN expires_at IS NOT NULL AND expires_at < datetime('now') THEN 1 ELSE 0 END) as expired_count "
|
|
299
|
+
"SUM(CASE WHEN expires_at IS NOT NULL AND datetime(expires_at) < datetime('now') THEN 1 ELSE 0 END) as expired_count "
|
|
299
300
|
"FROM kv_store WHERE deleted_at IS NULL GROUP BY namespace ORDER BY cnt DESC"
|
|
300
301
|
).fetchall()
|
|
301
302
|
|
|
@@ -372,7 +373,7 @@ async def kv_list(
|
|
|
372
373
|
rows = conn.execute(
|
|
373
374
|
"SELECT key, value, created_at, expires_at FROM kv_store "
|
|
374
375
|
"WHERE namespace = ? AND key LIKE ? "
|
|
375
|
-
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
376
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
376
377
|
"AND deleted_at IS NULL "
|
|
377
378
|
"ORDER BY key LIMIT ?",
|
|
378
379
|
(namespace, f"{prefix}%", limit),
|
|
@@ -426,7 +427,7 @@ async def kv_search(
|
|
|
426
427
|
"JOIN kv_store ON kv_store_fts.rowid = kv_store.id "
|
|
427
428
|
"WHERE kv_store_fts MATCH ? "
|
|
428
429
|
"AND kv_store.deleted_at IS NULL "
|
|
429
|
-
"AND (kv_store.expires_at IS NULL OR kv_store.expires_at > datetime('now'))"
|
|
430
|
+
"AND (kv_store.expires_at IS NULL OR datetime(kv_store.expires_at) > datetime('now'))"
|
|
430
431
|
)
|
|
431
432
|
params: list[str | int] = [fts_query]
|
|
432
433
|
|
|
@@ -474,7 +475,7 @@ async def purge_expired(
|
|
|
474
475
|
expired = conn.execute(
|
|
475
476
|
"SELECT key FROM kv_store "
|
|
476
477
|
"WHERE expires_at IS NOT NULL "
|
|
477
|
-
"AND expires_at < datetime('now') "
|
|
478
|
+
"AND datetime(expires_at) < datetime('now') "
|
|
478
479
|
"AND deleted_at IS NULL",
|
|
479
480
|
).fetchall()
|
|
480
481
|
expired_keys = [r[0] for r in expired]
|
|
@@ -490,7 +491,7 @@ async def purge_expired(
|
|
|
490
491
|
conn.execute(
|
|
491
492
|
"UPDATE kv_store SET deleted_at = datetime('now') "
|
|
492
493
|
"WHERE expires_at IS NOT NULL "
|
|
493
|
-
"AND expires_at < datetime('now') "
|
|
494
|
+
"AND datetime(expires_at) < datetime('now') "
|
|
494
495
|
"AND deleted_at IS NULL",
|
|
495
496
|
)
|
|
496
497
|
conn.commit()
|
|
@@ -516,7 +517,7 @@ def _opportunistic_auto_purge(conn: sqlite3.Connection, namespace: str, threshol
|
|
|
516
517
|
conn.execute(
|
|
517
518
|
"UPDATE kv_store SET deleted_at = datetime('now') "
|
|
518
519
|
"WHERE namespace = ? AND expires_at IS NOT NULL "
|
|
519
|
-
"AND expires_at < datetime('now') AND deleted_at IS NULL",
|
|
520
|
+
"AND datetime(expires_at) < datetime('now') AND deleted_at IS NULL",
|
|
520
521
|
(namespace,),
|
|
521
522
|
)
|
|
522
523
|
conn.commit()
|
|
@@ -659,7 +660,7 @@ async def context_get(
|
|
|
659
660
|
row = conn.execute(
|
|
660
661
|
"SELECT value FROM kv_store "
|
|
661
662
|
"WHERE namespace = ? AND key = ? "
|
|
662
|
-
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
663
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
663
664
|
"AND deleted_at IS NULL",
|
|
664
665
|
(ns, key),
|
|
665
666
|
).fetchone()
|
|
@@ -691,7 +692,7 @@ async def context_list(
|
|
|
691
692
|
rows = conn.execute(
|
|
692
693
|
"SELECT key, created_at, expires_at FROM kv_store "
|
|
693
694
|
"WHERE namespace = ? "
|
|
694
|
-
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
695
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
695
696
|
"AND deleted_at IS NULL "
|
|
696
697
|
"ORDER BY created_at DESC LIMIT 50",
|
|
697
698
|
(ns,),
|
|
@@ -737,7 +738,7 @@ async def context_stats(
|
|
|
737
738
|
|
|
738
739
|
expired = conn.execute(
|
|
739
740
|
"SELECT COUNT(*) FROM kv_store "
|
|
740
|
-
"WHERE namespace = ? AND expires_at < datetime('now') "
|
|
741
|
+
"WHERE namespace = ? AND datetime(expires_at) < datetime('now') "
|
|
741
742
|
"AND deleted_at IS NULL",
|
|
742
743
|
(ns,),
|
|
743
744
|
).fetchone()[0]
|
|
@@ -773,15 +774,15 @@ async def context_stats(
|
|
|
773
774
|
}
|
|
774
775
|
|
|
775
776
|
def _resolve_db_path(scope: str) -> Path | None:
|
|
776
|
-
"""Resolve the database file path for a given scope.
|
|
777
|
+
"""Resolve the database file path for a given scope.
|
|
778
|
+
|
|
779
|
+
Returns the actual path used at init (honoring an explicit
|
|
780
|
+
``--project-db``), so deletelog writes and db_size stats land next to
|
|
781
|
+
the real database file.
|
|
782
|
+
"""
|
|
777
783
|
if scope == "global":
|
|
778
784
|
return _global_root / "global.db"
|
|
779
|
-
|
|
780
|
-
# Find the project root from the project DB path
|
|
781
|
-
proj_root = pantheon_project()
|
|
782
|
-
if proj_root:
|
|
783
|
-
return proj_root / ".pantheon" / "persistence" / "project.db"
|
|
784
|
-
return None
|
|
785
|
+
return _project_db_path
|
|
785
786
|
|
|
786
787
|
|
|
787
788
|
# ── Main Entrypoint ─────────────────────────────────────────────────────────────
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# Auto-generated: resolved symlink from ../src/mcp/mcp_resources_server.py
|
|
2
1
|
#!/usr/bin/env python3
|
|
3
2
|
"""Pantheon MCP Resources Server.
|
|
4
3
|
|
|
@@ -19,6 +18,7 @@ from typing import Any
|
|
|
19
18
|
|
|
20
19
|
import yaml
|
|
21
20
|
from _pantheon_paths import pantheon_home, pantheon_project
|
|
21
|
+
from eval_store import get_latest_eval, list_evals
|
|
22
22
|
from mcp.server.fastmcp import FastMCP
|
|
23
23
|
|
|
24
24
|
# ── Path Resolution ──────────────────────────────────────────────────────────
|
|
@@ -112,6 +112,8 @@ async def list_skills() -> str:
|
|
|
112
112
|
async def get_routing() -> str:
|
|
113
113
|
"""Return the full content of routing.yml."""
|
|
114
114
|
routing_file = _PANTHEON_HOME / "routing.yml"
|
|
115
|
+
if not routing_file.exists() and _PANTHEON_PROJECT:
|
|
116
|
+
routing_file = _PANTHEON_PROJECT / "src" / "routing.yml"
|
|
115
117
|
if not routing_file.exists():
|
|
116
118
|
return "routing.yml not found."
|
|
117
119
|
return routing_file.read_text(encoding="utf-8")
|
|
@@ -200,6 +202,63 @@ async def get_memory_bank(path: str) -> str:
|
|
|
200
202
|
return resolved.read_text(encoding="utf-8")
|
|
201
203
|
|
|
202
204
|
|
|
205
|
+
# ── Plugin Eval Resources ─────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _valid_plugin_name(name: str) -> bool:
|
|
209
|
+
"""Validate a plugin name for the eval resource (no traversal)."""
|
|
210
|
+
return bool(name) and "/" not in name and "\\" not in name and ".." not in name
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@mcp.resource(
|
|
214
|
+
"pantheon://eval",
|
|
215
|
+
description="List of evaluated plugins/skills with their latest "
|
|
216
|
+
"certification score (from the plugin_eval memory namespace)",
|
|
217
|
+
)
|
|
218
|
+
async def list_plugin_evals() -> str:
|
|
219
|
+
"""Return a markdown list of evaluated plugins with latest scores."""
|
|
220
|
+
entries = list_evals()
|
|
221
|
+
if not entries:
|
|
222
|
+
return "No plugin evals recorded yet."
|
|
223
|
+
|
|
224
|
+
lines = ["# Plugin Eval Certification", ""]
|
|
225
|
+
for entry in entries:
|
|
226
|
+
name = entry.get("name", "?")
|
|
227
|
+
score = entry.get("metadata", {}).get("score", "?")
|
|
228
|
+
created = entry.get("created_at", "")
|
|
229
|
+
lines.append(f"- **{name}** — score {score} (evaluated {created})")
|
|
230
|
+
return "\n".join(lines)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@mcp.resource(
|
|
234
|
+
"pantheon://eval/{plugin}",
|
|
235
|
+
description="Latest certification report for a plugin/skill "
|
|
236
|
+
"(from the plugin_eval memory namespace)",
|
|
237
|
+
)
|
|
238
|
+
async def get_plugin_eval(plugin: str) -> str:
|
|
239
|
+
"""Return the latest eval certification report for a plugin.
|
|
240
|
+
|
|
241
|
+
Security: plugin names containing path separators or traversal
|
|
242
|
+
sequences are rejected before any lookup.
|
|
243
|
+
"""
|
|
244
|
+
if not _valid_plugin_name(plugin):
|
|
245
|
+
return "Invalid plugin name: blocked."
|
|
246
|
+
|
|
247
|
+
entry = get_latest_eval(plugin)
|
|
248
|
+
if entry is None:
|
|
249
|
+
return f"No eval found for plugin '{plugin}'."
|
|
250
|
+
|
|
251
|
+
score = entry.get("metadata", {}).get("score", "?")
|
|
252
|
+
created = entry.get("created_at", "")
|
|
253
|
+
value = entry.get("value", "{}")
|
|
254
|
+
return (
|
|
255
|
+
f"# Eval Report: {plugin}\n\n"
|
|
256
|
+
f"**Score:** {score}\n"
|
|
257
|
+
f"**Evaluated:** {created}\n\n"
|
|
258
|
+
f"```json\n{value}\n```"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
203
262
|
# ── Main Entrypoint ───────────────────────────────────────────────────────────
|
|
204
263
|
|
|
205
264
|
if __name__ == "__main__":
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# Auto-generated: resolved symlink from ../src/mcp/memory_mcp_server.py
|
|
2
1
|
#!/usr/bin/env python3
|
|
3
2
|
"""Pantheon Memory MCP Server — lightweight, zero heavy deps.
|
|
4
3
|
|
|
@@ -26,6 +25,7 @@ import json
|
|
|
26
25
|
import os
|
|
27
26
|
import sqlite3
|
|
28
27
|
import time
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
@@ -187,10 +187,55 @@ def _parse_metadata(row: dict[str, Any]) -> dict[str, Any]:
|
|
|
187
187
|
_RRF_CONST = 60
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
def _parse_iso_ts(value: str) -> float:
|
|
191
|
+
"""Parse an ISO 8601 UTC timestamp to epoch seconds.
|
|
192
|
+
|
|
193
|
+
Handles both aware ISO strings (``2026-08-21T10:00:00Z`` /
|
|
194
|
+
``...+00:00``) and legacy naive rows from ``DEFAULT (datetime('now'))``
|
|
195
|
+
(``YYYY-MM-DD HH:MM:SS``, space separator, no TZ). Naive timestamps are
|
|
196
|
+
assumed UTC — SQLite's ``datetime('now')`` is UTC, so interpreting them
|
|
197
|
+
in the local timezone would skew freshness decay.
|
|
198
|
+
"""
|
|
199
|
+
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
200
|
+
if dt.tzinfo is None:
|
|
201
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
202
|
+
return dt.timestamp()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _fetch_created_at_map(
|
|
206
|
+
db: sqlite3.Connection, ids: list[int]
|
|
207
|
+
) -> dict[int, str]:
|
|
208
|
+
"""Fetch created_at timestamps for the given memory IDs.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
db: Active SQLite connection.
|
|
212
|
+
ids: Memory row IDs to look up.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Mapping of id -> created_at ISO string. Empty on failure.
|
|
216
|
+
"""
|
|
217
|
+
created_at_map: dict[int, str] = {}
|
|
218
|
+
if not ids:
|
|
219
|
+
return created_at_map
|
|
220
|
+
try:
|
|
221
|
+
placeholders = ",".join("?" * len(ids))
|
|
222
|
+
rows = db.execute(
|
|
223
|
+
f"SELECT id, created_at FROM memories "
|
|
224
|
+
f"WHERE id IN ({placeholders})",
|
|
225
|
+
ids,
|
|
226
|
+
).fetchall()
|
|
227
|
+
created_at_map = {r["id"]: r["created_at"] for r in rows}
|
|
228
|
+
except Exception:
|
|
229
|
+
created_at_map = {}
|
|
230
|
+
return created_at_map
|
|
231
|
+
|
|
232
|
+
|
|
190
233
|
def _rrf_fuse(
|
|
191
234
|
vec_ids: list[int],
|
|
192
235
|
fts_ids: list[int],
|
|
193
236
|
top_k: int,
|
|
237
|
+
created_at_map: dict[int, str] | None = None,
|
|
238
|
+
decay_days: float | None = None,
|
|
194
239
|
) -> list[tuple[int, float]]:
|
|
195
240
|
"""Reciprocal Rank Fusion of vector and FTS5 result ID lists.
|
|
196
241
|
|
|
@@ -198,6 +243,12 @@ def _rrf_fuse(
|
|
|
198
243
|
vec_ids: Ordered list of IDs from vector search (most relevant first).
|
|
199
244
|
fts_ids: Ordered list of IDs from FTS5 search (most relevant first).
|
|
200
245
|
top_k: Maximum number of fused results to return.
|
|
246
|
+
created_at_map: Optional mapping of doc_id -> created_at ISO string,
|
|
247
|
+
used to compute freshness when ``decay_days`` is set.
|
|
248
|
+
decay_days: Optional freshness half-life in days. When set, each fused
|
|
249
|
+
score is multiplied by ``freshness = 2^(-days/decay_days)`` so
|
|
250
|
+
older entries rank lower. Default None = no decay (backward
|
|
251
|
+
compatible).
|
|
201
252
|
|
|
202
253
|
Returns:
|
|
203
254
|
List of (id, rrf_score) tuples sorted by descending score.
|
|
@@ -207,6 +258,19 @@ def _rrf_fuse(
|
|
|
207
258
|
scores[doc_id] = 1.0 / (_RRF_CONST + rank)
|
|
208
259
|
for rank, doc_id in enumerate(fts_ids, start=1):
|
|
209
260
|
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (_RRF_CONST + rank)
|
|
261
|
+
|
|
262
|
+
if decay_days is not None and decay_days > 0 and created_at_map:
|
|
263
|
+
now = time.time()
|
|
264
|
+
for doc_id in list(scores):
|
|
265
|
+
created = created_at_map.get(doc_id)
|
|
266
|
+
if not created:
|
|
267
|
+
continue
|
|
268
|
+
try:
|
|
269
|
+
days = max(0.0, (now - _parse_iso_ts(created)) / 86400.0)
|
|
270
|
+
except (ValueError, TypeError):
|
|
271
|
+
continue
|
|
272
|
+
scores[doc_id] *= 2.0 ** (-days / decay_days)
|
|
273
|
+
|
|
210
274
|
ranked = sorted(scores.items(), key=lambda x: (-x[1], x[0]))
|
|
211
275
|
return ranked[:top_k]
|
|
212
276
|
|
|
@@ -283,12 +347,15 @@ def memory_store(
|
|
|
283
347
|
@mcp.tool(
|
|
284
348
|
description="Hybrid semantic search across memories. "
|
|
285
349
|
"Combines vector cosine similarity and FTS5 BM25 keyword search "
|
|
286
|
-
"via Reciprocal Rank Fusion (RRF)."
|
|
350
|
+
"via Reciprocal Rank Fusion (RRF). Optional decay_days applies a "
|
|
351
|
+
"freshness half-life (2^(-days/decay_days)) so recent entries rank "
|
|
352
|
+
"higher; default None keeps the original rank-only behavior.",
|
|
287
353
|
)
|
|
288
354
|
def memory_search( # noqa: PLR0912
|
|
289
355
|
query: str,
|
|
290
356
|
namespace: str | None = None,
|
|
291
357
|
top_k: int = 5,
|
|
358
|
+
decay_days: float | None = None,
|
|
292
359
|
) -> list[dict[str, Any]]:
|
|
293
360
|
"""Hybrid search: vector + keyword fused via RRF.
|
|
294
361
|
|
|
@@ -300,6 +367,9 @@ def memory_search( # noqa: PLR0912
|
|
|
300
367
|
query: Search query text.
|
|
301
368
|
namespace: Optional namespace filter.
|
|
302
369
|
top_k: Maximum results (default 5, max 50).
|
|
370
|
+
decay_days: Optional freshness half-life in days. When set, fused
|
|
371
|
+
scores are multiplied by ``2^(-days_since_created/decay_days)``
|
|
372
|
+
so older entries rank lower. Default None = no decay.
|
|
303
373
|
|
|
304
374
|
Returns:
|
|
305
375
|
List of memory entries with score and metadata.
|
|
@@ -365,8 +435,14 @@ def memory_search( # noqa: PLR0912
|
|
|
365
435
|
except Exception:
|
|
366
436
|
fts_ids = []
|
|
367
437
|
|
|
368
|
-
# 4. RRF fusion
|
|
369
|
-
|
|
438
|
+
# 4. RRF fusion (optionally freshness-decayed)
|
|
439
|
+
candidate_ids = list(dict.fromkeys([*vec_ids, *fts_ids]))
|
|
440
|
+
created_at_map = (
|
|
441
|
+
_fetch_created_at_map(db, candidate_ids)
|
|
442
|
+
if decay_days is not None and decay_days > 0
|
|
443
|
+
else {}
|
|
444
|
+
)
|
|
445
|
+
fused = _rrf_fuse(vec_ids, fts_ids, top_k, created_at_map, decay_days)
|
|
370
446
|
|
|
371
447
|
if not fused:
|
|
372
448
|
return []
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -5,43 +5,45 @@
|
|
|
5
5
|
* Runs after `npm install` completes. Validates environment and prints
|
|
6
6
|
* setup instructions. Does NOT install Pantheon platform components.
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync } from 'node:fs'
|
|
9
|
-
import { join } from 'node:path'
|
|
10
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
11
|
|
|
12
|
-
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
13
|
-
const
|
|
12
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
13
|
+
const _ROOT = join(__dirname, '..')
|
|
14
14
|
|
|
15
|
-
const REQUIRED_NODE_MAJOR = 18
|
|
16
|
-
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10)
|
|
15
|
+
const REQUIRED_NODE_MAJOR = 18
|
|
16
|
+
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10)
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
19
|
// Validate Node.js version
|
|
20
20
|
if (nodeMajor < REQUIRED_NODE_MAJOR) {
|
|
21
|
-
console.error(
|
|
22
|
-
|
|
21
|
+
console.error(
|
|
22
|
+
`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`,
|
|
23
|
+
)
|
|
24
|
+
process.exit(1)
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
// Validate js-yaml is available
|
|
26
28
|
try {
|
|
27
|
-
await import('js-yaml')
|
|
29
|
+
await import('js-yaml')
|
|
28
30
|
} catch {
|
|
29
|
-
console.warn('⚠️ js-yaml not found — run `npm install` to install dependencies')
|
|
31
|
+
console.warn('⚠️ js-yaml not found — run `npm install` to install dependencies')
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
console.log('')
|
|
33
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
34
|
-
console.log(' Pantheon OpenCode — npm dependencies installed ✅')
|
|
35
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
36
|
-
console.log('')
|
|
37
|
-
console.log(' Next steps:')
|
|
38
|
-
console.log(' 1. Configure Pantheon:')
|
|
39
|
-
console.log(' npm run setup')
|
|
40
|
-
console.log(' # or: npx pantheon-opencode init')
|
|
41
|
-
console.log(' 2. Verify installation:')
|
|
42
|
-
console.log(' npm run doctor')
|
|
43
|
-
console.log('')
|
|
34
|
+
console.log('')
|
|
35
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
36
|
+
console.log(' Pantheon OpenCode — npm dependencies installed ✅')
|
|
37
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
38
|
+
console.log('')
|
|
39
|
+
console.log(' Next steps:')
|
|
40
|
+
console.log(' 1. Configure Pantheon:')
|
|
41
|
+
console.log(' npm run setup')
|
|
42
|
+
console.log(' # or: npx pantheon-opencode init')
|
|
43
|
+
console.log(' 2. Verify installation:')
|
|
44
|
+
console.log(' npm run doctor')
|
|
45
|
+
console.log('')
|
|
44
46
|
} catch (err) {
|
|
45
|
-
console.error(`❌ Post-install check failed: ${err.message}`)
|
|
46
|
-
process.exit(1)
|
|
47
|
+
console.error(`❌ Post-install check failed: ${err.message}`)
|
|
48
|
+
process.exit(1)
|
|
47
49
|
}
|
package/scripts/prune.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
34
34
|
import { fileURLToPath } from 'node:url'
|
|
35
35
|
|
|
36
36
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
37
|
-
const
|
|
37
|
+
const _ROOT = join(__dirname, '..')
|
|
38
38
|
const DEFAULT_BACKUP_AGE_DAYS = 30
|
|
39
39
|
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
@@ -30,7 +30,9 @@ function scanFile(path) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function scanTarball(path) {
|
|
33
|
-
const entries = execFileSync('tar', ['-tzf', path], { encoding: 'utf8' })
|
|
33
|
+
const entries = execFileSync('tar', ['-tzf', path], { encoding: 'utf8' })
|
|
34
|
+
.split('\n')
|
|
35
|
+
.filter(Boolean)
|
|
34
36
|
const result = []
|
|
35
37
|
for (const entry of entries) {
|
|
36
38
|
const content = execFileSync('tar', ['-xOzf', path, entry])
|
package/scripts/uninstall.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/** Status/exit policy shared by installation validation and its tests. */
|
|
2
|
-
export const VALIDATION_STATUS = Object.freeze({
|
|
2
|
+
export const VALIDATION_STATUS = Object.freeze({
|
|
3
|
+
PASS: 'PASS',
|
|
4
|
+
WARN: 'WARN',
|
|
5
|
+
ERROR: 'ERROR',
|
|
6
|
+
SKIP: 'SKIP',
|
|
7
|
+
})
|
|
3
8
|
|
|
4
9
|
export function classifyDoctorExit(exitStatus, output = '') {
|
|
5
10
|
if (exitStatus === 0) return VALIDATION_STATUS.PASS
|
|
@@ -26,7 +26,11 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
26
26
|
export const MANIFESTS = [
|
|
27
27
|
{ name: 'pyproject.toml', file: 'pyproject.toml', kind: 'toml' },
|
|
28
28
|
{ name: 'plugin.json', file: 'plugin.json', kind: 'json' },
|
|
29
|
-
{
|
|
29
|
+
{
|
|
30
|
+
name: 'src/plugins/tui/package.json',
|
|
31
|
+
file: join('src', 'plugins', 'tui', 'package.json'),
|
|
32
|
+
kind: 'json',
|
|
33
|
+
},
|
|
30
34
|
]
|
|
31
35
|
|
|
32
36
|
function readTomlVersion(filePath) {
|
|
@@ -109,8 +113,7 @@ function printDivergenceTable(result) {
|
|
|
109
113
|
console.log('')
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
const isDirectRun =
|
|
113
|
-
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
116
|
+
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
114
117
|
|
|
115
118
|
if (isDirectRun) {
|
|
116
119
|
// npm scripts run from the package root, so the working directory is the
|
|
@@ -138,7 +141,9 @@ if (isDirectRun) {
|
|
|
138
141
|
console.log(`✓ Synced ${changed} manifest(s) to v${after.source}`)
|
|
139
142
|
process.exit(0)
|
|
140
143
|
}
|
|
141
|
-
console.error(
|
|
144
|
+
console.error(
|
|
145
|
+
`version-check: sync incomplete — ${after.manifests.filter((m) => !m.ok).length} manifest(s) still divergent`,
|
|
146
|
+
)
|
|
142
147
|
process.exit(1)
|
|
143
148
|
}
|
|
144
149
|
console.error('version-check: manifests out of sync (run with --fix to sync)')
|
package/src/agents/aphrodite.md
CHANGED
package/src/agents/apollo.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
name: apollo
|
|
3
3
|
description: "Read-only investigation scout — 3-10 parallel searches across codebase, external docs, and GitHub. Called by: athena, zeus, hermes, aphrodite, demeter. No edits, no commands."
|
|
4
4
|
mode: subagent
|
|
5
|
+
visible: false
|
|
6
|
+
tools:
|
|
7
|
+
task: false
|
|
5
8
|
reasoning_effort: low
|
|
6
9
|
permission:
|
|
7
10
|
read: allow
|
package/src/agents/demeter.md
CHANGED
package/src/agents/gaia.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
name: gaia
|
|
3
3
|
description: "Remote sensing domain specialist — satellite image processing, spectral analysis, SAR, change detection, time series, ML/DL classification. Read-only analysis of geospatial data."
|
|
4
4
|
mode: subagent
|
|
5
|
+
visible: false
|
|
6
|
+
tools:
|
|
7
|
+
task: false
|
|
5
8
|
reasoning_effort: low
|
|
6
9
|
permission:
|
|
7
10
|
read: allow
|
package/src/agents/hephaestus.md
CHANGED
package/src/agents/hermes.md
CHANGED
package/src/agents/themis.md
CHANGED
|
@@ -97,6 +97,10 @@ Verificacoes rapidas de estilo e sanidade:
|
|
|
97
97
|
- [ ] Formatacao consistente (mesmo style guide)
|
|
98
98
|
- [ ] Nomes de variaveis/funcoes descritivos
|
|
99
99
|
- [ ] Comentarios TODO ou FIXME sem resolucao
|
|
100
|
+
- [ ] Comment Checker: codigo gerado indistinguivel de humano
|
|
101
|
+
- Flag comentarios obvios/excessivos (ex: `// increment i`, `# set x to 5`)
|
|
102
|
+
- Flag codigo morto comentado (blocos inteiros desativados)
|
|
103
|
+
- Flag cabecalhos boilerplate (autor/data/licenca repetitivos)
|
|
100
104
|
|
|
101
105
|
Tempo estimado: ~10% do esforco de review
|
|
102
106
|
|
package/src/agents/zeus.md
CHANGED
|
@@ -37,7 +37,7 @@ skills:
|
|
|
37
37
|
|
|
38
38
|
## Golden Rule
|
|
39
39
|
|
|
40
|
-
**Coordenador APENAS.**
|
|
40
|
+
**Coordenador APENAS. Zeus NUNCA lê arquivos de código-fonte.** Toda leitura de `src/`, `tests/`, `scripts/` vai para @apollo. Leia apenas: config, docs, memory bank, delegação.
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
## 🔒 Fusion-Style Enforcement
|
|
@@ -324,6 +324,10 @@ out = await guard.maybeRetry(result, async () => { ...task() de novo... })
|
|
|
324
324
|
Regra: retry 1x APENAS em `empty-mode1`/`empty-mode2`. Resultado com conteudo
|
|
325
325
|
nunca redispara. Apos 1 retry vazio → escalate (mesma regra do TODO Enforcer).
|
|
326
326
|
|
|
327
|
+
> **Waves DEVEM usar `zeusDelegateWithRetry`** (`src/pantheon/zeus-delegate-with-retry.ts`) — encapsula `delegate → waitForTerminal → classify → retry 1x → escalate` com cap duro 1; se ainda vazio throw `ZeusEscalationError` (escale: tente outro agente, simplifique, manual).
|
|
328
|
+
> Uso alto-nível: `import { zeusDelegateWithRetry } from './pantheon/zeus-delegate-with-retry.ts'` → `await zeusDelegateWithRetry({ board, client, sessionID, agent, prompt })`.
|
|
329
|
+
> Uso baixo-nível: `createZeusRetryHelper().executeWithRetry(first, () => secondRead())` — classifica empty e decide retry/escalate (mesmo cap 1).
|
|
330
|
+
|
|
327
331
|
### /cost — pantheon_cost tool (WIRED no plugin)
|
|
328
332
|
`pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY (node:sqlite;
|
|
329
333
|
fallback `scripts/cost.mjs`) e devolve tabela markdown de custo + tokens por
|