pantheon-opencode 1.5.0-beta.15 → 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 (54) hide show
  1. package/AGENTS.md +14 -63
  2. package/CHANGELOG.md +55 -0
  3. package/README.md +14 -27
  4. package/README.pt-BR.md +5 -6
  5. package/commands/pantheon.md +1 -35
  6. package/package.json +1 -1
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +1 -1
  9. package/scripts/mcp_persistence_server.py +1 -1
  10. package/src/agents/apollo.md +1 -0
  11. package/src/agents/gaia.md +1 -0
  12. package/src/agents/zeus.md +8 -29
  13. package/src/instructions/memory-protocol.instructions.md +6 -3
  14. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  15. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  16. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  17. package/src/mcp/mcp_persistence_server.py +1 -1
  18. package/src/mcp/toon_codec.py +2 -4
  19. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  20. package/src/pantheon/cost-command.ts +1 -2
  21. package/src/pantheon/delegation-enforce.ts +59 -14
  22. package/src/pantheon/goal-loop.ts +1 -1
  23. package/src/pantheon/goal-store.ts +1 -1
  24. package/src/pantheon/idle-continuation.ts +4 -6
  25. package/src/pantheon/model-command.ts +1 -2
  26. package/src/pantheon/native-task-status.ts +2 -2
  27. package/src/pantheon/session-id.ts +30 -0
  28. package/src/pantheon/task-result-guard.ts +2 -3
  29. package/src/pantheon/todo-enforcer.ts +3 -3
  30. package/src/pantheon/tool-context.ts +23 -0
  31. package/src/pantheon/v2-bridge.ts +1 -4
  32. package/src/plugin-v2.ts +4 -15
  33. package/src/plugin.ts +61 -245
  34. package/src/plugins/tui/dist/tui.d.ts +33 -64
  35. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  36. package/src/plugins/tui/dist/tui.js +30 -177
  37. package/src/plugins/tui/package-lock.json +2 -2
  38. package/src/plugins/tui/package.json +1 -1
  39. package/src/plugins/tui/src/index.tsx +67 -277
  40. package/src/skills/context-compression/SKILL.md +1 -1
  41. package/scripts/dev-tui.sh +0 -48
  42. package/scripts/generate-preset-docs.mjs +0 -300
  43. package/scripts/generate-prompts.sh +0 -222
  44. package/scripts/install/component.mjs +0 -24
  45. package/scripts/install/migrate-config.mjs +0 -302
  46. package/src/pantheon/delegate-manager.ts +0 -851
  47. package/src/pantheon/delegation-classifier.ts +0 -306
  48. package/src/pantheon/delegation-finalize.ts +0 -468
  49. package/src/pantheon/delegation-notify.ts +0 -212
  50. package/src/pantheon/delegation-toast.ts +0 -73
  51. package/src/pantheon/delegation.ts +0 -1349
  52. package/src/pantheon/native-probe.ts +0 -234
  53. package/src/pantheon/step-cap.ts +0 -110
  54. package/src/pantheon/toon-codec.ts +0 -481
@@ -9,7 +9,7 @@ absent/fails.
9
9
  Only the standard library (``json`` + ``re``) — prohibited: external
10
10
  gateway, generative embedding.
11
11
 
12
- Encoding rules (mirrored byte-for-byte by ``src/pantheon/toon-codec.ts``):
12
+ Encoding rules:
13
13
 
14
14
  - Scalars: ``None``/``True``/``False`` → ``null``/``true``/``false``;
15
15
  numbers → shortest form; strings bare when unambiguous, else
@@ -20,8 +20,7 @@ Encoding rules (mirrored byte-for-byte by ``src/pantheon/toon-codec.ts``):
20
20
  - Lists of scalars: ``- item`` lines; empty list → ``[]``.
21
21
  - Lists of dicts with uniform keys: ``@table k1|k2`` header + ``v1|v2``
22
22
  rows (savings are per payload class — ~11% on content-dominated board
23
- records up to ~47% on large tabular checkpoints — see
24
- ``docs/ws3-token-opt-measurements.md`` for the reproducible table).
23
+ records up to ~47% on large tabular checkpoints).
25
24
  - Dict values never need quoting for ``:`` (split is on the FIRST colon);
26
25
  list items / top-level scalars that look like ``key: ...`` are quoted.
27
26
  """
@@ -471,7 +470,6 @@ def toon_size_report(value: Any) -> dict[str, Any]:
471
470
  Reports BOTH chars and tokens: token ratios on tiny payloads suffer
472
471
  ceiling bias (``ceil`` rounds 161→41 vs 143→36), so the char columns are
473
472
  the honest basis and the token columns show what metering will debit.
474
- Per-class numbers: see ``docs/ws3-token-opt-measurements.md``.
475
473
  """
476
474
  compact_json = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
477
475
  toon = toon_encode(value)
@@ -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
  */
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Session ID safety — reject session IDs that could escape a per-session
3
+ * output directory.
4
+ *
5
+ * Shared helper for the file-backed stores that remain after the delegate
6
+ * removal (goal-store and friends) — it no longer lives alongside the
7
+ * deleted delegation module.
8
+ *
9
+ * `sessionID` is embedded in a path
10
+ * `<dir>/<sessionID>/file.json` — `path.join` does NOT strip `..`, so a
11
+ * traversing ID would write outside the sandbox. Call this BEFORE any path
12
+ * construction.
13
+ *
14
+ * @module session-id
15
+ */
16
+
17
+ /**
18
+ * Reject session IDs that could escape the target directory.
19
+ *
20
+ * @throws {Error} When the ID contains `..`, `/`, or `\`.
21
+ */
22
+ export function assertSafeParentSessionID(parentSessionID: string): void {
23
+ if (
24
+ parentSessionID.includes('..') ||
25
+ parentSessionID.includes('/') ||
26
+ parentSessionID.includes('\\')
27
+ ) {
28
+ throw new Error(`Invalid parentSessionID: ${parentSessionID}`)
29
+ }
30
+ }
@@ -58,14 +58,13 @@ export interface TaskResultGuardOptions {
58
58
 
59
59
  /**
60
60
  * The error message injected when a task() result is empty.
61
- * Matches the existing delegation-finalize.ts contract:
62
- * "Child session produced no assistant or tool output".
61
+ * Keeps the historical "no assistant or tool output" contract text.
63
62
  */
64
63
  export const EMPTY_TASK_ERROR =
65
64
  'ERROR: task() returned empty output — child session produced no assistant or tool output. ' +
66
65
  'This typically means the provider rejected the request (admission control / rate limit) ' +
67
66
  'or the child session crashed before producing any response. ' +
68
- 'Consider using pantheon_delegate (clean context) instead of task() for large payloads.'
67
+ 'Consider splitting the work into a smaller task() or retrying the child session.'
69
68
 
70
69
  // ─── Implementation ────────────────────────────────────────────────────
71
70
 
@@ -2,9 +2,9 @@
2
2
  * TODO Enforcer (Wave 1) — keeps root/non-board sessions working on their
3
3
  * todo list when the session goes idle.
4
4
  *
5
- * The plugin's `event` hook dispatches `session.idle`: board-child sessions go
6
- * to the delegation finalize path (`handleDelegationEvent`), everything else
7
- * (roots, non-board sessions) goes to `TodoEnforcer.onIdle`. The enforcer
5
+ * The plugin's `event` hook dispatches `session.idle`: board-child sessions
6
+ * are owned by the board transition path, everything else (roots, non-board
7
+ * sessions) goes to `TodoEnforcer.onIdle`. The enforcer
8
8
  * re-injects a continuation prompt when the session went idle with incomplete
9
9
  * todos, so the agent keeps working on its task list instead of stopping.
10
10
  *
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tool Context — the structural view of the context opencode passes to a
3
+ * tool's `execute()`.
4
+ *
5
+ * Extracted from `delegation.ts` (Fase 2 of the delegate removal) so the
6
+ * surviving structural tools (cost-command, model-command, goal-loop) no
7
+ * longer depend on the delegation module. Pure type — zero runtime code.
8
+ *
9
+ * @module tool-context
10
+ */
11
+
12
+ /** Structural view of the tool context opencode passes to execute(). */
13
+ export interface ToolContextLike {
14
+ sessionID: string
15
+ directory?: string
16
+ worktree?: string
17
+ /**
18
+ * The OpenCode SDK's ToolContext declares this as required. It remains
19
+ * optional here because the structural test/embedding surface can be
20
+ * supplied by older hosts; enforcement must skip when it is absent.
21
+ */
22
+ agent?: string
23
+ }
@@ -17,7 +17,6 @@
17
17
  */
18
18
 
19
19
  import type { BackgroundJobBoard } from './background-job-board.ts'
20
- import type { DelegationClient } from './delegation-finalize.ts'
21
20
  import type { GoalStore } from './goal-store.ts'
22
21
  import type { TodoEnforcer } from './todo-enforcer.ts'
23
22
 
@@ -57,8 +56,6 @@ export interface VisionHandler {
57
56
  export interface PantheonV2Bridge {
58
57
  /** Background job board — tracks running/completed/cancelled delegated jobs. */
59
58
  board?: BackgroundJobBoard
60
- /** Structural delegation client — creates child sessions and sends prompts. */
61
- delegationClient?: DelegationClient
62
59
  /** Goal store — file-based persistence for full-auto goal loop. */
63
60
  goalStore?: GoalStore
64
61
  /** Todo enforcer — idle continuation when todos are incomplete. */
@@ -85,7 +82,7 @@ export const BRIDGE_OPTIONS_KEY = '__pantheonV1Bridge' as const
85
82
  * @example
86
83
  * ```ts
87
84
  * // In V1 plugin setup (plugin.ts):
88
- * const bridge = createV2Bridge({ board, delegationClient, goalStore, ... })
85
+ * const bridge = createV2Bridge({ board, goalStore, ... })
89
86
  * // Then pass bridge to V2 context via options
90
87
  * ```
91
88
  */
package/src/plugin-v2.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Pantheon V2 Plugin — full orchestration plugin for OpenCode V2.
3
3
  *
4
4
  * Registers:
5
- * - 9 orchestration tools via V2 tool.transform (with V1 bridge fallback)
5
+ * - 6 orchestration tools via V2 tool.transform (with V1 bridge fallback)
6
6
  * - 4 event subscriptions (session.created, idle, error, compacted)
7
7
  * - Session hooks (prompt, context) and tool hooks (execute.before/after)
8
8
  * - Permission hooks for custom authorization
@@ -408,11 +408,11 @@ async function onSessionIdle(_event: V2SessionEvent): Promise<void> {
408
408
  }
409
409
 
410
410
  async function onSessionError(_event: V2SessionEvent): Promise<void> {
411
- // Error handling — delegation finalize as error.
411
+ // Error handling — board transition to error.
412
412
  const bridge = resolveBridge()
413
413
  if (bridge?.board != null) {
414
- // Bridge available: delegation error handling is done by V1 event hook.
415
- // V2 only logs — V1 finalizeDelegation handles the board transition.
414
+ // Bridge available: error handling is done by the V1 event hook.
415
+ // V2 only logs — V1 owns the board transition.
416
416
  }
417
417
  }
418
418
 
@@ -640,9 +640,6 @@ function createV2ToolDefinitionsFromContext(_context: PluginContext): V2ToolDef[
640
640
  'infrastructure through the V2 bridge (src/pantheon/v2-bridge.ts).'
641
641
 
642
642
  const toolNames = [
643
- 'pantheon_delegate',
644
- 'pantheon_delegation_read',
645
- 'pantheon_delegation_list',
646
643
  'hashline_edit',
647
644
  'pantheon_goal_create',
648
645
  'pantheon_goal_get',
@@ -652,14 +649,6 @@ function createV2ToolDefinitionsFromContext(_context: PluginContext): V2ToolDef[
652
649
  ]
653
650
 
654
651
  const toolDescriptions: Record<string, string> = {
655
- pantheon_delegate:
656
- 'Dispatch a background agent as a child session and register it on the job board. ' +
657
- 'Returns the readable alias (e.g. "apo-1"); read the result with pantheon_delegation_read.',
658
- pantheon_delegation_read:
659
- 'Block until a background delegation finishes (completed/error/cancelled), then return ' +
660
- 'its report markdown (with a trailing agent-activity section) and mark the job reconciled.',
661
- pantheon_delegation_list:
662
- 'List background delegations for the current session, with [unread] for finished jobs.',
663
652
  hashline_edit:
664
653
  'Edit a file anchored by hashline refs (LINE#TAG) instead of raw line numbers. ' +
665
654
  'Ops: replace, append, prepend, delete. ALL refs validated against ORIGINAL file first.',