pantheon-opencode 1.6.0-beta.4 → 1.6.0-beta.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 (51) hide show
  1. package/AGENTS.md +42 -23
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +114 -12
  4. package/README.pt-BR.md +15 -6
  5. package/bin/pantheon-init.mjs +26 -3
  6. package/package.json +5 -6
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +6 -6
  9. package/scripts/{code_mode_server.py → code_mode.py} +2 -2
  10. package/scripts/doctor.mjs +151 -1
  11. package/scripts/install/config-migration.mjs +80 -2
  12. package/scripts/install/health-check.mjs +12 -17
  13. package/scripts/install/opencode-version.mjs +246 -4
  14. package/scripts/install/opencode.mjs +250 -119
  15. package/scripts/install-mcp.mjs +28 -9
  16. package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
  17. package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +1 -1
  18. package/scripts/{memory_mcp_server.py → memory_mcp.py} +1 -1
  19. package/scripts/probe-context-rehydrate.mjs +2 -2
  20. package/scripts/run-node-tests.mjs +189 -0
  21. package/scripts/sync-artifacts.mjs +5 -5
  22. package/scripts/test-opencode-v2-sandbox.sh +21 -21
  23. package/skills-lock.json +2 -2
  24. package/src/agents/aphrodite.md +1 -1
  25. package/src/agents/apollo.md +7 -2
  26. package/src/agents/athena.md +1 -0
  27. package/src/agents/demeter.md +1 -1
  28. package/src/agents/hephaestus.md +1 -1
  29. package/src/agents/hermes.md +1 -1
  30. package/src/agents/iris.md +1 -0
  31. package/src/agents/nyx.md +1 -0
  32. package/src/agents/themis.md +1 -0
  33. package/src/agents/zeus.md +39 -10
  34. package/src/instructions/memory-protocol.instructions.md +21 -10
  35. package/src/instructions/zeus-anti-stall.instructions.md +19 -11
  36. package/src/instructions/zeus-council-synthesis.instructions.md +2 -2
  37. package/src/mcp/{code_mode_server.py → code_mode.py} +2 -2
  38. package/src/mcp/eval_store.py +2 -2
  39. package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
  40. package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +1 -1
  41. package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +1 -1
  42. package/src/pantheon/v2-unsupported.d.mts +11 -0
  43. package/src/pantheon/v2-unsupported.mjs +113 -0
  44. package/src/plugin-v2.ts +149 -53
  45. package/src/plugins/tui/dist/tui.js +1 -1
  46. package/src/plugins/tui/package-lock.json +2 -2
  47. package/src/plugins/tui/package.json +1 -1
  48. package/src/skills/auto-continue/SKILL.md +4 -4
  49. package/src/skills/context-compression/SKILL.md +1 -1
  50. package/opencode.json +0 -58
  51. /package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +0 -0
@@ -36,7 +36,7 @@ sequenceDiagram
36
36
  participant T as Themis (Audit + Moderator)
37
37
 
38
38
  U->>Z: /pantheon [--research] question
39
- Z->>M: memory_search(council_decisions)
39
+ Z->>M: memory_search(query, namespace="council_decisions")
40
40
  alt Precedent found (score > 0.85)
41
41
  M-->>Z: ⚠️ Cached decision
42
42
  Z->>U: Fast-path precedent
@@ -85,7 +85,7 @@ sequenceDiagram
85
85
  ## Dispatch Sequence (9-Step Protocol)
86
86
 
87
87
  ### Step 0 — Precedent Fast-Path (Fase 1)
88
- Run the precedent read path defined in `## Memory Protocol > Council Decisions Namespace` (`memory_search(query, top_k=2, namespace="council_decisions")`). If no precedent applies, proceed to Step 0b.
88
+ Run the precedent read path defined in `## Memory Protocol > Council Decisions Namespace` (`memory_search(query=question, top_k=2, namespace="council_decisions")`). If no precedent applies, proceed to Step 0b.
89
89
 
90
90
  ### Step 0b — Apollo Pre-Scan (Fase 2, --research flag)
91
91
  If `/pantheon --research <question>`: dispatch @apollo with 30s timeout. Inject findings as `shared_context` into ALL specialist prompts. Skip if flag absent.
@@ -5,12 +5,12 @@ Provides a confined execution environment for orchestration scripts
5
5
  via MCP tools and resources.
6
6
 
7
7
  Usage:
8
- python scripts/code_mode_server.py
8
+ python scripts/code_mode.py
9
9
 
10
10
  Or via MCP client (stdio transport):
11
11
  pantheon-code-mode:
12
12
  command: python
13
- args: ["scripts/code_mode_server.py"]
13
+ args: ["scripts/code_mode.py"]
14
14
  """
15
15
 
16
16
  from __future__ import annotations
@@ -2,7 +2,7 @@
2
2
  """Plugin eval certification storage — ``plugin_eval`` memory namespace.
3
3
 
4
4
  Thin stdlib-only wrapper around the memory SQLite DB (same schema as
5
- ``src/mcp/memory_mcp_server.py``) for plugin-eval certification results.
5
+ ``src/mcp/memory_mcp.py``) for plugin-eval certification results.
6
6
 
7
7
  Namespace: ``plugin_eval``
8
8
  Key format: ``eval:<name>:<date>`` (e.g. ``eval:tdd-with-agents:2026-08-21``)
@@ -30,7 +30,7 @@ from typing import Any
30
30
 
31
31
  NAMESPACE = "plugin_eval"
32
32
 
33
- # Minimal schema — matches memory_mcp_server.SCHEMA_SQL for the memories
33
+ # Minimal schema — matches memory_mcp.SCHEMA_SQL for the memories
34
34
  # table (vec/FTS tables are initialized by the memory MCP server on first
35
35
  # run; eval entries remain readable via list/recall regardless).
36
36
  _SCHEMA_SQL = """
@@ -5,7 +5,7 @@ Key-Value store with FTS5 full-text search, TTL-based expiration,
5
5
  and namespace isolation. Uses SQLite with zero external dependencies.
6
6
 
7
7
  Usage:
8
- python scripts/mcp_persistence_server.py
8
+ python scripts/mcp_persistence.py
9
9
  """
10
10
 
11
11
  from __future__ import annotations
@@ -7,7 +7,7 @@ Provides MCP resources for the Pantheon agent framework:
7
7
  plans/status, memory-bank files
8
8
 
9
9
  Usage:
10
- python scripts/mcp_resources_server.py
10
+ python scripts/mcp_resources.py
11
11
  """
12
12
 
13
13
  from __future__ import annotations
@@ -19,7 +19,7 @@ Tools:
19
19
  memory_stats — DB statistics
20
20
 
21
21
  Usage:
22
- python scripts/memory_mcp_server.py
22
+ python scripts/memory_mcp.py
23
23
  """
24
24
 
25
25
  from __future__ import annotations
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Type declarations for v2-unsupported.mjs (the V2 feature-reduction seed).
3
+ * Consumed by src/plugin-v2.ts via `import ... from './pantheon/v2-unsupported.mjs'`,
4
+ * and by the plain-`.mjs` install/health scripts at runtime.
5
+ */
6
+
7
+ /** Statically-known V2 feature reductions, in report order. */
8
+ export declare const V2_UNSUPPORTED_FEATURE_SEED: readonly string[]
9
+
10
+ /** A fresh, mutable copy of the seed, so a reporter cannot mutate the shared list. */
11
+ export declare function listV2UnsupportedFeatures(): string[]
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * v2-unsupported.mjs — the SEED of V2 feature reductions, in plain ESM.
4
+ *
5
+ * WHY A SEPARATE PLAIN-`.mjs` MODULE: these strings must be readable by three
6
+ * very different consumers, and only one of them can import TypeScript.
7
+ *
8
+ * - `src/plugin-v2.ts` (TypeScript) — owns the LIVE list, appending to it at
9
+ * runtime via `markUnsupported()` when a host API turns out to be missing,
10
+ * and exposes it through `getUnsupportedFeatures()`.
11
+ * - `scripts/install/opencode.mjs` — reports the reduction at install time
12
+ * for a V2-generation install.
13
+ * - `scripts/doctor.mjs` — reports it in the health gate.
14
+ *
15
+ * The scripts are plain Node ESM and must stay that way, and `.mjs` is a
16
+ * REPO-WIDE INVARIANT rather than a judgement about Node: `tsx` is a
17
+ * devDependency only, and no plain-Node script in `scripts/` or `bin/` imports
18
+ * a `.ts` module — every `.ts` path there is a string to copy, stat or read as
19
+ * text (`scripts/install/opencode.mjs`, `scripts/probe-pantheon-cost.mjs`,
20
+ * `scripts/package-evidence.mjs`, `scripts/check-plugin-v2-coverage.mjs`).
21
+ * So a `.mjs` import is the only form all three consumers can share, and it
22
+ * matches the existing `src/pantheon/presets.mjs` + `.d.mts` precedent.
23
+ *
24
+ * NOT the reason: type-stripping. `package.json#engines` admits
25
+ * `^22.22.2 || ^24.15.0 || >=26.0.0`, every admitted version already defaults to
26
+ * unflagged type-stripping (it landed in 22.18.0), so that argument would not
27
+ * have held on any admitted version. The live hazard is that `engines` is not
28
+ * ENFORCED: a Node 20 user can run `doctor` and would get no type-stripping,
29
+ * which is an argument for a runtime version check, not for this file's shape.
30
+ *
31
+ * The SEED is the statically-known half of `getUnsupportedFeatures()`: what a
32
+ * migrating user loses by choosing the V2 contract, knowable with no host at
33
+ * all. Runtime-appended entries (`tool-transform`, `event-stream`, …) are
34
+ * observations about one live host and are deliberately NOT seeded here — they
35
+ * are not known until the plugin runs against a real V2 host.
36
+ *
37
+ * ONE SOURCE OF TRUTH: install and doctor both read the seed from here, so the
38
+ * two cannot drift apart, and what they report IS what this file lists. Note the
39
+ * scope of that claim: it holds between the SEED and the two reporters. The
40
+ * LIVE list `getUnsupportedFeatures()` returns is this seed PLUS markers appended
41
+ * at runtime (`tool-transform`, `event-stream`, …), so it is a superset and the
42
+ * two are expected to differ there. Do not copy these strings into a script;
43
+ * import them.
44
+ *
45
+ * @module v2-unsupported
46
+ */
47
+
48
+ /**
49
+ * Statically-known V2 feature reductions, in the order a reader should meet
50
+ * them. Frozen: `plugin-v2.ts` copies it into its own mutable live array, and
51
+ * every other consumer goes through {@link listV2UnsupportedFeatures}.
52
+ *
53
+ * @type {readonly string[]}
54
+ */
55
+ export const V2_UNSUPPORTED_FEATURE_SEED = Object.freeze([
56
+ 'legacy-hooks',
57
+ // `ctx.catalog` is the one domain a live 2.0.22 host does NOT have: it is
58
+ // absent from `Object.keys(ctx)`, measured in
59
+ // tests/canary/plugin-v2-tool-canary.test.mjs. (`catalog` IS in the 1.18.33
60
+ // SDK's PluginContext, so the SDK types it and the host does not provide it.)
61
+ 'catalog-transform',
62
+ // Adapter support, NOT host availability: 2.0.22 exposes ctx.integration and
63
+ // ctx.skill with callable transforms, and Pantheon deliberately registers
64
+ // neither. These entries record what this plugin does not implement.
65
+ 'integration-transform',
66
+ // The inspected SkillEditor shape has no `source()` helper — i.e. no
67
+ // `SkillEditor.source()` — for adding a directory source. This is narrower
68
+ // than (and distinct from) ctx.skill's host availability or callable
69
+ // transform, which a live 2.0.22 host does provide.
70
+ 'skill-transform',
71
+ // Adapter limitation, not a host gap: the goal loop needs a GoalStore, a
72
+ // GoalLoopClient and a BackgroundJobBoard, none of which the V2
73
+ // PluginContext exposes, and the V1 bridge resolves to null outside V1.
74
+ // pantheon_goal_create/get/update are therefore absent from the V2 surface
75
+ // rather than registered as non-functional placeholders.
76
+ 'goal-tools',
77
+ // Adapter limitation, not a host gap — TWO separate reasons, which are easy
78
+ // to conflate and were once stated wrongly (see the v2EnforcementGuard
79
+ // comment in src/plugin-v2.ts for the long form):
80
+ // - MISSING WIRING: V2 keeps no session→agent map (the host puts the agent
81
+ // on the execute.before event), so there is no `getSessionAgent` to pass.
82
+ // The guard would then call isDelegationAllowed(undefined, target) → false
83
+ // and throw "caller agent is unavailable" on EVERY task() call.
84
+ // - UNWIRED HIERARCHY: V2 seeds no session→parent map anywhere, and an
85
+ // unseeded SessionHierarchyRegistry gates exactly two things — the depth-2
86
+ // child deny and the root-session gate. Unseeded, isChild is always false,
87
+ // so neither can be trusted. UNWIRED, not impossible: the V2
88
+ // `session.created` event carries `properties.info: Session` with
89
+ // `parentID?`, the same field V1 seeds from on the same event, and one
90
+ // line in onSessionCreated would seed live sessions. V1's OTHER source —
91
+ // the client.session.list() startup seed for sessions predating plugin
92
+ // load — has no V2 equivalent at all, because V2's PluginContext exposes
93
+ // no `client`. Whether a live 2.0.22 host populates info.parentID on that
94
+ // event is UNVERIFIED: the canary records Object.keys(ctx), not payloads.
95
+ // Note the hierarchy is NOT what would deny: isRoot reports `true` for an
96
+ // unknown session while unseeded, which PASSES the root gate.
97
+ // Read-only depth-2 still holds via the blocked-tool list in the guard, so
98
+ // this marker records an unenforced matrix, not an unenforced depth limit.
99
+ 'delegation-matrix',
100
+ ])
101
+
102
+ /**
103
+ * A fresh, mutable copy of the seed.
104
+ *
105
+ * Callers get a copy so a reporter that sorts, filters or appends cannot reach
106
+ * back into the shared list — this is what makes "both surfaces report the same
107
+ * strings" a structural property rather than a convention.
108
+ *
109
+ * @returns {string[]}
110
+ */
111
+ export function listV2UnsupportedFeatures() {
112
+ return [...V2_UNSUPPORTED_FEATURE_SEED]
113
+ }
package/src/plugin-v2.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * - 3 self-sufficient tools via V2 tool.transform (hashline_edit,
6
6
  * pantheon_cost, pantheon_model — see ./pantheon/v2-tools.ts; the goal tools
7
7
  * need V1 infrastructure and are absent from this surface)
8
- * - 4 event subscriptions (session.created, idle, error, compacted)
8
+ * - 5 event subscriptions (session.created, idle, deleted, error, compacted)
9
9
  * - Session hooks (prompt, context, compaction)
10
10
  * - A tool `execute.before` hook that ENFORCES read-only sessions: the agent
11
11
  * arrives on the host's event, a read-only agent (apollo/gaia) registers its
@@ -94,6 +94,7 @@ import {
94
94
  type V2ContextLike,
95
95
  } from './pantheon/v2-bridge.ts'
96
96
  import { createV2ToolDefinitions, type V2ToolResult } from './pantheon/v2-tools.ts'
97
+ import { V2_UNSUPPORTED_FEATURE_SEED } from './pantheon/v2-unsupported.mjs'
97
98
  import { type HookPayload, runHook } from './plugins/hook-runner.ts'
98
99
 
99
100
  // ─── Unsupported Features Registry ───────────────────────────────────────
@@ -103,37 +104,18 @@ import { type HookPayload, runHook } from './plugins/hook-runner.ts'
103
104
  * the observed host. A listed feature is not necessarily a host-absent API.
104
105
  * Additional features are appended during setup when a required API is missing.
105
106
  */
106
- export const V2_UNSUPPORTED_FEATURES: string[] = [
107
- 'legacy-hooks',
108
- // `ctx.catalog` is the one domain a live 2.0.22 host does NOT have: it is
109
- // absent from `Object.keys(ctx)`, measured in
110
- // tests/canary/plugin-v2-tool-canary.test.mjs. (`catalog` IS in the 1.18.33
111
- // SDK's PluginContext, so the SDK types it and the host does not provide it.)
112
- 'catalog-transform',
113
- // Adapter support, NOT host availability: 2.0.22 exposes ctx.integration and
114
- // ctx.skill with callable transforms, and Pantheon deliberately registers
115
- // neither. These entries record what this plugin does not implement.
116
- 'integration-transform',
117
- // The inspected SkillEditor shape has no `source()` helper — i.e. no
118
- // `SkillEditor.source()` — for adding a directory source. This is narrower
119
- // than (and distinct from) ctx.skill's host availability or callable
120
- // transform, which a live 2.0.22 host does provide.
121
- 'skill-transform',
122
- // Adapter limitation, not a host gap: the goal loop needs a GoalStore, a
123
- // GoalLoopClient and a BackgroundJobBoard, none of which the V2
124
- // PluginContext exposes, and the V1 bridge resolves to null outside V1.
125
- // pantheon_goal_create/get/update are therefore absent from the V2 surface
126
- // rather than registered as non-functional placeholders.
127
- 'goal-tools',
128
- // Adapter limitation, not a host gap: the caller/target delegation matrix
129
- // needs a session hierarchy seeded from session metadata, and V2 exposes no
130
- // seed path for it — SessionHierarchyRegistry.isRoot reports `true` for
131
- // unknown sessions while unseeded, which would deny every `task()` call. The
132
- // branch is therefore skipped rather than left to deny indiscriminately.
133
- // Read-only depth-2 still holds via the blocked-tool list in the guard below,
134
- // so this marker records an unenforced matrix, not an unenforced depth limit.
135
- 'delegation-matrix',
136
- ]
107
+ /**
108
+ * Live unsupported-feature list: the shared seed plus anything `markUnsupported`
109
+ * appends while this process talks to a real host.
110
+ *
111
+ * The seed lives in `src/pantheon/v2-unsupported.mjs` — a plain-`.mjs` module —
112
+ * so that the V2 installer and `scripts/doctor.mjs` report the SAME strings
113
+ * without either of them having to import TypeScript. This array stays a
114
+ * mutable copy rather than the shared seed itself: `markUnsupported` appends to
115
+ * it, and the seed must stay frozen. `getUnsupportedFeatures()` still returns
116
+ * this exact reference.
117
+ */
118
+ export const V2_UNSUPPORTED_FEATURES: string[] = [...V2_UNSUPPORTED_FEATURE_SEED]
137
119
 
138
120
  // ─── Constants ───────────────────────────────────────────────────────────
139
121
 
@@ -508,6 +490,9 @@ async function handleV2SessionEvent(event: V2SessionEvent): Promise<void> {
508
490
  case 'session.idle':
509
491
  await onSessionIdle(event)
510
492
  break
493
+ case 'session.deleted':
494
+ await onSessionDeleted(event)
495
+ break
511
496
  case 'session.error':
512
497
  await onSessionError(event)
513
498
  break
@@ -522,18 +507,44 @@ async function handleV2SessionEvent(event: V2SessionEvent): Promise<void> {
522
507
  /**
523
508
  * Session-created handler: currently a no-op on the V2-only surface.
524
509
  *
525
- * V1 seeds session state here (`sessionHierarchy.register(info)` plus the
526
- * root-session set) from the full session metadata carried by its own event.
527
- * The V2 event does not carry that metadata, and — more to the point — V1 is a
528
- * separate plugin instance that is not loaded when only `plugin-v2` is
529
- * configured. So nothing runs here on a V2-only install. Do not read the bridge
530
- * check below as coverage: a non-null bridge means a host process that ALSO
531
- * loaded V1, and V1 does the seeding itself.
510
+ * V1 seeds session state here — `sessionHierarchy.register(info)` plus the
511
+ * root-session set — from the full session metadata carried by its own event.
512
+ *
513
+ * THE V2 EVENT CARRIES THE SAME METADATA, so this is an UNWIRED seed, not an
514
+ * impossible one. The V2 SDK types `session.created` as
515
+ * `{ sessionID: string; info: Session }`, and `Session` declares
516
+ * `parentID?: string` (`@opencode-ai/sdk` v2 `gen/types.gen.d.ts`) — a superset
517
+ * of the two fields V1 reads on this exact event. Seeding here would be a copy
518
+ * of V1's line. An earlier revision of this comment claimed the metadata was
519
+ * absent from the V2 event; the SDK types refute that, so do not restore it.
520
+ *
521
+ * WHAT IS NOT MEASURED, in either direction: whether a live 2.0.22 host
522
+ * populates `info.parentID` on this event. The tool canary records
523
+ * `Object.keys(ctx)` from inside the host, not event payloads, so host
524
+ * population is UNVERIFIED. What is proven is only that the type carries the
525
+ * field and that no code path here reads it.
526
+ *
527
+ * The real asymmetry is the OTHER of V1's two seed sources. Besides this event,
528
+ * V1 runs a fail-open startup seed from `client.session.list()`
529
+ * (`seedRootSessionsInBackground`, `src/plugin.ts`) to cover sessions that
530
+ * predate plugin load, because opencode does not replay `session.created` for
531
+ * them. V2's `PluginContext` exposes no `client` at all — `options`, `agent`,
532
+ * `aisdk`, `catalog`, `command`, `integration`, `plugin`, `reference`, `skill`,
533
+ * and nothing else — so that enumeration has NO V2 EQUIVALENT. Wiring this
534
+ * handler would recover live sessions only, never resumed ones.
532
535
  *
533
- * The absent hierarchy seed is why the V2 enforcement guard is built without
534
- * `isRootSession` / `isChildSession`; see `docs/UPGRADING.md`, "Known
535
- * limitation — V2 enforcement covers the blocked-tool list, not the V1
536
- * delegation matrix".
536
+ * And — more to the point — V1 is a separate plugin instance that is not loaded
537
+ * when only `plugin-v2` is configured, so on a V2-only install nothing seeds
538
+ * the hierarchy at all. Do not read the bridge check below as coverage: a
539
+ * non-null bridge means a host process that ALSO loaded V1, and V1 does the
540
+ * seeding itself.
541
+ *
542
+ * The unwired hierarchy seed is ONE of the two reasons the V2 enforcement guard
543
+ * is built without `isRootSession` / `isChildSession`; the other — and the one
544
+ * that would actually deny every `task()` call — is the absent
545
+ * `getSessionAgent`. See the `v2EnforcementGuard` comment below for both, and
546
+ * `docs/UPGRADING.md`, "Known limitation — V2 enforcement covers the
547
+ * blocked-tool list, not the V1 delegation matrix".
537
548
  */
538
549
  async function onSessionCreated(_event: V2SessionEvent): Promise<void> {
539
550
  const _bridge = resolveBridge()
@@ -560,6 +571,58 @@ async function onSessionIdle(_event: V2SessionEvent): Promise<void> {
560
571
  }
561
572
  }
562
573
 
574
+ /**
575
+ * Session-deleted handler: drop the read-only registration.
576
+ *
577
+ * `readOnlyRegistry` is a process-wide singleton and the `execute.before` hook
578
+ * populates it on every delegated `apollo`/`gaia` call. V1 prunes it on
579
+ * `session.deleted` (`src/plugin.ts`); V2 did not, so on a long-lived host
580
+ * every finished investigation session stayed registered for the life of the
581
+ * process — a slow leak, and a stale entry that would keep denying tools if
582
+ * the id were ever reused.
583
+ *
584
+ * This mirrors V1: unregister the session, which drops its agent entry with it
585
+ * (the registry stores one `ReadOnlyEntry` per session id). V1 also clears a
586
+ * `sessionAgents` map here; V2 has no such map and needs no equivalent, since
587
+ * it reads the active agent off the event instead of caching it.
588
+ *
589
+ * The id is read from `properties.sessionID` — the field the sibling
590
+ * `session.idle` handler already uses — falling back to V1's
591
+ * `properties.info.id`. Both spellings are accepted because the V2 SDK types
592
+ * `session.deleted` as `{ sessionID, info }` and a miss here is a silent leak,
593
+ * which is precisely the bug this handler exists to close.
594
+ *
595
+ * A payload with NEITHER spelling is fail-open, and unavoidably so: unregistering
596
+ * is what needs the id, so an id that cannot be resolved cannot be cleaned up
597
+ * and the entry survives. The docstring used to leave that as a documented
598
+ * silent branch; it now warns on the way out, because the only other symptom of
599
+ * a renamed field is a registry that grows for the life of the process.
600
+ */
601
+ async function onSessionDeleted(event: V2SessionEvent): Promise<void> {
602
+ const properties = event.properties
603
+ if (properties === undefined) return
604
+ const info = properties.info
605
+ const sessionID =
606
+ typeof properties.sessionID === 'string'
607
+ ? properties.sessionID
608
+ : info !== null && typeof info === 'object'
609
+ ? (info as { id?: unknown }).id
610
+ : undefined
611
+ if (typeof sessionID !== 'string' || sessionID === '') {
612
+ // Fail-open by necessity, loud by choice: the entry for this session (if
613
+ // any) cannot be identified, so it stays registered. Same `console.warn`
614
+ // channel as the event-subscription error above — this module has no
615
+ // logger, and a diagnostic nobody reads on stderr is still a silent leak.
616
+ console.warn(
617
+ '[Pantheon V2] session.deleted carried no session id ' +
618
+ '(neither properties.sessionID nor properties.info.id); ' +
619
+ 'its read-only registration cannot be pruned and will persist.',
620
+ )
621
+ return
622
+ }
623
+ readOnlyRegistry.unregister(sessionID)
624
+ }
625
+
563
626
  /**
564
627
  * Session-error handler: no V2-side implementation.
565
628
  *
@@ -768,15 +831,41 @@ interface V2ToolExecuteBeforeEvent {
768
831
  * tool — `hashline_edit` and `pantheon_model` are denied here for exactly the
769
832
  * reason they are denied in V1.
770
833
  *
771
- * Deliberately NOT passed `getSessionAgent` / `isRootSession` / `isChildSession`:
772
- * those drive V1's native-`task()` delegation matrix, which needs a session
773
- * hierarchy seeded from session metadata. `SessionHierarchyRegistry.isRoot`
774
- * reports `true` for unknown sessions while unseeded, so passing an unseeded
775
- * predicate here would DENY every `task()` call in every session. Leaving them
776
- * out skips that branch (the guard tests `options?.isRootSession !== undefined`).
777
- * `task` is still denied inside a read-only session via the blocked-tool list,
778
- * so depth-2 holds for apollo/gaia; the caller/target matrix itself is NOT
779
- * enforced on V2. That is a known gap, not a covered case.
834
+ * Deliberately NOT passed `getSessionAgent` / `isRootSession` / `isChildSession`.
835
+ * There are TWO distinct reasons, and conflating them is what produced a wrong
836
+ * causal chain here once already — keep them apart:
837
+ *
838
+ * 1. MISSING `getSessionAgent` (wiring). This is what would deny every
839
+ * `task()` call. V2 learns the active agent from the `execute.before` event
840
+ * itself and keeps no session→agent map, so there is no lookup to hand the
841
+ * guard. The guard would call `isDelegationAllowed(undefined, target)`,
842
+ * which returns `false`, and throw "caller agent is unavailable" — for
843
+ * every caller, including Zeus. Note what is NOT happening: an unseeded
844
+ * `isRootSession` would *pass* the root gate (`isRoot` reports `true` for an
845
+ * unknown session), so the hierarchy is not what denies here.
846
+ *
847
+ * 2. UNWIRED HIERARCHY (design). `SessionHierarchyRegistry` is what gates
848
+ * exactly two things, and nothing else: the depth-2 child deny
849
+ * (`isChildSession`) and the root-session gate (`isRootSession`). Left
850
+ * unseeded, `isChild` is `false` for every session, so depth-2 would never
851
+ * fire on a genuine child — a hierarchy that cannot tell a child from a root
852
+ * is not a trustworthy input to either check.
853
+ *
854
+ * "Unwired", not "impossible": the V2 `session.created` event carries
855
+ * `properties.info: Session` including `parentID?`, the same field V1 seeds
856
+ * from on the same event, so one line in `onSessionCreated` would seed live
857
+ * sessions (see its docstring). What has no V2 equivalent is V1's SECOND
858
+ * source, the `client.session.list()` startup seed — V2's `PluginContext`
859
+ * exposes no `client`. Whether a live 2.0.22 host populates `info.parentID`
860
+ * is UNVERIFIED; no canary reads an event payload. So the correct summary is
861
+ * "seeded for nothing", not "unseedable", and the guard's omission is right
862
+ * today for the weaker reason that nothing seeds it at all.
863
+ *
864
+ * Omitting all three skips the branch wholesale: the guard only enters it when
865
+ * `options?.isRootSession !== undefined`. `task` is still denied inside a
866
+ * read-only session via the blocked-tool list, so depth-2 holds for
867
+ * apollo/gaia; the caller/target matrix itself is NOT enforced on V2. That is
868
+ * a known gap, not a covered case.
780
869
  */
781
870
  const v2EnforcementGuard = createEnforcementGuard({
782
871
  getReadOnlySessions: () => readOnlyRegistry.sessionIDs(),
@@ -1115,7 +1204,14 @@ export function v2Dispose(): void {
1115
1204
  }
1116
1205
 
1117
1206
  /**
1118
- * Get the current list of unsupported V2 features (for diagnostics).
1207
+ * The current list of unsupported V2 features.
1208
+ *
1209
+ * Read by the installer and by `doctor` (both via the shared seed in
1210
+ * `src/pantheon/v2-unsupported.mjs`) so the markers a user is shown are the
1211
+ * same ones this returns, rather than a hand-copied duplicate.
1212
+ *
1213
+ * This returns the LIVE array: the shared seed plus whatever `markUnsupported`
1214
+ * appended while this process talked to a real host.
1119
1215
  */
1120
1216
  export function getUnsupportedFeatures(): readonly string[] {
1121
1217
  return V2_UNSUPPORTED_FEATURES
@@ -167,7 +167,7 @@ async function detectVersion(api) {
167
167
  if (ver) return ver;
168
168
  }
169
169
  } catch {}
170
- return "1.6.0-beta.4";
170
+ return "1.6.0-beta.6";
171
171
  }
172
172
  /**
173
173
  * usage-bar — AI subscription usage gauge for the opencode TUI.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pantheon-tui",
3
- "version": "1.6.0-beta.4",
3
+ "version": "1.6.0-beta.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pantheon-tui",
9
- "version": "1.6.0-beta.4",
9
+ "version": "1.6.0-beta.6",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@opencode-ai/plugin": "1.18.33",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon-tui",
3
3
  "type": "module",
4
- "version": "1.6.0-beta.4",
4
+ "version": "1.6.0-beta.6",
5
5
  "description": "Pantheon TUI plugin — sidebar (version, preset, sessions, real-time delegations) + AI subscription usage gauges incl. OpenCode Go/Zen dollar limits (vendored, MIT)",
6
6
  "license": "MIT",
7
7
  "engines": {
@@ -163,19 +163,19 @@ Should I now run the migration tests? [waiting]
163
163
  Session state is **not** file-based. It lives in **pantheon-persistence** under the
164
164
  `checkpoint:<slug>` namespace and is cleaned up automatically by a 4h TTL.
165
165
 
166
- Write state with `context_save(slug, key, value, session_id=...)` and read it back with
167
- `context_get(slug, "latest", session_id=...)`. Capture `session_id` from the first `context_save`
166
+ Write state with `context_save(slug=slug, key=key, content=JSON.stringify(state), session_id=...)` and read it back with
167
+ `context_get(slug=slug, key="latest", session_id=...)`. Capture `session_id` from the first `context_save`
168
168
  of the session and reuse it for the rest of the session, so the `latest` pointer survives
169
169
  compaction.
170
170
 
171
171
  ### Heartbeat Check
172
- - If `context_get(slug, "heartbeat")` returns a checkin older than 300s, log a stall warning and resume
172
+ - If `context_get(slug=slug, key="heartbeat", session_id=...)` returns a checkin older than 300s, log a stall warning and resume
173
173
  - Write a heartbeat after every anti-stall recovery action
174
174
 
175
175
  ### Checkpoint Rules
176
176
  1. Save a checkpoint before **every** delegate dispatch and after every agent returns
177
177
  2. Include current phase, turn count, and remaining tasks
178
- 3. On resume: `context_get(slug, "latest")` restores the most recent checkpoint
178
+ 3. On resume: `context_get(slug=slug, key="latest", session_id=...)` restores the most recent checkpoint
179
179
 
180
180
  > Do not create `heartbeat.json`, `checkpoint-<N>.json`, or `session.json` under
181
181
  > `.pantheon/deepwork/`. That file-based mechanism is retired — TTL handles cleanup, and reading
@@ -639,7 +639,7 @@ Allowed fields: file paths, status, pass/fail verdicts, phase names, agent names
639
639
 
640
640
  ### Layer 2 — Regex pattern scrub (automatic via MCP layer)
641
641
 
642
- **Source of truth:** `scripts/scrub-secrets.py` — the single canonical scrubber. Both `memory_mcp_server.py` and `compress-inline.py` load it via `importlib` (the filename has a hyphen and cannot be `import`ed normally). Do NOT maintain a separate inline pattern list.
642
+ **Source of truth:** `scripts/scrub-secrets.py` — the single canonical scrubber. Both `memory_mcp.py` and `compress-inline.py` load it via `importlib` (the filename has a hyphen and cannot be `import`ed normally). Do NOT maintain a separate inline pattern list.
643
643
 
644
644
  **Real signature:**
645
645
 
package/opencode.json DELETED
@@ -1,58 +0,0 @@
1
- {
2
- "$schema": "https://opencode.ai/config.json",
3
- "provider": {
4
- "opencode-go": {
5
- "models": {
6
- "mimo-v2.5": {
7
- "attachment": true,
8
- "modalities": { "input": ["text", "image"] }
9
- }
10
- }
11
- }
12
- },
13
- "default_agent": "zeus",
14
- "plugins": ["src/plugin-v2"],
15
- "experimental": {
16
- "subagent_depth": 2
17
- },
18
- "permission": {
19
- "skill": {
20
- "*": "allow"
21
- },
22
- "websearch": "deny",
23
- "bash": {
24
- "git *": "allow",
25
- "npm *": "allow",
26
- "npx *": "allow",
27
- "pytest *": "allow",
28
- "ruff *": "allow",
29
- "pip *": "allow",
30
- "python3 *": "allow"
31
- }
32
- },
33
- "mcp": {
34
- "bifrost": {
35
- "type": "remote",
36
- "url": "https://llm.ofertachina.cloud/mcp",
37
- "enabled": true
38
- }
39
- },
40
- "agent": {
41
- "talos": {
42
- "permission": {
43
- "edit": {
44
- "*": "allow",
45
- "**/migrations/**": "deny",
46
- "**/alembic/**": "deny",
47
- "**/models/**": "deny",
48
- "**/auth/**": "deny",
49
- "**/*.sql": "deny"
50
- },
51
- "bash": {
52
- "*": "allow",
53
- "alembic *": "deny"
54
- }
55
- }
56
- }
57
- }
58
- }