pantheon-opencode 1.1.1 → 1.2.0-beta.9.7bef03e

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 (45) hide show
  1. package/README.md +74 -1
  2. package/bin/pantheon-init.mjs +79 -0
  3. package/commands/pantheon-audit.md +25 -0
  4. package/commands/pantheon-consolidate.md +11 -0
  5. package/commands/pantheon-deepwork.md +79 -0
  6. package/commands/pantheon-optimize.md +79 -0
  7. package/commands/pantheon-vision.md +67 -0
  8. package/commands/pantheon.md +49 -0
  9. package/opencode.json +12 -41
  10. package/package.json +5 -1
  11. package/scripts/doctor.mjs +4 -2
  12. package/scripts/install/health-check.mjs +101 -1
  13. package/scripts/install/model-picker.mjs +118 -0
  14. package/scripts/install/opencode.mjs +98 -17
  15. package/scripts/install-mcp.mjs +98 -13
  16. package/scripts/mcp_persistence_server.py +331 -1
  17. package/scripts/secret-scan.mjs +76 -0
  18. package/scripts/validate-routing.mjs +15 -0
  19. package/src/agents/aphrodite.md +7 -0
  20. package/src/agents/demeter.md +7 -0
  21. package/src/agents/hephaestus.md +7 -0
  22. package/src/agents/hermes.md +7 -0
  23. package/src/agents/zeus.md +31 -18
  24. package/src/instructions/agent-return-format.instructions.md +1 -0
  25. package/src/instructions/zeus-anti-stall.instructions.md +43 -5
  26. package/src/instructions/zeus-timeout-retry.instructions.md +47 -10
  27. package/src/mcp/install-mcp.mjs +6 -913
  28. package/src/mcp/mcp_persistence_server.py +332 -0
  29. package/src/mcp/pantheon_vision_server.py +495 -0
  30. package/src/mcp/requirements-vision.txt +3 -0
  31. package/src/pantheon/auto-wake.ts +91 -0
  32. package/src/pantheon/background-job-board.ts +568 -0
  33. package/src/pantheon/file-persistence.ts +95 -0
  34. package/src/pantheon/presets.d.mts +99 -0
  35. package/src/pantheon/presets.mjs +491 -0
  36. package/src/pantheon/vision.ts +1340 -0
  37. package/src/plugin.ts +40 -25
  38. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  39. package/src/plugins/tui/dist/tui.js +806 -222
  40. package/src/plugins/tui/dist/tui.tsx +866 -121
  41. package/src/plugins/tui/package.json +2 -2
  42. package/src/plugins/tui/src/index.tsx +866 -121
  43. package/src/routing.yml +179 -0
  44. package/commands/pantheon-reflect.md +0 -16
  45. package/commands/pantheon-verify.md +0 -17
@@ -1,4 +1,3 @@
1
- # Auto-generated: resolved symlink from ../src/mcp/mcp_persistence_server.py
2
1
  #!/usr/bin/env python3
3
2
  """Pantheon Persistence MCP Server.
4
3
 
@@ -36,6 +35,9 @@ CREATE TABLE IF NOT EXISTS kv_store (
36
35
  UNIQUE(namespace, key)
37
36
  );
38
37
 
38
+ CREATE INDEX IF NOT EXISTS idx_kv_namespace_expires
39
+ ON kv_store(namespace, expires_at);
40
+
39
41
  CREATE VIRTUAL TABLE IF NOT EXISTS kv_store_fts USING fts5(
40
42
  namespace, key, value,
41
43
  content='kv_store', content_rowid='id',
@@ -52,6 +54,14 @@ CREATE TRIGGER IF NOT EXISTS kv_store_ad AFTER DELETE ON kv_store BEGIN
52
54
  VALUES('delete', old.id, old.namespace, old.key, old.value);
53
55
  END;
54
56
 
57
+ CREATE TRIGGER IF NOT EXISTS kv_store_softdelete_ad
58
+ AFTER UPDATE OF deleted_at ON kv_store
59
+ WHEN new.deleted_at IS NOT NULL AND old.deleted_at IS NULL
60
+ BEGIN
61
+ INSERT INTO kv_store_fts(kv_store_fts, rowid)
62
+ VALUES('delete', old.id);
63
+ END;
64
+
55
65
  CREATE TRIGGER IF NOT EXISTS kv_store_au AFTER UPDATE ON kv_store BEGIN
56
66
  INSERT INTO kv_store_fts(kv_store_fts, rowid, namespace, key, value)
57
67
  VALUES('delete', old.id, old.namespace, old.key, old.value);
@@ -220,6 +230,10 @@ async def kv_store(
220
230
  (namespace, key, value, expires_at),
221
231
  )
222
232
  conn.commit()
233
+
234
+ # Opportunistic auto-purge: if namespace has >500 entries, purge expired
235
+ _opportunistic_auto_purge(conn, namespace)
236
+
223
237
  return {"status": "stored", "namespace": namespace, "key": key}
224
238
 
225
239
 
@@ -254,6 +268,52 @@ async def kv_get(
254
268
  return str(row[0]) if row else None
255
269
 
256
270
 
271
+ @mcp.tool(
272
+ name="kv_stats",
273
+ description="Return storage statistics: total entries, expired count, per-namespace breakdown, DB file size.",
274
+ )
275
+ async def kv_stats(
276
+ scope: str = "project",
277
+ ) -> dict:
278
+ """Return storage statistics for the given scope.
279
+
280
+ Args:
281
+ scope: 'project' (default) or 'global'.
282
+
283
+ Returns:
284
+ Dict with total_entries, expired_entries, namespaces breakdown, db_size_bytes.
285
+ """
286
+ conn = _db(scope)
287
+
288
+ total = conn.execute("SELECT COUNT(*) FROM kv_store WHERE deleted_at IS NULL").fetchone()[0]
289
+ expired = conn.execute(
290
+ "SELECT COUNT(*) FROM kv_store "
291
+ "WHERE expires_at IS NOT NULL AND expires_at < datetime('now') "
292
+ "AND deleted_at IS NULL"
293
+ ).fetchone()[0]
294
+
295
+ ns_rows = conn.execute(
296
+ "SELECT namespace, COUNT(*) as cnt, "
297
+ "SUM(CASE WHEN expires_at IS NOT NULL AND expires_at < datetime('now') THEN 1 ELSE 0 END) as expired_count "
298
+ "FROM kv_store WHERE deleted_at IS NULL GROUP BY namespace ORDER BY cnt DESC"
299
+ ).fetchall()
300
+
301
+ namespaces = {
302
+ r[0]: {"count": r[1], "expired": r[2]}
303
+ for r in ns_rows
304
+ }
305
+
306
+ db_path = _resolve_db_path(scope)
307
+ db_size = db_path.stat().st_size if db_path and db_path.exists() else 0
308
+
309
+ return {
310
+ "scope": scope,
311
+ "total_entries": total,
312
+ "expired_entries": expired,
313
+ "namespaces": namespaces,
314
+ "db_size_bytes": db_size,
315
+ }
316
+
257
317
  @mcp.tool(
258
318
  name="kv_delete",
259
319
  description="Delete a key-value pair by namespace and key. "
@@ -353,6 +413,7 @@ async def kv_search(
353
413
 
354
414
  # Sanitize: extract up to 10 word tokens, wrap each in quotes
355
415
  terms = re.findall(r"\w+", query)[:10]
416
+ terms = [re.escape(t) for t in terms]
356
417
  if not terms:
357
418
  return []
358
419
  fts_query = " OR ".join(f'"{t}"' for t in terms)
@@ -441,6 +502,275 @@ async def purge_expired(
441
502
  return {"purged": count, "dry_run": False}
442
503
 
443
504
 
505
+
506
+ def _opportunistic_auto_purge(conn: sqlite3.Connection, namespace: str, threshold: int = 500) -> None:
507
+ """Lightweight auto-purge: if namespace exceeds threshold, soft-delete expired entries."""
508
+ count = conn.execute(
509
+ "SELECT COUNT(*) FROM kv_store WHERE namespace = ? AND deleted_at IS NULL",
510
+ (namespace,),
511
+ ).fetchone()[0]
512
+ if count <= threshold:
513
+ return
514
+
515
+ conn.execute(
516
+ "UPDATE kv_store SET deleted_at = datetime('now') "
517
+ "WHERE namespace = ? AND expires_at IS NOT NULL "
518
+ "AND expires_at < datetime('now') AND deleted_at IS NULL",
519
+ (namespace,),
520
+ )
521
+ conn.commit()
522
+
523
+
524
+ @mcp.tool(
525
+ name="kv_delete_namespace",
526
+ description="Delete all entries in a namespace. Optionally limit to entries older than N days.",
527
+ )
528
+ async def kv_delete_namespace(
529
+ namespace: str,
530
+ scope: str = "project",
531
+ older_than_days: int | None = None,
532
+ ) -> dict:
533
+ """Delete all entries in a namespace, optionally old entries only.
534
+
535
+ Args:
536
+ namespace: Namespace to clear.
537
+ scope: 'project' (default) or 'global'.
538
+ older_than_days: If set, only delete entries older than this many days.
539
+
540
+ Returns:
541
+ Dict with deleted count.
542
+ """
543
+ conn = _db(scope)
544
+
545
+ if older_than_days is not None:
546
+ cursor = conn.execute(
547
+ "DELETE FROM kv_store WHERE namespace = ? "
548
+ "AND datetime(created_at) < datetime('now', ? || ' days')",
549
+ (namespace, f"-{older_than_days}"),
550
+ )
551
+ else:
552
+ cursor = conn.execute(
553
+ "DELETE FROM kv_store WHERE namespace = ?",
554
+ (namespace,),
555
+ )
556
+ conn.commit()
557
+
558
+ return {"deleted": cursor.rowcount}
559
+
560
+
561
+ # ── Context Checkpoint Tools ────────────────────────────────────────────────────
562
+ # Session-scoped context storage for deepwork checkpoints, phase state,
563
+ # tool call history, and reasoning chains. All entries use namespace
564
+ # "checkpoint:{slug}" with auto-TTL of 4 hours (session duration).
565
+
566
+ DEFAULT_CONTEXT_TTL: int = 14400 # 4 hours
567
+
568
+
569
+ @mcp.tool(
570
+ name="context_save",
571
+ description="Save a context checkpoint for a session/phase. "
572
+ "Stores structured JSON in persistence KV with auto-TTL of 4h.",
573
+ )
574
+ async def context_save(
575
+ slug: str,
576
+ key: str,
577
+ content: str,
578
+ session_id: str | None = None,
579
+ ttl: int | None = None,
580
+ scope: str = "project",
581
+ ) -> dict:
582
+ """Save a context checkpoint with session-level isolation.
583
+
584
+ If session_id is provided, namespace becomes "checkpoint:{slug}:{session_id}"
585
+ preventing cross-session reads. If omitted, a random UUID is auto-generated
586
+ and returned so the caller can pass it to subsequent calls.
587
+
588
+ Args:
589
+ slug: Session identifier (e.g. "auth-refactor").
590
+ key: Checkpoint key (e.g. "phase:3", "latest", "heartbeat").
591
+ content: JSON-serializable string or any text content.
592
+ session_id: Unique session ID for namespace isolation (auto-generated if None).
593
+ ttl: TTL in seconds (default 4h / 14400). None = session duration.
594
+ scope: 'project' (default) or 'global'.
595
+
596
+ Returns:
597
+ Status dict with namespace, key, and session_id for subsequent calls.
598
+ """
599
+ actual_session = session_id or uuid.uuid4().hex[:12]
600
+ ns = f"checkpoint:{slug}:{actual_session}"
601
+ actual_ttl = ttl if ttl is not None else DEFAULT_CONTEXT_TTL
602
+
603
+ conn = _db(scope)
604
+ expires_at = (datetime.now(timezone.utc) + timedelta(seconds=actual_ttl)).isoformat()
605
+
606
+ conn.execute(
607
+ "INSERT INTO kv_store (namespace, key, value, expires_at, "
608
+ "created_at, updated_at) VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))"
609
+ "ON CONFLICT(namespace, key) DO UPDATE SET "
610
+ " value = excluded.value, "
611
+ " expires_at = excluded.expires_at, "
612
+ " updated_at = datetime('now')",
613
+ (ns, key, content, expires_at),
614
+ )
615
+ conn.commit()
616
+
617
+ # Also always update "latest" pointer
618
+ if key != "latest":
619
+ # Use a larger TTL for "latest" to outlive individual phase entries
620
+ conn.execute(
621
+ "INSERT INTO kv_store (namespace, key, value, expires_at, "
622
+ "created_at, updated_at) VALUES (?, 'latest', ?, ?, datetime('now'), datetime('now'))"
623
+ "ON CONFLICT(namespace, key) DO UPDATE SET "
624
+ " value = excluded.value, "
625
+ " expires_at = excluded.expires_at, "
626
+ " updated_at = datetime('now')",
627
+ (ns, content, expires_at),
628
+ )
629
+ conn.commit()
630
+
631
+ return {"status": "stored", "namespace": ns, "key": key, "session_id": actual_session, "ttl": actual_ttl}
632
+
633
+
634
+ @mcp.tool(
635
+ name="context_get",
636
+ description="Retrieve a context checkpoint by session slug and key. "
637
+ "Returns the raw content string or null if expired/not found.",
638
+ )
639
+ async def context_get(
640
+ slug: str,
641
+ key: str = "latest",
642
+ session_id: str | None = None,
643
+ scope: str = "project",
644
+ ) -> str | None:
645
+ """Get a context checkpoint with session isolation.
646
+
647
+ Args:
648
+ slug: Session identifier.
649
+ key: Checkpoint key (default "latest").
650
+ session_id: Required for isolation. If None, searches without session scope.
651
+ scope: 'project' (default) or 'global'.
652
+
653
+ Returns:
654
+ Content string or None if not found/expired.
655
+ """
656
+ conn = _db(scope)
657
+ ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
658
+ row = conn.execute(
659
+ "SELECT value FROM kv_store "
660
+ "WHERE namespace = ? AND key = ? "
661
+ "AND (expires_at IS NULL OR expires_at > datetime('now')) "
662
+ "AND deleted_at IS NULL",
663
+ (ns, key),
664
+ ).fetchone()
665
+ return row[0] if row else None
666
+
667
+
668
+ @mcp.tool(
669
+ name="context_list",
670
+ description="List all context checkpoints for a session slug. "
671
+ "Returns keys, timestamps, and TTL info.",
672
+ )
673
+ async def context_list(
674
+ slug: str,
675
+ session_id: str | None = None,
676
+ scope: str = "project",
677
+ ) -> list[dict]:
678
+ """List all checkpoints for a session.
679
+
680
+ Args:
681
+ slug: Session identifier.
682
+ session_id: Optional. If provided, narrows to specific session.
683
+ scope: 'project' (default) or 'global'.
684
+
685
+ Returns:
686
+ List of dicts with key, created_at, expires_at.
687
+ """
688
+ conn = _db(scope)
689
+ ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
690
+ rows = conn.execute(
691
+ "SELECT key, created_at, expires_at FROM kv_store "
692
+ "WHERE namespace = ? "
693
+ "AND (expires_at IS NULL OR expires_at > datetime('now')) "
694
+ "AND deleted_at IS NULL "
695
+ "ORDER BY created_at DESC LIMIT 50",
696
+ (ns,),
697
+ ).fetchall()
698
+ return [
699
+ {
700
+ "key": r[0],
701
+ "created_at": r[1],
702
+ "expires_at": r[2],
703
+ }
704
+ for r in rows
705
+ ]
706
+
707
+
708
+ @mcp.tool(
709
+ name="context_stats",
710
+ description="Return session context statistics: "
711
+ "checkpoint count, TTL remaining, total size per slug.",
712
+ )
713
+ async def context_stats(
714
+ slug: str,
715
+ session_id: str | None = None,
716
+ scope: str = "project",
717
+ ) -> dict:
718
+ """Return storage statistics for a session's context.
719
+
720
+ Args:
721
+ slug: Session identifier.
722
+ session_id: Optional. If provided, narrows to specific session.
723
+ scope: 'project' (default) or 'global'.
724
+
725
+ Returns:
726
+ Dict with entry_count, total_bytes, ttl_remaining.
727
+ """
728
+ conn = _db(scope)
729
+ ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
730
+
731
+ count = conn.execute(
732
+ "SELECT COUNT(*) FROM kv_store "
733
+ "WHERE namespace = ? AND deleted_at IS NULL",
734
+ (ns,),
735
+ ).fetchone()[0]
736
+
737
+ expired = conn.execute(
738
+ "SELECT COUNT(*) FROM kv_store "
739
+ "WHERE namespace = ? AND expires_at < datetime('now') "
740
+ "AND deleted_at IS NULL",
741
+ (ns,),
742
+ ).fetchone()[0]
743
+
744
+ size = conn.execute(
745
+ "SELECT COALESCE(SUM(LENGTH(value)), 0) FROM kv_store "
746
+ "WHERE namespace = ? AND deleted_at IS NULL",
747
+ (ns,),
748
+ ).fetchone()[0]
749
+
750
+ # Check TTL of latest entry
751
+ remaining: int | None = None
752
+ row = conn.execute(
753
+ "SELECT expires_at FROM kv_store "
754
+ "WHERE namespace = ? AND key = 'latest' AND deleted_at IS NULL",
755
+ (ns,),
756
+ ).fetchone()
757
+ if row and row[0]:
758
+ try:
759
+ exp = datetime.fromisoformat(row[0])
760
+ now = datetime.now(timezone.utc)
761
+ remaining = max(0, int((exp - now).total_seconds()))
762
+ except (ValueError, TypeError):
763
+ pass
764
+
765
+ return {
766
+ "slug": slug,
767
+ "namespace": ns,
768
+ "entry_count": count,
769
+ "expired_entries": expired,
770
+ "total_bytes": size,
771
+ "ttl_remaining_seconds": remaining,
772
+ }
773
+
444
774
  def _resolve_db_path(scope: str) -> Path | None:
445
775
  """Resolve the database file path for a given scope."""
446
776
  if scope == "global":
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ /** Scan versionable files without printing secret values. */
4
+
5
+ import { execFileSync } from 'node:child_process'
6
+ import { readFileSync } from 'node:fs'
7
+ import { resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const bifrostHeader = ['x', '-bf-', 'vk'].join('')
11
+ const bifrostTokenPrefix = ['sk', '-bf-'].join('')
12
+ const apiKeyName = ['api', 'Key'].join('')
13
+ const authorizationName = ['Author', 'ization'].join('')
14
+
15
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
16
+
17
+ const patterns = [
18
+ { label: 'Bifrost credential header', regex: new RegExp(escapeRegExp(bifrostHeader), 'i') },
19
+ {
20
+ label: 'Bifrost credential value',
21
+ regex: new RegExp(`${escapeRegExp(bifrostTokenPrefix)}[A-Za-z0-9_-]{8,}`, 'i'),
22
+ },
23
+ {
24
+ label: 'literal API key value',
25
+ regex: new RegExp(
26
+ `["']${escapeRegExp(apiKeyName)}["']\\s*[:=]\\s*["'](?!\\$\\{|process\\.env|os\\.getenv|env:)[^"']{12,}["']`,
27
+ 'i',
28
+ ),
29
+ },
30
+ {
31
+ label: 'literal Authorization bearer value',
32
+ regex: new RegExp(
33
+ `["']${escapeRegExp(authorizationName)}["']\\s*[:=]\\s*["']Bearer\\s+[A-Za-z0-9._-]{12,}["']`,
34
+ 'i',
35
+ ),
36
+ },
37
+ ]
38
+
39
+ export function scanText(text, file = '<text>') {
40
+ return patterns.filter(({ regex }) => regex.test(text)).map(({ label }) => ({ file, label }))
41
+ }
42
+
43
+ export function versionableFiles() {
44
+ return execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
45
+ encoding: 'buffer',
46
+ })
47
+ .toString()
48
+ .split('\0')
49
+ .filter(Boolean)
50
+ }
51
+
52
+ export function scanVersionableFiles() {
53
+ const findings = []
54
+ for (const file of versionableFiles()) {
55
+ let text
56
+ try {
57
+ const content = readFileSync(file)
58
+ if (content.includes(0)) continue
59
+ text = content.toString('utf8')
60
+ } catch {
61
+ continue
62
+ }
63
+ findings.push(...scanText(text, file))
64
+ }
65
+ return findings
66
+ }
67
+
68
+ if (fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
69
+ const findings = scanVersionableFiles()
70
+ if (findings.length > 0) {
71
+ for (const finding of findings) console.error(`[SECRET SCAN] ${finding.file}: ${finding.label}`)
72
+ process.exitCode = 1
73
+ } else {
74
+ console.log('Secret scan passed: no versionable Bifrost/API credentials detected.')
75
+ }
76
+ }
@@ -18,6 +18,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
18
18
  import { dirname, join } from 'node:path'
19
19
  import { fileURLToPath } from 'node:url'
20
20
  import yaml from 'js-yaml'
21
+ import { validatePresetDefs } from '../src/pantheon/presets.mjs'
21
22
 
22
23
  const __dirname = dirname(fileURLToPath(import.meta.url))
23
24
  const ROOT = join(__dirname, '..')
@@ -150,6 +151,20 @@ for (const [name, info] of Object.entries(routing.agents || {})) {
150
151
  }
151
152
  }
152
153
 
154
+ // F. Model preset validation
155
+ const presetDefs = routing.presets || {}
156
+ console.log(`\n Model presets: ${Object.keys(presetDefs).length} defined`)
157
+
158
+ const presetKnownAgents = routingAgents.filter((a) => a !== 'zen' && a !== 'zeus_copilot')
159
+ const presetValidation = validatePresetDefs(presetDefs, { agents: presetKnownAgents })
160
+ for (const err of presetValidation.errors) {
161
+ check(false, `Preset ${err}`)
162
+ }
163
+ for (const w of presetValidation.warnings) {
164
+ warn(`Preset ${w}`)
165
+ }
166
+ console.log(` Presets defined: ${Object.keys(presetDefs).length}`)
167
+
153
168
  // Summary
154
169
  console.log(`\n${'='.repeat(50)}`)
155
170
  const status = failures === 0 ? '✅ PASSED' : `❌ FAILED (${failures}/${checks} checks failed)`
@@ -118,3 +118,10 @@ Compress working context with the `context-compression` skill (L1, Pantheon-nati
118
118
 
119
119
  ## Skills
120
120
  Frontend: `tdd-with-agents`, `visual-review-pipeline`, `incremental-implementation`
121
+
122
+
123
+ ## Session Context Retrieval
124
+ When dispatched by Zeus, call context_get(slug, "latest", session_id=SESSION_ID)
125
+ if session_slug is provided in dispatch metadata. Apply any "remaining_tasks",
126
+ "current_phase", or "gotchas" from the retrieved context.
127
+ If context_get returns None, proceed fresh (first phase or expired session).
@@ -107,3 +107,10 @@ Compress working context with the `context-compression` skill (L1, Pantheon-nati
107
107
 
108
108
  ## Skills
109
109
  `tdd-with-agents`, `incremental-implementation`
110
+
111
+
112
+ ## Session Context Retrieval
113
+ When dispatched by Zeus, call context_get(slug, "latest", session_id=SESSION_ID)
114
+ if session_slug is provided in dispatch metadata. Apply any "remaining_tasks",
115
+ "current_phase", or "gotchas" from the retrieved context.
116
+ If context_get returns None, proceed fresh (first phase or expired session).
@@ -60,3 +60,10 @@ Compress working context with the `context-compression` skill (L1, Pantheon-nati
60
60
  **How**: call `execute_code_script("compress-inline.py", args=["compress", "--text", "<content>"])`. Use `score` to preview priority, `batch` for multiple files. See the `context-compression` skill for the full protocol.
61
61
 
62
62
  **Note**: scrubbing is automatic in the MCP layer; never embed raw secrets in the `--text` argument beyond what the tool scrubs.
63
+
64
+
65
+ ## Session Context Retrieval
66
+ When dispatched by Zeus, call context_get(slug, "latest", session_id=SESSION_ID)
67
+ if session_slug is provided in dispatch metadata. Apply any "remaining_tasks",
68
+ "current_phase", or "gotchas" from the retrieved context.
69
+ If context_get returns None, proceed fresh (first phase or expired session).
@@ -265,3 +265,10 @@ Compress working context with the `context-compression` skill (L1, Pantheon-nati
265
265
 
266
266
  ## Skills
267
267
  Implementação: `tdd-with-agents`, `incremental-implementation`, `code-review-checklist`, `git-workflow-and-versioning`
268
+
269
+
270
+ ## Session Context Retrieval
271
+ When dispatched by Zeus, call context_get(slug, "latest", session_id=SESSION_ID)
272
+ if session_slug is provided in dispatch metadata. Apply any "remaining_tasks",
273
+ "current_phase", or "gotchas" from the retrieved context.
274
+ If context_get returns None, proceed fresh (first phase or expired session).
@@ -4,10 +4,9 @@ mode: primary
4
4
  reasoning_effort: medium
5
5
  permission:
6
6
  edit: deny
7
- bash: allow
7
+ bash: deny
8
8
  task:
9
9
  "*": allow
10
- pantheon-code-mode_*: ask
11
10
  temperature: 0.2
12
11
  steps: 45
13
12
  mcp_tools:
@@ -16,8 +15,6 @@ mcp_tools:
16
15
  - memory_recall
17
16
  - memory_store
18
17
  - memory_search
19
- pantheon-code-mode:
20
- - execute_code_script
21
18
  pantheon-persistence:
22
19
  - kv_get
23
20
  - kv_store
@@ -42,6 +39,34 @@ skills:
42
39
 
43
40
  **Coordenador APENAS.** Leu arquivo → delegue. Nunca implemente, nunca edite, nunca debugue.
44
41
 
42
+
43
+ ## 🔒 Fusion-Style Enforcement
44
+
45
+ **Zeus NUNCA edita arquivos.** Esta é uma trava de PERMISSÃO, não só instrução:
46
+ - `edit: deny` — OpenCode bloqueia qualquer tentativa de edição
47
+ - `bash: deny` — Zero acesso a shell. Nem leitura, nem diagnóstico.
48
+ - `execute_code_script` removido — Zeus não executa scripts
49
+
50
+ O fluxo é SEMPRE: **Planejar → Especificar → Delegar → Revisar**. Zeus nunca toca no código.
51
+
52
+ ### Bloqueios explícitos
53
+ | Ação | Status | Como fazer |
54
+ |------|--------|------------|
55
+ | Editar arquivo | ❌ BLOQUEADO | Delegar para @hermes, @aphrodite, @talos |
56
+ | Bash shell | ❌ BLOQUEADO | Usar task() com subagente apropriado |
57
+ | Executar script | ❌ BLOQUEADO | Delegar para @prometheus |
58
+ | Instalar dep | ❌ BLOQUEADO | Delegar para @prometheus |
59
+ | Git commit/push | ❌ BLOQUEADO | Delegar para @iris |
60
+ | Ler arquivo | ✅ Permitido | Via Read/Glob/Grep tools |
61
+ | task() delegar | ✅ Permitido | Única ferramenta de ação de Zeus |
62
+ | memory MCP | ✅ Permitido | memory_search, memory_store, memory_recall |
63
+ | skill() | ✅ Permitido | Carregar skills |
64
+
65
+ ### Se algo precisar ser feito e não houver subagente apropriado
66
+ 1. Consulte a árvore de roteamento (pantheon://routing)
67
+ 2. Pergunte ao usuário qual agente usar
68
+ 3. NUNCA tente fazer você mesmo
69
+
45
70
  **Approval gates** (via `agent/askQuestions`):
46
71
  0. Council -> FULL STOP -> AGUARDAR approve/changes/discard
47
72
  1. Planejamento -> "Plan approved?"
@@ -50,18 +75,6 @@ skills:
50
75
 
51
76
  **Auto-continue** só com pedido explícito do usuário.
52
77
 
53
- ## ⚠️ Bash Scoping
54
-
55
- **`bash: allow` é para leitura e diagnóstico APENAS.**
56
- NUNCA use bash para:
57
- - Editar arquivos (use sed/echo/redirect → delegue para @hermes)
58
- - Instalar dependências (delegue para @prometheus)
59
- - Executar comandos que modificam o sistema
60
-
61
- Comandos permitidos: `ls`, `git status`, `git log`, `ps`, `python3 -c`, `cat` (leitura), `npm test` (para verificar).
62
-
63
- ---
64
-
65
78
  ## Delegation Cache (Otimizacao de Tokens)
66
79
 
67
80
  Antes de usar a arvore de roteamento, consulte o memory:
@@ -159,7 +172,7 @@ Limite maximo de 2 niveis de nesting: Zeus -> subagente -> sub-subagente.
159
172
 
160
173
  ```
161
174
  depth = kv_get("deleg:depth") ?? 0
162
- if depth >= 2 → NAO delegar, execute voce mesmo
175
+ if depth >= 2 → NAO delegar, ESCALAR para o usuario
163
176
  else → kv_store("deleg:depth", depth + 1)
164
177
 
165
178
  Quando subagente retornar:
@@ -177,7 +190,7 @@ Zeus (nivel 0) -> Apollo/Hermes (nivel 1) -> sub-subagente (nivel 2 max).
177
190
 
178
191
  ## MCP Tools
179
192
 
180
- `memory_recall()` inicio, `memory_store()` apos cada fase. `pantheon://routing` para consultar. `execute_code_script()` para sequencias.
193
+ `memory_recall()` inicio, `memory_store()` apos cada fase. `pantheon://routing` para consultar.
181
194
 
182
195
  ### References
183
196
  - Routing: `pantheon://routing`
@@ -13,6 +13,7 @@ All implementation agents (Hermes, Aphrodite, Demeter, Hephaestus, Prometheus) M
13
13
  **summary:** What was done, in 2-3 sentences
14
14
  **tests:** ✅ All passing / ⚠️ X failing / ❌ Not run (reason)
15
15
  **coverage:** X% (if applicable)
16
+ **tokens:** ~N input / ~M output (estimated)
16
17
  **status:** complete | partial (reason) | escalated (reason)
17
18
  **blockers:** [list any blockers or null]
18
19
 
@@ -54,14 +54,52 @@ On tasks expected to run > 5 turns:
54
54
 
55
55
  ## Heartbeat & Checkpoint Integration
56
56
 
57
+ All session state lives in **pantheon-persistence** (namespace `checkpoint:<slug>`).
58
+ No file I/O, no checkpoint_session.py — TTL (4h) handles cleanup automatically.
59
+
57
60
  ### Heartbeat Check
58
- - If `.pantheon/deepwork/<slug>/heartbeat.json` exists and `last_action` is older than 300s, log a stall warning and resume
59
- - Write heartbeat after every anti-stall recovery action
61
+ - If `context_get(slug, "heartbeat")` returns a checkin older than 300s, log a stall warning and resume
62
+ - Write heartbeat after every anti-stall recovery action:
63
+ ```
64
+ context_save(slug, "heartbeat", json({"status": "alive", "last_action": "...", "turn_count": N}))
65
+ ```
60
66
 
61
- ### Checkpoint Auto-Save
67
+ ### Checkpoint Auto-Save (Pré-Compactação)
62
68
  Before ANY delegate dispatch, save a checkpoint:
63
- ```bash
64
- python .pantheon/code-mode/checkpoint_session.py save <slug>
69
+ ```
70
+ context_save(slug, "phase:N", json({
71
+ "phase": N, "turn_count": N, "agent": "...", "summary": "..."
72
+ }), session_id=SESSION_ID)
73
+ ```
74
+ All checkpoints auto-expire after 4h (TTL=14400).
75
+
76
+ ### Gatilho de Pré-Compactação (Anti-perda de estado)
77
+ Antes da compactação nativa do OpenCode disparar (75-96% do context window),
78
+ o Zeus DEVE salvar o estado atual:
79
+ 1. Capture session_id do primeiro `context_save` da sessão
80
+ 2. Salve heartbeat + phase atual + tarefas pendentes
81
+ 3. Só então permita que a compactação prossiga
82
+ ```
83
+ # Ao iniciar sessão:
84
+ result = context_save(slug, "init", session_state)
85
+ SESSION_ID = result.session_id # ← guarde para toda a sessão
86
+
87
+ # Antes de CADA delegação:
88
+ context_save(slug, f"pre:{agent}", current_state, session_id=SESSION_ID)
89
+
90
+ # Após retorno do agente:
91
+ context_save(slug, f"post:{agent}", result_state, session_id=SESSION_ID)
92
+ ```
93
+ Isso garante que o estado sobreviva à compactação — o "latest" pointer
94
+ sempre aponta para o checkpoint mais recente, mesmo após compactação.
95
+
96
+
97
+ ### Context Retrieval
98
+ Next-phase agents retrieve previous context via:
99
+ ```
100
+ context_get(slug, "latest") # most recent checkpoint
101
+ context_get(slug, "phase:3") # specific phase
102
+ context_list(slug) # all checkpoints
65
103
  ```
66
104
 
67
105
  ### Long-Session Progress