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.
- package/CHANGELOG.md +28 -0
- package/README.md +13 -25
- package/README.pt-BR.md +4 -5
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/mcp_persistence_server.py +1 -1
- package/src/agents/apollo.md +1 -0
- package/src/agents/gaia.md +1 -0
- package/src/agents/zeus.md +8 -29
- package/src/mcp/mcp_persistence_server.py +1 -1
- package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
- package/src/pantheon/cost-command.ts +1 -2
- package/src/pantheon/delegation-enforce.ts +59 -14
- package/src/pantheon/goal-loop.ts +1 -1
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/idle-continuation.ts +4 -6
- package/src/pantheon/model-command.ts +1 -2
- package/src/pantheon/native-task-status.ts +2 -2
- package/src/pantheon/session-id.ts +30 -0
- package/src/pantheon/task-result-guard.ts +2 -3
- package/src/pantheon/todo-enforcer.ts +3 -3
- package/src/pantheon/tool-context.ts +23 -0
- package/src/pantheon/v2-bridge.ts +1 -4
- package/src/plugin-v2.ts +4 -15
- package/src/plugin.ts +61 -245
- package/src/plugins/tui/dist/tui.d.ts +33 -64
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +30 -177
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +67 -277
- package/src/skills/context-compression/SKILL.md +1 -1
- package/src/pantheon/delegate-manager.ts +0 -851
- package/src/pantheon/delegation-classifier.ts +0 -306
- package/src/pantheon/delegation-finalize.ts +0 -468
- package/src/pantheon/delegation-notify.ts +0 -212
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1349
- package/src/pantheon/native-probe.ts +0 -234
- 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
|
|
79
|
+
## Delegation (native `task()`)
|
|
80
80
|
|
|
81
|
-
Pantheon
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
|
|
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 |
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
343
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon",
|
|
3
3
|
"publisher": "ils15",
|
|
4
|
-
"version": "1.5.0-beta.
|
|
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
|
@@ -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
|
|
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:
|
package/src/agents/apollo.md
CHANGED
package/src/agents/gaia.md
CHANGED
package/src/agents/zeus.md
CHANGED
|
@@ -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
|
-
###
|
|
147
|
+
### DELEGATION_RULES (native task API)
|
|
148
148
|
|
|
149
|
-
|
|
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
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
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
|
|
3
|
-
*
|
|
4
|
-
* compacted session does not lose
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
218
|
-
*
|
|
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
|
|
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 (
|
|
4
|
-
*
|
|
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
|
|
26
|
-
*
|
|
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. `
|
|
240
|
-
* sessions
|
|
241
|
-
*
|
|
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 ?? '
|
|
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'
|
|
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
|
-
//
|
|
305
|
-
//
|
|
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
|
|
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 './
|
|
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
|
|
9
|
-
*
|
|
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
|
|
38
|
-
*
|
|
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
|
|
6
|
-
*
|
|
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
|
*/
|