pantheon-opencode 1.5.0-beta.16 → 1.5.0-beta.17

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 (41) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +13 -25
  3. package/README.pt-BR.md +4 -5
  4. package/package.json +1 -1
  5. package/plugin.json +1 -1
  6. package/pyproject.toml +1 -1
  7. package/scripts/mcp_persistence_server.py +1 -1
  8. package/src/agents/apollo.md +1 -0
  9. package/src/agents/gaia.md +1 -0
  10. package/src/agents/zeus.md +8 -29
  11. package/src/mcp/mcp_persistence_server.py +1 -1
  12. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  13. package/src/pantheon/cost-command.ts +1 -2
  14. package/src/pantheon/delegation-enforce.ts +59 -14
  15. package/src/pantheon/goal-loop.ts +1 -1
  16. package/src/pantheon/goal-store.ts +1 -1
  17. package/src/pantheon/idle-continuation.ts +4 -6
  18. package/src/pantheon/model-command.ts +1 -2
  19. package/src/pantheon/native-task-status.ts +2 -2
  20. package/src/pantheon/session-id.ts +30 -0
  21. package/src/pantheon/task-result-guard.ts +2 -3
  22. package/src/pantheon/todo-enforcer.ts +3 -3
  23. package/src/pantheon/tool-context.ts +23 -0
  24. package/src/pantheon/v2-bridge.ts +1 -4
  25. package/src/plugin-v2.ts +4 -15
  26. package/src/plugin.ts +61 -245
  27. package/src/plugins/tui/dist/tui.d.ts +33 -64
  28. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  29. package/src/plugins/tui/dist/tui.js +30 -177
  30. package/src/plugins/tui/package-lock.json +2 -2
  31. package/src/plugins/tui/package.json +1 -1
  32. package/src/plugins/tui/src/index.tsx +67 -277
  33. package/src/skills/context-compression/SKILL.md +1 -1
  34. package/src/pantheon/delegate-manager.ts +0 -851
  35. package/src/pantheon/delegation-classifier.ts +0 -306
  36. package/src/pantheon/delegation-finalize.ts +0 -468
  37. package/src/pantheon/delegation-notify.ts +0 -212
  38. package/src/pantheon/delegation-toast.ts +0 -73
  39. package/src/pantheon/delegation.ts +0 -1349
  40. package/src/pantheon/native-probe.ts +0 -234
  41. package/src/pantheon/step-cap.ts +0 -110
package/CHANGELOG.md CHANGED
@@ -18,6 +18,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
18
18
 
19
19
  ## ✅ Closed Issues
20
20
 
21
+ ## [v1.5.0-beta.17] - 2026-09-15
22
+
23
+ <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
24
+ move this section to a versioned entry and reset the template below. -->
25
+
26
+ ## 🆕 What's New
27
+
28
+ - **Delegação nativa no host:** o motor `pantheon_delegate` foi removido
29
+ (~3,9k linhas) junto com as tools `pantheon_delegate`,
30
+ `pantheon_delegation_read` e `pantheon_delegation_list`; a delegação agora usa
31
+ o **`task()` nativo** do host (child sessions). 16 arquivos de teste obsoletos
32
+ removidos.
33
+ - **TUI — painel Delegations lê a sessão nativa:** o painel passa a derivar de
34
+ `session.children` + `session.status` + md (o arquivo de board deixou de ser
35
+ fonte). Mantidas as cores de status, retenção, ceiling, filtro de sessão e
36
+ guards.
37
+ - **Camada fina preservada:** `task-result-guard` (verified completion de child
38
+ vazio), `native-task-status` e guards de enforcement.
39
+
40
+ ## 🐞 Fixed
41
+
42
+ - **Read-only enforcement re-apontado para sessões nativas:** aplicado no
43
+ público via hook `chat.params` e `hashline_edit` bloqueado para `apollo`/`gaia`,
44
+ fechando o bypass aberto após a remoção do motor de delegação.
45
+ - **Docs/comentários limpos:** referências aos módulos removidos atualizadas,
46
+ incluindo nits de comentário em `tests/pantheon/tui-delegations.test.ts` e
47
+ `src/pantheon/session-id.ts`.
48
+
21
49
  ## [v1.5.0-beta.16] - 2026-09-15
22
50
 
23
51
  <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
package/README.md CHANGED
@@ -76,25 +76,14 @@ availability and configuration of OpenCode and any optional services you choose
76
76
  to use. Check the [releases](https://github.com/ils15/pantheon-opencode/releases)
77
77
  and [changelog](CHANGELOG.md) for the latest published changes.
78
78
 
79
- ## Delegation engines (legacy default, native opt-in)
79
+ ## Delegation (native `task()`)
80
80
 
81
- Pantheon exposes two delegation engines behind one tool surface:
82
-
83
- - **Legacy (default)** — `pantheon_delegate`, the original engine registered by
84
- the V1 plugin. No configuration is required.
85
- - **Native (opt-in, experimental)** — OpenCode's native `task()` child-session
86
- engine, selected with `PANTHEON_DELEGATE_MODE=native`. Native is strict: it
87
- does not use foreground fallback, model failover, or retry; it performs a
88
- lazy capability probe on the first dispatch and includes the resulting
89
- status in the delegation receipt.
90
-
91
- Legacy stays the default for v1.5.x; native is not promoted to default until
92
- v1.6. See [ADR-0011](.pantheon/memory-bank/adr/0011-delegation-engine-contract.md)
93
- (Proposed) for the engine contract, rationale, and migration prerequisites.
94
-
95
- **Kill-switch:** set `PANTHEON_DELEGATION=off` (case-insensitive) to disable
96
- delegation entirely. The switch covers **both** engines — the legacy
97
- `pantheon_delegate`/read/list tools throw, and the native manager throws too.
81
+ Pantheon delegates exclusively through OpenCode's native `task()` child-session
82
+ engine. The former custom `pantheon_delegate` tool and the V1 delegation engine
83
+ (`delegation.ts`, `delegate-manager.ts` and supporting modules) were removed;
84
+ there is no Pantheon-specific delegation tool surface to configure. See
85
+ [ADR-0011](.pantheon/memory-bank/adr/0011-delegation-engine-contract.md) for the
86
+ historical engine contract.
98
87
 
99
88
  ## Cost tool backend
100
89
 
@@ -185,23 +174,22 @@ per installation; V1 and V2 Pantheon plugins must never be registered together.
185
174
  |---|---|---|
186
175
  | OpenCode config key | singular `plugin` | plural `plugins` |
187
176
  | Pantheon registration | `src/plugin.ts` plus `src/plugins/pantheon-hooks.ts` | `<installed>/src/plugin-v2` directory (`index.ts` re-exports `src/plugin-v2.ts`) |
188
- | Runtime contract | Legacy Pantheon plugin, including `pantheon_delegate`, read/list tools, event/tool hooks and V1 compaction handling | Full V2 plugin: 9 orchestration tools, 4 event subscriptions, session hooks (`prompt`, `context`), tool hooks (`execute.before`/`after`), plus configuration transforms |
177
+ | Runtime contract | Pantheon V1 plugin: 6 tools (`hashline_edit`, the 3 goal tools, `pantheon_cost`, `pantheon_model`), event/tool hooks and V1 compaction handling | Full V2 plugin: 6 orchestration tools, 4 event subscriptions, session hooks (`prompt`, `context`), tool hooks (`execute.before`/`after`), plus configuration transforms |
189
178
  | V1 APIs | Registered | Own tool definitions via `ctx.tool.transform()` — not the V1 plugin path |
190
179
 
191
- The V2 plugin provides 9 orchestration tools (`pantheon_delegate`,
192
- `pantheon_delegation_read`, `pantheon_delegation_list`, `hashline_edit`,
180
+ The V2 plugin provides 6 orchestration tools (`hashline_edit`,
193
181
  `pantheon_goal_create`, `pantheon_goal_get`, `pantheon_goal_update`,
194
182
  `pantheon_cost`, `pantheon_model`), 4 event subscriptions (`session.created`,
195
183
  `session.idle`, `session.error`, `session.compacted`), session hooks (`prompt`,
196
184
  `context`), and tool hooks (`execute.before`, `execute.after`). The only
197
- unsupported V2 feature is `legacy-hooks` (the V1-specific delegate API surface).
185
+ unsupported V2 feature is `legacy-hooks` (the V1-specific hook surface).
198
186
 
199
187
  The package exposes both contracts as importable exports: `pantheon-opencode/plugin`
200
188
  (V1), `pantheon-opencode/plugin-v2` (V2) and `pantheon-opencode/v2-bridge`
201
189
  (optional interop), so a host can load either contract explicitly.
202
190
 
203
191
  The V1→V2 bridge (`src/pantheon/v2-bridge.ts`) enables optional interop:
204
- V1 infrastructure singletons (BackgroundJobBoard, DelegationClient, GoalStore,
192
+ V1 infrastructure singletons (BackgroundJobBoard, GoalStore,
205
193
  TodoEnforcer, VisionHandler) are passed through V2 `ctx.options`. The bridge is
206
194
  optional — V2 works standalone with graceful degradation.
207
195
 
@@ -339,8 +327,8 @@ child session returns an empty or missing result. Instead of surfacing a silent
339
327
  `completed` with no payload (which confuses the orchestrator), the guard now
340
328
  raises an explicit error. This catches the common free-tier failure mode where a
341
329
  child session exceeds the uncached-prefill token budget (`BackendAdmissionRejected`)
342
- and returns nothing. For large payloads, prefer `pantheon_delegate` over native
343
- `task()` — the delegation layer has better timeout and error handling.
330
+ and returns nothing. Prefer `background=true` dispatches with an explicit
331
+ `task_status(wait=true)` fan-in so large payloads are collected deterministically.
344
332
 
345
333
 
346
334
  ## Documentation
package/README.pt-BR.md CHANGED
@@ -130,16 +130,15 @@ registrados juntos.
130
130
  |---|---|---|
131
131
  | Chave de config do OpenCode | `plugin` singular | `plugins` plural |
132
132
  | Registro Pantheon | `src/plugin.ts` mais `src/plugins/pantheon-hooks.ts` | `pantheon-opencode/plugin-v2` (`src/plugin-v2.ts`) |
133
- | Contrato de runtime | Plugin Pantheon legado, incluindo `pantheon_delegate`, ferramentas read/list, hooks de evento/ferramenta e tratamento de compactação V1 | Plugin V2 completo: 9 ferramentas de orquestração, 4 assinaturas de eventos, session hooks (`prompt`, `context`), tool hooks (`execute.before`/`after`), além de transforms de configuração |
133
+ | Contrato de runtime | Plugin Pantheon V1: 6 ferramentas (`hashline_edit`, as 3 ferramentas de goal, `pantheon_cost`, `pantheon_model`), hooks de evento/ferramenta e tratamento de compactação V1 | Plugin V2 completo: 6 ferramentas de orquestração, 4 assinaturas de eventos, session hooks (`prompt`, `context`), tool hooks (`execute.before`/`after`), além de transforms de configuração |
134
134
  | APIs V1 | Registradas | Definições próprias de ferramentas via `ctx.tool.transform()` — não pelo caminho do plugin V1 |
135
135
 
136
- O plugin V2 fornece 9 ferramentas de orquestração (`pantheon_delegate`,
137
- `pantheon_delegation_read`, `pantheon_delegation_list`, `hashline_edit`,
136
+ O plugin V2 fornece 6 ferramentas de orquestração (`hashline_edit`,
138
137
  `pantheon_goal_create`, `pantheon_goal_get`, `pantheon_goal_update`,
139
138
  `pantheon_cost`, `pantheon_model`), 4 assinaturas de eventos (`session.created`,
140
139
  `session.idle`, `session.error`, `session.compacted`), session hooks (`prompt`,
141
140
  `context`) e tool hooks (`execute.before`, `execute.after`). O único recurso V2
142
- sem suporte é `legacy-hooks` (a superfície de API de delegate específica do V1).
141
+ sem suporte é `legacy-hooks` (a superfície de hooks específica do V1).
143
142
 
144
143
  O pacote expõe os dois contratos como exports importáveis:
145
144
  `pantheon-opencode/plugin` (V1), `pantheon-opencode/plugin-v2` (V2) e
@@ -147,7 +146,7 @@ O pacote expõe os dois contratos como exports importáveis:
147
146
  explicitamente o contrato desejado.
148
147
 
149
148
  A ponte V1→V2 (`src/pantheon/v2-bridge.ts`) habilita interop opcional:
150
- singletons de infraestrutura V1 (BackgroundJobBoard, DelegationClient,
149
+ singletons de infraestrutura V1 (BackgroundJobBoard,
151
150
  GoalStore, TodoEnforcer, VisionHandler) são repassados via `ctx.options` do V2.
152
151
  A ponte é opcional — o V2 funciona standalone com degradação graciosa.
153
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.5.0-beta.16",
3
+ "version": "1.5.0-beta.17",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon",
3
3
  "publisher": "ils15",
4
- "version": "1.5.0-beta.16",
4
+ "version": "1.5.0-beta.17",
5
5
  "displayName": "Pantheon",
6
6
  "description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
7
7
  "agents": "./src/agents",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
4
4
 
5
5
  [project]
6
6
  name = "pantheon"
7
- version = "1.5.0-beta.16"
7
+ version = "1.5.0-beta.17"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []
@@ -1323,7 +1323,7 @@ def build_rehydration_blocks(checkpoint: dict) -> list[str] | None:
1323
1323
  """Build deterministic rehydration blocks from a checkpoint dict.
1324
1324
 
1325
1325
  Pure string templating — no LLM, no embeddings. Mirrors the
1326
- ``<mission_context>`` convention of delegation-compaction.ts: only a
1326
+ ``<mission_context>`` convention of the compaction-context builder: only a
1327
1327
  non-done goal is re-emitted. Returns None when nothing critical remains.
1328
1328
  """
1329
1329
  if not isinstance(checkpoint, dict) or not checkpoint:
@@ -12,6 +12,7 @@ permission:
12
12
  glob: allow
13
13
  webfetch: allow
14
14
  edit: deny
15
+ hashline_edit: deny
15
16
  bash: deny
16
17
  task:
17
18
  "*": deny
@@ -11,6 +11,7 @@ permission:
11
11
  grep: allow
12
12
  webfetch: allow
13
13
  edit: deny
14
+ hashline_edit: deny
14
15
  bash: deny
15
16
  task:
16
17
  "*": deny
@@ -144,33 +144,11 @@ task_status(task_id="ses_xxx", wait=true)
144
144
  -> bloqueia ate completar: { state: "completed", task_result: "..." }
145
145
  ```
146
146
 
147
- ### Pantheon Delegation Tools (plugin) — 3-tool API
147
+ ### DELEGATION_RULES (native task API)
148
148
 
149
- O plugin Pantheon adiciona 3 tools de delegacao gerenciada pelo BackgroundJobBoard:
150
-
151
- ```
152
- pantheon_delegate({prompt, agent, description?, read_only?})
153
- -> cria sessao filha, registra no board, retorna alias: [apo-1]
154
- pantheon_delegation_read({id: "apo-1"})
155
- -> BLOQUEIA ate o job terminar, retorna o report markdown, marca reconciled
156
- -> durante a espera, mostra o que o agente esta fazendo: seccao final "## Agent Activity"
157
- (amostra as messages do child a cada ~2s; tool calls + args truncados / texto)
158
- pantheon_delegation_list()
159
- -> lista jobs da sessao, com [unread] para jobs terminados nao lidos
160
- -> jobs RUNNING mostram "last activity:" (ultima acao visivel do child)
161
- -> fail-open: sem suporte a messages, formato atual inalterado
162
- ```
163
-
164
- - **Read-only enforcement:** agentes read-only (apollo, gaia — `read_only_agents` no routing.yml) tem `edit`, `write`, `bash`, `task` NEGADOS dentro da sessao delegada (o guard de `tool.execute.before` THROW com mensagem acionavel). Use `read_only: true` no delegate para qualquer agente de investigacao.
165
- - **Depth-2 hard-enforced:** sessao read-only NAO cria subagentes (`task` bloqueado) — investigacao nunca vira arvore.
166
- - **Compactacao:** jobs em voo (running + unread terminal) sao injetados no contexto de compactacao via `experimental.session.compacting` — delegacao em andamento nao se perde ao compactar.
167
-
168
- ### DELEGATION_RULES (plugin path)
169
-
170
- 1. **NUNCA** polle `pantheon_delegation_list` para checar se um job terminou.
171
- 2. Voce NAO recebe notificacao injetada no chat — politica do usuario: ZERO `task-notification` no transcript. Visibilidade de conclusao: marcador `[unread]` em `pantheon_delegation_list`, `pantheon_delegation_read({id})`, toasts TUI (pantheon-hooks) e carry-forward de compactacao.
172
- 3. Use `pantheon_delegation_read({id})` APENAS para fan-in explicito / recuperacao de resultado sob demanda (bloqueia ate o fim).
173
- 4. `pantheon_delegation_list` e para diagnostico, nao para polling.
149
+ 1. Todo dispatch usa `task(background=true, subagent_type=..., prompt=...)`.
150
+ 2. Recolha com `task_status(task_id=..., wait=true)` — bloqueia ate o fim, sem polling.
151
+ 3. Use `task_status(wait=true)` apenas para fan-in explicito / recuperacao sob demanda.
174
152
 
175
153
  ### Background (sempre usar)
176
154
  - **Apollo, Hermes, Aphrodite, Demeter, Hephaestus, Prometheus**
@@ -302,9 +280,10 @@ vão a idle com todos incompletos. Guards (todos no `src/pantheon/todo-enforcer.
302
280
  ## Wave 4 (PR #46): Empty-Result Retry + /cost + Themis Tier
303
281
 
304
282
  ### Empty-Result Retry (runtime — automatic)
305
- Empty-response detection (`empty-mode1` / `empty-mode2`) and stuck-agent
306
- classification are wired at runtime in `delegation-classifier.ts` (consumed by
307
- `delegation.ts`). No manual retry helper is required.
283
+ Empty-response detection for native `task()` subagent calls is wired at runtime
284
+ in `task-result-guard.ts` (plugin `tool.execute.after` chain): an
285
+ empty/whitespace-only result is converted into an explicit error so a silent
286
+ child failure is never mistaken for success. No manual retry helper is required.
308
287
 
309
288
  ### /cost — pantheon_cost tool (WIRED no plugin)
310
289
  `pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY usando o backend
@@ -1323,7 +1323,7 @@ def build_rehydration_blocks(checkpoint: dict) -> list[str] | None:
1323
1323
  """Build deterministic rehydration blocks from a checkpoint dict.
1324
1324
 
1325
1325
  Pure string templating — no LLM, no embeddings. Mirrors the
1326
- ``<mission_context>`` convention of delegation-compaction.ts: only a
1326
+ ``<mission_context>`` convention of the compaction-context builder: only a
1327
1327
  non-done goal is re-emitted. Returns None when nothing critical remains.
1328
1328
  """
1329
1329
  if not isinstance(checkpoint, dict) or not checkpoint:
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Compaction Context (Phase 4 + release-134 Phase 2) — build the context
3
- * blocks injected by the `experimental.session.compacting` hook so a
4
- * compacted session does not lose its working state: in-flight background
5
- * delegations, active goals and pending todos.
2
+ * Compaction Context (Phase 4 + release-134 Phase 2, slimmed in Fase 2 of the
3
+ * delegate removal) — build the context blocks injected by the
4
+ * `experimental.session.compacting` hook so a compacted session does not lose
5
+ * its working state: active goals and pending todos.
6
6
  *
7
7
  * Returns a Promise<string[]> (empty when there is nothing to preserve).
8
8
  * Section order is stable:
@@ -17,23 +17,10 @@
17
17
  * `opts.todos`: " [id] description — status". Omitted when the todo
18
18
  * source is absent, disabled, unscoped (no sessionID), fails, or has
19
19
  * no pending todos.
20
- * - "Background Delegations (running):" — every running job launched by
21
- * the session (active work is never capped): alias, agent, started ISO,
22
- * truncated prompt.
23
- * - "Background Delegations (finished, unread):" — terminal-unreconciled
24
- * jobs, newest first, capped at `maxItems` (default 10, matching
25
- * routing.yml background_delegation.max_compaction_items).
26
20
  *
27
- * Reconciled jobs are excluded — they have been read already.
28
- *
29
- * The block style mirrors board.formatForPrompt (" [alias] description —
30
- * STATUS") so compaction context reads consistently with the system prompt,
31
- * but with the extra id/agent/started detail the compaction hook needs.
32
- *
33
- * @module delegation-compaction
21
+ * @module compaction-context
34
22
  */
35
23
 
36
- import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
37
24
  import type { Goal } from './goal-store.ts'
38
25
  import { createPantheonLogger } from './logger.ts'
39
26
  import type { TodoLike } from './todo-enforcer.ts'
@@ -72,56 +59,14 @@ export interface CompactionTodoSource {
72
59
  }
73
60
 
74
61
  export interface CompactionContextOptions {
75
- /** Scope to jobs launched by this session; omit for all jobs. */
62
+ /** Scope to this session; omit to skip the per-session sections. */
76
63
  sessionID?: string
77
- /** Cap on unread terminal jobs kept (default 10). */
78
- maxItems?: number
79
64
  /** Goal source for <mission_context> (optional — omitted when absent). */
80
65
  goals?: CompactionGoalSource
81
66
  /** Todo source for <todo_context> (optional — omitted when absent). */
82
67
  todos?: CompactionTodoSource
83
68
  }
84
69
 
85
- /** Prompt truncation length for a context line. */
86
- const PROMPT_MAX_LEN = 100
87
-
88
- // ─── Formatting ────────────────────────────────────────────────────────
89
-
90
- /** Collapse whitespace and truncate a prompt to PROMPT_MAX_LEN chars. */
91
- function truncatePrompt(text: string): string {
92
- const flat = text.trim().replace(/\s+/g, ' ')
93
- if (flat.length <= PROMPT_MAX_LEN) return flat
94
- return `${flat.slice(0, PROMPT_MAX_LEN)}…`
95
- }
96
-
97
- /** One-line terminal status label, mirroring formatForPrompt. */
98
- function terminalLabel(state: BackgroundJobRecord['state']): string {
99
- switch (state) {
100
- case 'completed':
101
- return 'OK'
102
- case 'error':
103
- return 'ERR'
104
- case 'cancelled':
105
- return 'CAN'
106
- default:
107
- return state.toUpperCase()
108
- }
109
- }
110
-
111
- function runningLine(job: BackgroundJobRecord): string {
112
- return (
113
- ` [${job.alias}] ${job.agent} — ${truncatePrompt(job.description)} — ` +
114
- `RUNNING since ${new Date(job.launchedAt).toISOString()}`
115
- )
116
- }
117
-
118
- function unreadTerminalLine(job: BackgroundJobRecord): string {
119
- return (
120
- ` [${job.alias}] ${job.agent} — ${truncatePrompt(job.description)} — ` +
121
- `${terminalLabel(job.state)} ${new Date(job.updatedAt).toISOString()} [unread]`
122
- )
123
- }
124
-
125
70
  // ─── Sections ──────────────────────────────────────────────────────────
126
71
 
127
72
  /**
@@ -180,58 +125,23 @@ async function todoContextBlock(opts: CompactionContextOptions): Promise<string
180
125
  return `<todo_context>\n${lines.join('\n')}`
181
126
  }
182
127
 
183
- /**
184
- * `<delegation_context>` — the existing delegation blocks, byte-for-byte
185
- * unchanged from Phase 4 (running, then unread terminal ≤ maxItems).
186
- */
187
- function delegationBlocks(board: BackgroundJobBoard, opts: CompactionContextOptions): string[] {
188
- const maxItems = opts.maxItems ?? 10
189
- const jobs = opts.sessionID !== undefined ? board.list(opts.sessionID) : board.list()
190
-
191
- const running = jobs
192
- .filter((j) => j.state === 'running')
193
- .sort((a, b) => a.launchedAt - b.launchedAt)
194
- const unreadTerminal = jobs
195
- .filter((j) => j.terminalUnreconciled)
196
- .sort((a, b) => b.updatedAt - a.updatedAt)
197
- .slice(0, maxItems)
198
-
199
- const blocks: string[] = []
200
- if (running.length > 0) {
201
- blocks.push(`Background Delegations (running):\n${running.map(runningLine).join('\n')}`)
202
- }
203
- if (unreadTerminal.length > 0) {
204
- blocks.push(
205
- `Background Delegations (finished, unread):\n${unreadTerminal
206
- .map(unreadTerminalLine)
207
- .join('\n')}`,
208
- )
209
- }
210
- return blocks
211
- }
212
-
213
128
  // ─── Build ─────────────────────────────────────────────────────────────
214
129
 
215
130
  /**
216
131
  * Build the compaction context blocks for a session: preservation
217
- * directive, active goals, pending todos, and in-flight background
218
- * delegations. Returns an empty array when there is nothing to preserve
219
- * (caller skips the hook injection entirely).
132
+ * directive, active goals, and pending todos. Returns an empty array when
133
+ * there is nothing to preserve (caller skips the hook injection entirely).
220
134
  */
221
135
  export async function buildCompactionContext(
222
- board: BackgroundJobBoard,
223
136
  opts: CompactionContextOptions = {},
224
137
  ): Promise<string[]> {
225
138
  const [mission, todo] = await Promise.all([missionContextBlock(opts), todoContextBlock(opts)])
226
- const delegation = delegationBlocks(board, opts)
227
139
 
228
140
  // Totally-empty state → nothing to preserve; a lone directive is noise.
229
- if (mission === undefined && todo === undefined && delegation.length === 0) return []
141
+ if (mission === undefined && todo === undefined) return []
230
142
 
231
- const blocks: string[] = []
232
- blocks.push(`<pantheon-context directive>\n${PANTHEON_COMPACTION_DIRECTIVE}`)
143
+ const blocks: string[] = [`<pantheon-context directive>\n${PANTHEON_COMPACTION_DIRECTIVE}`]
233
144
  if (mission !== undefined) blocks.push(mission)
234
145
  if (todo !== undefined) blocks.push(todo)
235
- blocks.push(...delegation)
236
146
  return blocks
237
147
  }
@@ -21,9 +21,8 @@ import { homedir } from 'node:os'
21
21
  import { delimiter, dirname, join } from 'node:path'
22
22
  import { fileURLToPath } from 'node:url'
23
23
  import { z } from 'zod'
24
-
25
- import type { ToolContextLike } from './delegation.ts'
26
24
  import type { NativeTaskStatus } from './native-task-status.ts'
25
+ import type { ToolContextLike } from './tool-context.ts'
27
26
 
28
27
  const __filename = fileURLToPath(import.meta.url)
29
28
  const __dirname = dirname(__filename)
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Read-Only Enforcement (Phase 4) — deny mutating tools inside sessions that
3
- * were delegated as read-only (advisory `read_only: true` on
4
- * pantheon_delegate, or the agent ∈ `readOnlyAgents` from routing.yml).
3
+ * were delegated as read-only (an explicit read-only flag, or the agent ∈
4
+ * `readOnlyAgents` from routing.yml).
5
5
  *
6
6
  * OpenCode hook: `tool.execute.before`. The hook shape is
7
7
  * `(input, output) => Promise<void>`; THROWING from the hook denies the tool
@@ -22,8 +22,10 @@
22
22
  * fail-closed when the runtime matrix is wired: missing agent identity or
23
23
  * target metadata is denied rather than inferred.
24
24
  *
25
- * The registry is populated by delegation.ts at delegate time: the CHILD
26
- * session is registered when the delegate call is read-only.
25
+ * The registry is populated by the plugin's `chat.params` hook — the only
26
+ * place the SDK exposes the active agent: when the session's agent is
27
+ * read-only (apollo, gaia — routing.yml `background_delegation.read_only_agents`),
28
+ * the session is registered; `session.deleted` unregisters it.
27
29
  *
28
30
  * @module delegation-enforce
29
31
  */
@@ -147,7 +149,6 @@ export function isDelegationAllowed(
147
149
  function targetFromTool(tool: string, args: unknown): string | undefined {
148
150
  if (args === null || typeof args !== 'object') return undefined
149
151
  const record = args as Record<string, unknown>
150
- if (tool === 'pantheon_delegate') return normalizeDelegationAgent(record.agent)
151
152
  if (tool === 'task') {
152
153
  return normalizeDelegationAgent(record.subagent_type ?? record.subagentType)
153
154
  }
@@ -236,9 +237,10 @@ export function zeusReadGuard(tool: string, args: unknown, agent: string | undef
236
237
  // ─── Registry ──────────────────────────────────────────────────────────
237
238
 
238
239
  /**
239
- * Read-only session registry. `createDelegationTools` registers child
240
- * sessions here; the plugin's `tool.execute.before` guard consults it.
241
- * Process-wide singleton for plugin wiring + factory for test isolation.
240
+ * Read-only session registry. The plugin's `chat.params` hook registers
241
+ * sessions whose active agent is read-only (see {@link syncReadOnlySession});
242
+ * the `tool.execute.before` guard consults it. Process-wide singleton for
243
+ * plugin wiring + factory for test isolation.
242
244
  */
243
245
  export class ReadOnlySessionRegistry {
244
246
  private readonly sessions = new Map<string, ReadOnlyEntry>()
@@ -279,6 +281,50 @@ export class ReadOnlySessionRegistry {
279
281
  /** Process-wide registry wired into the plugin. */
280
282
  export const readOnlyRegistry = new ReadOnlySessionRegistry()
281
283
 
284
+ // ─── Read-Only Agent Policy ────────────────────────────────────────────
285
+
286
+ /**
287
+ * Agents that must stay investigation-only. Mirrors
288
+ * `background_delegation.read_only_agents` in routing.yml — the parity is
289
+ * enforced by tests/pantheon/delegation-enforce.test.ts so the two cannot
290
+ * drift silently.
291
+ */
292
+ export const READ_ONLY_AGENTS: ReadonlySet<string> = new Set(['apollo', 'gaia'])
293
+
294
+ /**
295
+ * Whether an agent identity belongs to the read-only set. Identity is never
296
+ * inferred from prompts or tool names — only the normalized agent name counts.
297
+ */
298
+ export function isReadOnlyAgent(
299
+ agent: unknown,
300
+ readOnlyAgents: ReadonlySet<string> = READ_ONLY_AGENTS,
301
+ ): boolean {
302
+ const normalized = normalizeDelegationAgent(agent)
303
+ return normalized !== undefined && readOnlyAgents.has(normalized)
304
+ }
305
+
306
+ /**
307
+ * Sync the read-only registry with a session's active agent. Called from the
308
+ * plugin's `chat.params` hook: a read-only agent registers the session;
309
+ * anything else (unknown/absent/non-read-only) unregisters it, which also
310
+ * covers an in-session agent switch. Returns whether the session is now
311
+ * registered.
312
+ */
313
+ export function syncReadOnlySession(
314
+ registry: ReadOnlySessionRegistry,
315
+ sessionID: string,
316
+ agent: unknown,
317
+ readOnlyAgents: ReadonlySet<string> = READ_ONLY_AGENTS,
318
+ ): boolean {
319
+ const normalized = normalizeDelegationAgent(agent)
320
+ if (normalized === undefined || !readOnlyAgents.has(normalized)) {
321
+ registry.unregister(sessionID)
322
+ return false
323
+ }
324
+ registry.register(sessionID, { agent: normalized })
325
+ return true
326
+ }
327
+
282
328
  // ─── Guard ─────────────────────────────────────────────────────────────
283
329
 
284
330
  /**
@@ -291,19 +337,18 @@ export function createEnforcementGuard(input: {
291
337
  options?: EnforcementGuardOptions
292
338
  }): ToolExecuteBeforeHandler {
293
339
  const blockedTools = input.options?.blockedTools ?? DEFAULT_BLOCKED_TOOLS
294
- const prefix = input.options?.messagePrefix ?? 'pantheon_delegate guard'
340
+ const prefix = input.options?.messagePrefix ?? 'pantheon delegation guard'
295
341
 
296
342
  return async (hook: ToolExecuteBeforeInput, output?: ToolExecuteBeforeOutput): Promise<void> => {
297
- const isDelegationTool = hook.tool === 'task' || hook.tool === 'pantheon_delegate'
343
+ const isDelegationTool = hook.tool === 'task'
298
344
  if (isDelegationTool && input.options?.isChildSession?.(hook.sessionID) === true) {
299
345
  throw new Error(
300
346
  `${prefix}: session ${hook.sessionID} is a child session — delegation tools are denied ` +
301
347
  'to prevent nested delegation',
302
348
  )
303
349
  }
304
- // pantheon_delegate has the authoritative ToolContext.agent at execution
305
- // time, so its matrix check lives in delegation.ts. The hook only has the
306
- // session-scoped agent learned from chat.params for native task().
350
+ // The hook only has the session-scoped agent learned from chat.params,
351
+ // so the runtime matrix check for native task() lives here.
307
352
  if (
308
353
  hook.tool === 'task' &&
309
354
  input.options?.isRootSession !== undefined &&
@@ -336,7 +381,7 @@ export function createEnforcementGuard(input: {
336
381
  `This session was delegated for investigation only (read-only agent); it cannot ` +
337
382
  `modify files, run commands, or spawn subagents. ` +
338
383
  `If a write result is needed, dispatch a write-capable agent ` +
339
- `(e.g. hermes, aphrodite) with pantheon_delegate instead.`,
384
+ `(e.g. hermes, aphrodite) with native task() instead.`,
340
385
  )
341
386
  }
342
387
  }
@@ -17,11 +17,11 @@
17
17
  import { z } from 'zod'
18
18
 
19
19
  import type { BackgroundJobBoard } from './background-job-board.ts'
20
- import type { ToolContextLike } from './delegation.ts'
21
20
  import { GoalStore } from './goal-store.ts'
22
21
  import { createPantheonLogger } from './logger.ts'
23
22
  import { safeSessionPath } from './session-guard.ts'
24
23
  import type { TodoEnforcerMessage } from './todo-enforcer.ts'
24
+ import type { ToolContextLike } from './tool-context.ts'
25
25
 
26
26
  // Silence-by-default TUI policy (pantheon-hooks L42-58): warn → hooks.log,
27
27
  // console echo opt-in via PANTHEON_HOOKS_LOG=1. `deps.logger` stays for tests.
@@ -15,7 +15,7 @@
15
15
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
16
16
  import { join } from 'node:path'
17
17
 
18
- import { assertSafeParentSessionID } from './delegation-finalize.ts'
18
+ import { assertSafeParentSessionID } from './session-id.ts'
19
19
 
20
20
  export type GoalStatus = 'pending' | 'in_progress' | 'done'
21
21
 
@@ -5,9 +5,8 @@
5
5
  * Priority: an ACTIVE GOAL owns the idle — `GoalLoop.onIdle` continues the
6
6
  * goal. Without an active goal, the TODO enforcer gets the idle (it
7
7
  * self-guards on its own enabled flag + guards). Board-child sessions never
8
- * reach the dispatcher — the plugin's event hook routes those to the
9
- * delegation finalize path first (`handleDelegationEvent` returns true) and
10
- * only calls the dispatcher on the `!delegated` branch.
8
+ * reach the dispatcher — the plugin's event hook owns board transitions and
9
+ * only calls the dispatcher for non-board sessions.
11
10
  *
12
11
  * Independently testable — the loop and enforcer are structural interfaces;
13
12
  * the real GoalLoop/TodoEnforcer satisfy them.
@@ -34,9 +33,8 @@ export interface IdleDispatcher {
34
33
  }
35
34
 
36
35
  /**
37
- * Build the idle router. The plugin event hook calls this only on the
38
- * `!delegated` branch of `handleDelegationEvent`, so board children never
39
- * arrive here.
36
+ * Build the idle router. The plugin event hook calls this only for non-board
37
+ * sessions, so board children never arrive here.
40
38
  */
41
39
  export function createIdleDispatcher(deps: {
42
40
  goalLoop: GoalLoopLike
@@ -23,8 +23,6 @@ import { lstat, mkdir, open, rename, unlink } from 'node:fs/promises'
23
23
  import { homedir } from 'node:os'
24
24
  import { basename, dirname, join, resolve } from 'node:path'
25
25
  import { z } from 'zod'
26
-
27
- import type { ToolContextLike } from './delegation.ts'
28
26
  import {
29
27
  capabilityEntry,
30
28
  hasVision,
@@ -32,6 +30,7 @@ import {
32
30
  normalizeCapability,
33
31
  resolveActivePreset,
34
32
  } from './presets.mjs'
33
+ import type { ToolContextLike } from './tool-context.ts'
35
34
 
36
35
  export type ModelScope = 'project' | 'global'
37
36
  export type ModelAction = 'status' | 'show' | 'set' | 'reset'
@@ -2,8 +2,8 @@
2
2
  * Native Task Status — classifies BackgroundJobRecord results from native
3
3
  * task() delegation into actionable status codes.
4
4
  *
5
- * Pure module — zero external dependencies. Consumed by native-probe.ts
6
- * and the delegation manager to decide retry/skip/escalate strategy.
5
+ * Pure module — zero external dependencies. Consumed by the tool-ceiling and
6
+ * cost layers to decide retry/skip/escalate strategy.
7
7
  *
8
8
  * @module native-task-status
9
9
  */