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.
Files changed (75) hide show
  1. package/README.md +38 -6
  2. package/package.json +4 -3
  3. package/scripts/_pantheon_paths.py +0 -1
  4. package/scripts/build-agents-md.mjs +4 -2
  5. package/scripts/code_mode_server.py +307 -21
  6. package/scripts/doctor.mjs +58 -15
  7. package/scripts/hooks/format-multi-language.sh +6 -3
  8. package/scripts/hooks/log-session-start.sh +6 -0
  9. package/scripts/hooks/on-subagent-delegation-start.sh +22 -6
  10. package/scripts/hooks/validate-post-conditions.sh +1 -1
  11. package/scripts/hooks/validate-tool-safety.sh +5 -2
  12. package/scripts/install/agents-md.mjs +3 -1
  13. package/scripts/install/cli-ui.mjs +24 -4
  14. package/scripts/install/interactive.mjs +18 -7
  15. package/scripts/install/migrate-config.mjs +13 -19
  16. package/scripts/install/migrate.mjs +8 -5
  17. package/scripts/install/model-picker.mjs +4 -1
  18. package/scripts/install/opencode.mjs +8 -2
  19. package/scripts/install/shared.mjs +8 -4
  20. package/scripts/install/state.mjs +9 -6
  21. package/scripts/mcp_persistence_server.py +23 -22
  22. package/scripts/mcp_resources_server.py +60 -1
  23. package/scripts/memory_mcp_server.py +80 -4
  24. package/scripts/postinstall.mjs +27 -25
  25. package/scripts/prune.mjs +1 -1
  26. package/scripts/redaction-gate.mjs +3 -1
  27. package/scripts/uninstall.mjs +0 -2
  28. package/scripts/validation-policy.mjs +6 -1
  29. package/scripts/version-check.mjs +9 -4
  30. package/src/agents/aphrodite.md +1 -1
  31. package/src/agents/apollo.md +3 -0
  32. package/src/agents/demeter.md +1 -1
  33. package/src/agents/gaia.md +3 -0
  34. package/src/agents/hephaestus.md +1 -1
  35. package/src/agents/hermes.md +1 -1
  36. package/src/agents/themis.md +4 -0
  37. package/src/agents/zeus.md +5 -1
  38. package/src/mcp/code_mode_server.py +304 -18
  39. package/src/mcp/eval_store.py +233 -0
  40. package/src/mcp/mcp_persistence_server.py +23 -22
  41. package/src/mcp/mcp_resources_server.py +58 -0
  42. package/src/mcp/memory_mcp_server.py +80 -3
  43. package/src/pantheon/auto-wake.ts +2 -4
  44. package/src/pantheon/background-job-board.ts +17 -4
  45. package/src/pantheon/comment-checker.ts +48 -0
  46. package/src/pantheon/delegation-classifier.ts +307 -0
  47. package/src/pantheon/delegation-enforce.ts +67 -3
  48. package/src/pantheon/delegation-finalize.ts +44 -4
  49. package/src/pantheon/delegation.ts +413 -30
  50. package/src/pantheon/dispatch-guard.ts +2 -0
  51. package/src/pantheon/file-persistence.ts +7 -3
  52. package/src/pantheon/goal-loop.ts +13 -2
  53. package/src/pantheon/permission-eval.ts +284 -0
  54. package/src/pantheon/permission-globs.ts +86 -0
  55. package/src/pantheon/presets.d.mts +41 -0
  56. package/src/pantheon/presets.mjs +135 -0
  57. package/src/pantheon/retry-policy.ts +212 -0
  58. package/src/pantheon/session-guard.ts +48 -0
  59. package/src/pantheon/step-cap.ts +110 -0
  60. package/src/pantheon/todo-enforcer.ts +31 -5
  61. package/src/pantheon/todo-preserve.ts +7 -1
  62. package/src/pantheon/vision.ts +7 -5
  63. package/src/pantheon/zeus-delegate-with-retry.ts +401 -0
  64. package/src/plugin.ts +72 -13
  65. package/src/plugins/hook-runner.ts +1 -1
  66. package/src/plugins/pantheon-hooks.ts +1 -1
  67. package/src/plugins/tui/dist/tui.d.ts +1 -1
  68. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  69. package/src/plugins/tui/dist/tui.js +36 -14
  70. package/src/plugins/tui/dist/tui.tsx +71 -16
  71. package/src/plugins/tui/package.json +1 -1
  72. package/src/plugins/tui/src/index.tsx +71 -16
  73. package/src/plugins/tui/tsdown.config.ts +1 -1
  74. package/src/routing.yml +47 -0
  75. 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
- _project_db_instance = _init_db(Path(args.project_db))
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
- _project_db_instance = _init_db(
187
- _proj / ".pantheon" / "persistence" / "project.db"
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
- if _project_db_instance:
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
- fused = _rrf_fuse(vec_ids, fts_ids, top_k)
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 []
@@ -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 ROOT = join(__dirname, '..');
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(`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`);
22
- process.exit(1);
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 ROOT = join(__dirname, '..')
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' }).split('\n').filter(Boolean)
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])
@@ -37,13 +37,11 @@ const PANTHEON_AGENT_NAMES = [
37
37
  'prometheus',
38
38
  'hephaestus',
39
39
 
40
-
41
40
  'nyx',
42
41
  'gaia',
43
42
  'iris',
44
43
  'mnemosyne',
45
44
  'talos',
46
-
47
45
  ]
48
46
 
49
47
  const PLATFORM_LABELS = {
@@ -1,5 +1,10 @@
1
1
  /** Status/exit policy shared by installation validation and its tests. */
2
- export const VALIDATION_STATUS = Object.freeze({ PASS: 'PASS', WARN: 'WARN', ERROR: 'ERROR', SKIP: 'SKIP' })
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
- { name: 'src/plugins/tui/package.json', file: join('src', 'plugins', 'tui', 'package.json'), kind: 'json' },
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(`version-check: sync incomplete — ${after.manifests.filter((m) => !m.ok).length} manifest(s) still divergent`)
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)')
@@ -14,7 +14,7 @@ permission:
14
14
  task:
15
15
  "*": deny
16
16
  temperature: 0.3
17
- steps: 50
17
+ steps: 70
18
18
  skills:
19
19
  - tdd-with-agents
20
20
  - visual-review-pipeline
@@ -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
@@ -12,7 +12,7 @@ permission:
12
12
  task:
13
13
  "*": deny
14
14
  temperature: 0.2
15
- steps: 40
15
+ steps: 60
16
16
  skills:
17
17
  - tdd-with-agents
18
18
  - incremental-implementation
@@ -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
@@ -12,7 +12,7 @@ permission:
12
12
  task:
13
13
  "*": deny
14
14
  temperature: 0.3
15
- steps: 40
15
+ steps: 60
16
16
  skills:
17
17
  - tdd-with-agents
18
18
  - auto-continue
@@ -11,7 +11,7 @@ mcp_tools:
11
11
  pantheon-memory: [memory_search]
12
12
  pantheon-code-mode: [execute_code_script]
13
13
  temperature: 0.3
14
- steps: 50
14
+ steps: 70
15
15
  skills:
16
16
  - tdd-with-agents
17
17
  - file-prompts
@@ -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
 
@@ -37,7 +37,7 @@ skills:
37
37
 
38
38
  ## Golden Rule
39
39
 
40
- **Coordenador APENAS.** Leu arquivo → delegue. Nunca implemente, nunca edite, nunca debugue.
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