pantheon-opencode 1.3.5 → 1.3.7
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 +33 -1
- package/package.json +3 -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 +26 -13
- 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 +2 -0
- package/scripts/install/shared.mjs +6 -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/sync-tui.mjs +148 -0
- 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/demeter.md +1 -1
- package/src/agents/hephaestus.md +1 -1
- package/src/agents/hermes.md +1 -1
- package/src/agents/themis.md +4 -0
- 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 +21 -21
- package/src/pantheon/delegation-finalize.ts +44 -4
- package/src/pantheon/delegation-notify.ts +97 -0
- package/src/pantheon/delegation.ts +413 -23
- package/src/pantheon/dispatch-guard.ts +2 -0
- package/src/pantheon/file-persistence.ts +7 -3
- 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 +134 -0
- package/src/pantheon/retry-policy.ts +212 -0
- package/src/pantheon/session-guard.ts +1 -3
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/tui-version-check.ts +72 -0
- package/src/pantheon/vision.ts +7 -5
- package/src/pantheon/zeus-delegate-with-retry.ts +81 -12
- package/src/plugin.ts +57 -3
- 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 +19 -14
- package/src/plugins/tui/dist/tui.tsx +52 -18
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +106 -24
- package/src/plugins/tui/tsdown.config.ts +1 -1
- package/src/routing.yml +42 -0
|
@@ -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 ─────────────────────────────────────────────────────────────
|
|
@@ -18,6 +18,7 @@ from typing import Any
|
|
|
18
18
|
|
|
19
19
|
import yaml
|
|
20
20
|
from _pantheon_paths import pantheon_home, pantheon_project
|
|
21
|
+
from eval_store import get_latest_eval, list_evals
|
|
21
22
|
from mcp.server.fastmcp import FastMCP
|
|
22
23
|
|
|
23
24
|
# ── Path Resolution ──────────────────────────────────────────────────────────
|
|
@@ -201,6 +202,63 @@ async def get_memory_bank(path: str) -> str:
|
|
|
201
202
|
return resolved.read_text(encoding="utf-8")
|
|
202
203
|
|
|
203
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
|
+
|
|
204
262
|
# ── Main Entrypoint ───────────────────────────────────────────────────────────
|
|
205
263
|
|
|
206
264
|
if __name__ == "__main__":
|
|
@@ -25,6 +25,7 @@ import json
|
|
|
25
25
|
import os
|
|
26
26
|
import sqlite3
|
|
27
27
|
import time
|
|
28
|
+
from datetime import datetime, timezone
|
|
28
29
|
from pathlib import Path
|
|
29
30
|
from typing import Any
|
|
30
31
|
|
|
@@ -186,10 +187,55 @@ def _parse_metadata(row: dict[str, Any]) -> dict[str, Any]:
|
|
|
186
187
|
_RRF_CONST = 60
|
|
187
188
|
|
|
188
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
|
+
|
|
189
233
|
def _rrf_fuse(
|
|
190
234
|
vec_ids: list[int],
|
|
191
235
|
fts_ids: list[int],
|
|
192
236
|
top_k: int,
|
|
237
|
+
created_at_map: dict[int, str] | None = None,
|
|
238
|
+
decay_days: float | None = None,
|
|
193
239
|
) -> list[tuple[int, float]]:
|
|
194
240
|
"""Reciprocal Rank Fusion of vector and FTS5 result ID lists.
|
|
195
241
|
|
|
@@ -197,6 +243,12 @@ def _rrf_fuse(
|
|
|
197
243
|
vec_ids: Ordered list of IDs from vector search (most relevant first).
|
|
198
244
|
fts_ids: Ordered list of IDs from FTS5 search (most relevant first).
|
|
199
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).
|
|
200
252
|
|
|
201
253
|
Returns:
|
|
202
254
|
List of (id, rrf_score) tuples sorted by descending score.
|
|
@@ -206,6 +258,19 @@ def _rrf_fuse(
|
|
|
206
258
|
scores[doc_id] = 1.0 / (_RRF_CONST + rank)
|
|
207
259
|
for rank, doc_id in enumerate(fts_ids, start=1):
|
|
208
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
|
+
|
|
209
274
|
ranked = sorted(scores.items(), key=lambda x: (-x[1], x[0]))
|
|
210
275
|
return ranked[:top_k]
|
|
211
276
|
|
|
@@ -282,12 +347,15 @@ def memory_store(
|
|
|
282
347
|
@mcp.tool(
|
|
283
348
|
description="Hybrid semantic search across memories. "
|
|
284
349
|
"Combines vector cosine similarity and FTS5 BM25 keyword search "
|
|
285
|
-
"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.",
|
|
286
353
|
)
|
|
287
354
|
def memory_search( # noqa: PLR0912
|
|
288
355
|
query: str,
|
|
289
356
|
namespace: str | None = None,
|
|
290
357
|
top_k: int = 5,
|
|
358
|
+
decay_days: float | None = None,
|
|
291
359
|
) -> list[dict[str, Any]]:
|
|
292
360
|
"""Hybrid search: vector + keyword fused via RRF.
|
|
293
361
|
|
|
@@ -299,6 +367,9 @@ def memory_search( # noqa: PLR0912
|
|
|
299
367
|
query: Search query text.
|
|
300
368
|
namespace: Optional namespace filter.
|
|
301
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.
|
|
302
373
|
|
|
303
374
|
Returns:
|
|
304
375
|
List of memory entries with score and metadata.
|
|
@@ -364,8 +435,14 @@ def memory_search( # noqa: PLR0912
|
|
|
364
435
|
except Exception:
|
|
365
436
|
fts_ids = []
|
|
366
437
|
|
|
367
|
-
# 4. RRF fusion
|
|
368
|
-
|
|
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)
|
|
369
446
|
|
|
370
447
|
if not fused:
|
|
371
448
|
return []
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* @module auto-wake
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { existsSync } from 'node:fs'
|
|
18
19
|
import { readdir, readFile, rename } from 'node:fs/promises'
|
|
19
20
|
import { join } from 'node:path'
|
|
20
|
-
import { existsSync } from 'node:fs'
|
|
21
21
|
|
|
22
22
|
/** Signal structure written by BackgroundJobBoard.writeSignal(). */
|
|
23
23
|
export interface WakeSignal {
|
|
@@ -48,9 +48,7 @@ const DEFAULT_SIGNAL_DIR = '.pantheon/deepwork/board-signals'
|
|
|
48
48
|
* @param signalDir Directory to scan (default: `.pantheon/deepwork/board-signals`)
|
|
49
49
|
* @returns List of wake signals from newly consumed files
|
|
50
50
|
*/
|
|
51
|
-
export async function consumeWakeSignals(
|
|
52
|
-
signalDir?: string,
|
|
53
|
-
): Promise<WakeSignal[]> {
|
|
51
|
+
export async function consumeWakeSignals(signalDir?: string): Promise<WakeSignal[]> {
|
|
54
52
|
const dir = signalDir ?? DEFAULT_SIGNAL_DIR
|
|
55
53
|
|
|
56
54
|
// If the signal directory doesn't exist, there are no signals
|
|
@@ -23,7 +23,14 @@ const log = createPantheonLogger({ module: 'BackgroundJobBoard' })
|
|
|
23
23
|
// ─── Types ─────────────────────────────────────────────────────────────
|
|
24
24
|
|
|
25
25
|
/** Valid states for a background job. */
|
|
26
|
-
export type BackgroundJobState =
|
|
26
|
+
export type BackgroundJobState =
|
|
27
|
+
| 'running'
|
|
28
|
+
| 'completed'
|
|
29
|
+
| 'error'
|
|
30
|
+
| 'startup_failed'
|
|
31
|
+
| 'startup_unknown'
|
|
32
|
+
| 'cancelled'
|
|
33
|
+
| 'reconciled'
|
|
27
34
|
|
|
28
35
|
/** Terminal states that can transition to reconciled. */
|
|
29
36
|
const TERMINAL_STATES: ReadonlySet<BackgroundJobState> = new Set([
|
|
@@ -38,9 +45,11 @@ const TERMINAL_STATES: ReadonlySet<BackgroundJobState> = new Set([
|
|
|
38
45
|
* Only running → terminal, or same-terminal → same-terminal (idempotent re-notify).
|
|
39
46
|
*/
|
|
40
47
|
const UPDATE_TRANSITIONS: Record<BackgroundJobState, ReadonlySet<BackgroundJobState>> = {
|
|
41
|
-
running: new Set(['completed', 'error', 'cancelled']),
|
|
48
|
+
running: new Set(['completed', 'error', 'startup_failed', 'startup_unknown', 'cancelled']),
|
|
42
49
|
completed: new Set(['completed']),
|
|
43
50
|
error: new Set(['error']),
|
|
51
|
+
startup_failed: new Set(['startup_failed']),
|
|
52
|
+
startup_unknown: new Set(['startup_unknown']),
|
|
44
53
|
cancelled: new Set(['cancelled']),
|
|
45
54
|
reconciled: new Set([]),
|
|
46
55
|
}
|
|
@@ -101,7 +110,7 @@ export interface LaunchInput {
|
|
|
101
110
|
/** Input for updating a job's status to a terminal state. */
|
|
102
111
|
export interface StatusInput {
|
|
103
112
|
taskID: string
|
|
104
|
-
state: 'completed' | 'error' | 'cancelled'
|
|
113
|
+
state: 'completed' | 'error' | 'startup_failed' | 'startup_unknown' | 'cancelled'
|
|
105
114
|
resultSummary?: string
|
|
106
115
|
error?: string
|
|
107
116
|
timedOut?: boolean
|
|
@@ -289,7 +298,11 @@ export class BackgroundJobBoard {
|
|
|
289
298
|
if (input.timedOut !== undefined) {
|
|
290
299
|
job.timedOut = input.timedOut
|
|
291
300
|
}
|
|
292
|
-
if (
|
|
301
|
+
if (
|
|
302
|
+
input.state === 'error' ||
|
|
303
|
+
input.state === 'startup_failed' ||
|
|
304
|
+
input.state === 'startup_unknown'
|
|
305
|
+
) {
|
|
293
306
|
job.totalErrors++
|
|
294
307
|
}
|
|
295
308
|
if (input.timedOut) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment Checker — detects trivial/auto-generated comments that make code
|
|
3
|
+
* look machine-produced rather than human-written.
|
|
4
|
+
*
|
|
5
|
+
* Used by @themis (Layer 1: Surface review) to flag comments that add no
|
|
6
|
+
* information value and signal AI-generated code.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Regex patterns matching trivial, uninformative comments. */
|
|
10
|
+
export const TRIVIAL_PATTERNS: readonly RegExp[] = [
|
|
11
|
+
/#\s*increment\s+\w+/i,
|
|
12
|
+
/#\s*decrement\s+\w+/i,
|
|
13
|
+
/#\s*set\s+\w+/i,
|
|
14
|
+
/#\s*loop\s+over/i,
|
|
15
|
+
/#\s*(initialize|init)\s+\w+/i,
|
|
16
|
+
/#\s*(return|yield)\s+\w+/i,
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export interface CommentCheckResult {
|
|
20
|
+
/** Number of trivial comments found (0 = clean). */
|
|
21
|
+
score: number
|
|
22
|
+
/** Human-readable descriptions of each flagged comment. */
|
|
23
|
+
flags: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Scan source code for trivial comments that add no information value.
|
|
28
|
+
*
|
|
29
|
+
* Returns a score (count of trivial comments) and a list of flags describing
|
|
30
|
+
* each match. A score of 0 means no trivial comments were detected.
|
|
31
|
+
*/
|
|
32
|
+
export function checkCommentDensity(source: string): CommentCheckResult {
|
|
33
|
+
const flags: string[] = []
|
|
34
|
+
const lines = source.split('\n')
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
const line = lines[i] as string
|
|
38
|
+
for (const pattern of TRIVIAL_PATTERNS) {
|
|
39
|
+
if (pattern.test(line)) {
|
|
40
|
+
const trimmed = line.trim()
|
|
41
|
+
flags.push(`Line ${i + 1}: trivial comment — "${trimmed}"`)
|
|
42
|
+
break // one flag per line, even if multiple patterns match
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { score: flags.length, flags }
|
|
48
|
+
}
|