pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.10

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 (82) hide show
  1. package/.pantheon/code-mode/manifest.json +21 -0
  2. package/CHANGELOG.md +1026 -0
  3. package/README.md +171 -16
  4. package/README.pt-BR.md +99 -13
  5. package/bin/pantheon-init.mjs +178 -21
  6. package/package.json +23 -14
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +65 -0
  9. package/scripts/code_mode_server.py +431 -55
  10. package/scripts/cost.mjs +30 -14
  11. package/scripts/doctor.mjs +178 -9
  12. package/scripts/install/cli-ui.mjs +17 -11
  13. package/scripts/install/config-migration.mjs +4 -6
  14. package/scripts/install/opencode.mjs +137 -35
  15. package/scripts/install/plugin.mjs +3 -33
  16. package/scripts/install/shared.mjs +43 -2
  17. package/scripts/install/strings.mjs +129 -0
  18. package/scripts/install-mcp.mjs +1 -0
  19. package/scripts/manifest-inventory.mjs +410 -0
  20. package/scripts/mcp_persistence_server.py +537 -128
  21. package/scripts/package-evidence.mjs +242 -0
  22. package/scripts/probe-context-rehydrate.mjs +240 -0
  23. package/scripts/probe-pantheon-cost.mjs +313 -0
  24. package/scripts/release-beta-version.mjs +56 -60
  25. package/scripts/sync-artifacts.mjs +169 -0
  26. package/scripts/sync-tui.mjs +121 -53
  27. package/scripts/test-opencode-v1-v2-sandbox.sh +367 -96
  28. package/scripts/validation-policy.mjs +7 -7
  29. package/scripts/version-check.mjs +113 -108
  30. package/scripts/versioning.mjs +230 -86
  31. package/src/agents/mnemosyne.md +1 -1
  32. package/src/agents/zeus.md +2 -2
  33. package/src/mcp/code_mode_server.py +431 -55
  34. package/src/mcp/mcp_persistence_server.py +537 -128
  35. package/src/pantheon/auto-wake.ts +1 -5
  36. package/src/pantheon/background-job-board.ts +43 -1
  37. package/src/pantheon/compaction-handoff.ts +304 -0
  38. package/src/pantheon/cost-command.ts +345 -62
  39. package/src/pantheon/delegate-manager.ts +390 -8
  40. package/src/pantheon/delegation-finalize.ts +32 -0
  41. package/src/pantheon/delegation.ts +235 -18
  42. package/src/pantheon/file-persistence-salvage.ts +64 -0
  43. package/src/pantheon/file-persistence.ts +194 -45
  44. package/src/pantheon/native-probe.ts +234 -0
  45. package/src/pantheon/native-task-status.ts +139 -0
  46. package/src/pantheon/provider-capabilities.ts +221 -0
  47. package/src/pantheon/provider-stream-tracker.ts +182 -0
  48. package/src/pantheon/shared-board.ts +37 -0
  49. package/src/pantheon/task-result-guard.ts +107 -0
  50. package/src/pantheon/token-opt.ts +146 -12
  51. package/src/pantheon/tool-ceiling.ts +159 -0
  52. package/src/pantheon/v2-events.ts +18 -1
  53. package/src/plugin-v2.ts +2 -14
  54. package/src/plugin.ts +28 -17
  55. package/src/plugins/pantheon-hooks.ts +51 -0
  56. package/src/plugins/tui/dist/server.d.ts +1 -1
  57. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  58. package/src/plugins/tui/dist/server.js +1 -1
  59. package/src/plugins/tui/dist/server.js.map +1 -1
  60. package/src/plugins/tui/dist/tui.d.ts +116 -27
  61. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  62. package/src/plugins/tui/dist/tui.js +414 -150
  63. package/src/plugins/tui/dist/tui.js.map +1 -1
  64. package/src/plugins/tui/dist/tui.tsx +447 -84
  65. package/src/plugins/tui/package-lock.json +2 -2
  66. package/src/plugins/tui/package.json +1 -1
  67. package/src/plugins/tui/src/index.tsx +447 -84
  68. package/.pantheon/code-mode/.gitkeep +0 -0
  69. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +0 -8
  70. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +0 -8
  71. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +0 -8
  72. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +0 -8
  73. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +0 -8
  74. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +0 -8
  75. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +0 -8
  76. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +0 -8
  77. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +0 -8
  78. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +0 -8
  79. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +0 -8
  80. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +0 -8
  81. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +0 -11
  82. package/src/pantheon/delegate-task-adapter.ts +0 -444
File without changes
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 1,
5
- "timestamp": "2026-07-24T10:57:09.398807+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 10,
5
- "timestamp": "2026-07-24T11:14:15.230136+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 11,
5
- "timestamp": "2026-07-24T11:21:25.938170+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 2,
5
- "timestamp": "2026-07-24T11:00:35.522164+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 3,
5
- "timestamp": "2026-07-24T11:01:21.558497+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 4,
5
- "timestamp": "2026-07-24T11:02:18.695362+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 5,
5
- "timestamp": "2026-07-24T11:04:13.337080+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 6,
5
- "timestamp": "2026-07-24T11:07:03.322884+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 7,
5
- "timestamp": "2026-07-24T11:08:30.280736+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 8,
5
- "timestamp": "2026-07-24T11:10:03.409420+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "phase": 0,
4
- "turn_count": 9,
5
- "timestamp": "2026-07-24T11:12:14.924527+00:00",
6
- "context_hash": "",
7
- "version": 2
8
- }
@@ -1,8 +0,0 @@
1
- {
2
- "slug": "initial-token-40k",
3
- "last_action": "2026-07-24T11:21:25.938170+00:00",
4
- "turn_count": 11,
5
- "current_phase": 0,
6
- "status": "alive",
7
- "next_action": "checkpoint saved"
8
- }
@@ -1,11 +0,0 @@
1
- {
2
- "task_id": "initial-token-40k",
3
- "mode": "deepwork",
4
- "start_time": "2026-07-24T10:57:06.212552+00:00",
5
- "last_activity": "2026-07-24T11:21:25.938170+00:00",
6
- "status": "in_progress",
7
- "gate_history": [],
8
- "stop_reason": null,
9
- "stopped_at": null,
10
- "turn_count": 11
11
- }
@@ -1,444 +0,0 @@
1
- /**
2
- * V1 Session Task Adapter (WS1, PR #94) — task()-shaped function over the V1
3
- * SDK session surface (`session.create` / `promptAsync` / `messages`).
4
- *
5
- * The thin manager (delegate-manager.ts) orchestrates launch → monitor →
6
- * reconcile → verify; THIS module is the only place that touches the host:
7
- *
8
- * - `createSessionTaskFn()` — prompt acceptance (bounded) + completion
9
- * observed via the board (the idle hook / finalize path owns the terminal
10
- * transition) + verified content pulled from the MD report written by
11
- * `finalizeDelegation`. No MD report (or an empty one) means the result is
12
- * NOT verified — the manager records `error`, never `completed`.
13
- * - `createNativeDelegateTools()` — the three delegation tools in the same
14
- * structural shape as the legacy toolset, wired to per-parent managers.
15
- * The child session is created FIRST so `taskID == childSessionID`: the
16
- * V1 idle hook and the manager share one board record (no double
17
- * registration, no scheduler of its own).
18
- *
19
- * The legacy fire-and-forget path (delegation.ts) is untouched; the plugin
20
- * selects legacy vs native via `PANTHEON_DELEGATE_MODE`.
21
- *
22
- * @module delegate-task-adapter
23
- */
24
-
25
- import { z } from 'zod'
26
-
27
- import type { BackgroundJobBoard } from './background-job-board.ts'
28
- import {
29
- createDelegateManager,
30
- type DelegateManager,
31
- type DelegateManagerOptions,
32
- isDelegationEnabled,
33
- type NativeTaskFn,
34
- } from './delegate-manager.ts'
35
- import type { DelegationToolset } from './delegation.ts'
36
- import {
37
- DELEGATION_DEFAULTS,
38
- type DelegationClient,
39
- readDelegationReport,
40
- } from './delegation-finalize.ts'
41
- import type { StepCapTracker } from './step-cap.ts'
42
-
43
- // ─── Mode gate + kill-switch ───────────────────────────────────────────
44
-
45
- /** Delegation backend selected by `PANTHEON_DELEGATE_MODE`. */
46
- export type DelegateMode = 'native' | 'legacy'
47
-
48
- /**
49
- * Resolve the delegation backend: `PANTHEON_DELEGATE_MODE=native` selects the
50
- * thin native manager; anything else (unset, `legacy`, garbage) stays on the
51
- * V1 fire-and-forget path. Fail-safe default = legacy (V1 intacto).
52
- */
53
- export function resolveDelegateMode(
54
- env: Record<string, string | undefined> = process.env,
55
- ): DelegateMode {
56
- return (env.PANTHEON_DELEGATE_MODE ?? '').trim().toLowerCase() === 'native' ? 'native' : 'legacy'
57
- }
58
-
59
- /** Minimal structural shape of a delegation tool (legacy or native). */
60
- export interface KillSwitchableTool {
61
- description: string
62
- args: unknown
63
- // biome-ignore lint/suspicious/noExplicitAny: tool args/ctx shapes differ per toolset
64
- execute: (args: any, ctx: any) => Promise<string>
65
- }
66
-
67
- /**
68
- * Wrap a delegation toolset with the `PANTHEON_DELEGATION=off` kill-switch.
69
- * Every entry exposing `execute()` is guarded (throws when disabled);
70
- * non-tool entries (e.g. legacy `finalizeDelegation`) pass through
71
- * untouched — the completion observer must keep working so children never
72
- * orphan. Native tools already guard internally; wrapping them again is a
73
- * harmless double-check. Returns a new object; the input is untouched.
74
- */
75
- export function withDelegationKillSwitch<T extends object>(
76
- toolset: T,
77
- env: Record<string, string | undefined> = process.env,
78
- ): T {
79
- const out: Record<string, unknown> = {}
80
- for (const [key, value] of Object.entries(toolset)) {
81
- const tool = value as Partial<KillSwitchableTool>
82
- if (tool !== null && typeof tool === 'object' && typeof tool.execute === 'function') {
83
- const execute = tool.execute.bind(tool)
84
- out[key] = {
85
- ...(tool as Record<string, unknown>),
86
- // biome-ignore lint/suspicious/noExplicitAny: passthrough to the wrapped tool
87
- execute: async (args: any, ctx: any) => {
88
- if (!isDelegationEnabled(env)) {
89
- throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
90
- }
91
- return execute(args, ctx)
92
- },
93
- }
94
- } else {
95
- out[key] = value
96
- }
97
- }
98
- return out as T
99
- }
100
-
101
- // ─── Args schemas (same contract as the legacy toolset) ────────────────
102
-
103
- const delegateArgs = {
104
- prompt: z.string().min(1).describe('Task prompt delivered to the background agent.'),
105
- agent: z.string().min(1).describe('Agent name, e.g. "apollo" or "hermes".'),
106
- description: z.string().optional().describe('Human-readable description shown on the job board.'),
107
- read_only: z.boolean().optional().describe('Advisory flag for Phase 4 read-only enforcement.'),
108
- model: z
109
- .string()
110
- .optional()
111
- .describe(
112
- 'Explicit model for the child session (provider/model, e.g. "opencode/deepseek-v4-flash-free").',
113
- ),
114
- } satisfies z.ZodRawShape
115
-
116
- const readArgs = {
117
- id: z.string().min(1).describe('Job alias (e.g. "apo-1") or task ID to read.'),
118
- } satisfies z.ZodRawShape
119
-
120
- const listArgs = {} satisfies z.ZodRawShape
121
-
122
- export interface ToolContextLike {
123
- sessionID: string
124
- agent?: string
125
- }
126
-
127
- // ─── Session task function ─────────────────────────────────────────────
128
-
129
- export interface SessionTaskAdapterOptions {
130
- client: DelegationClient
131
- board: BackgroundJobBoard
132
- outputDir?: string
133
- /** Bound for the promptAsync acceptance request (default: 60_000). */
134
- promptTimeoutMs?: number
135
- /** Bound for observing the terminal transition (default: 900_000). */
136
- settleTimeoutMs?: number
137
- }
138
-
139
- /** Split `provider/model` into the `{ id, providerID }` ref session.create expects. */
140
- export function parseModelRef(model: string): { id: string; providerID: string } | undefined {
141
- const idx = model.indexOf('/')
142
- if (idx <= 0 || idx === model.length - 1) return undefined
143
- return { providerID: model.slice(0, idx), id: model.slice(idx + 1) }
144
- }
145
-
146
- /** Text after `## Output` (cut before a trailing `**Error**:` line). */
147
- export function extractOutputSection(md: string | undefined): string {
148
- if (md === undefined) return ''
149
- const marker = '## Output'
150
- const at = md.indexOf(marker)
151
- const body = at < 0 ? md : md.slice(at + marker.length)
152
- const errAt = body.indexOf('\n**Error**:')
153
- return (errAt < 0 ? body : body.slice(0, errAt)).trim()
154
- }
155
-
156
- async function promptAccept(
157
- client: DelegationClient,
158
- taskID: string,
159
- agent: string,
160
- prompt: string,
161
- model: string | undefined,
162
- signal: AbortSignal | undefined,
163
- timeoutMs: number,
164
- ): Promise<void> {
165
- const modelRef = model !== undefined ? parseModelRef(model) : undefined
166
- let timer: ReturnType<typeof setTimeout> | undefined
167
- try {
168
- await Promise.race([
169
- client.session
170
- .promptAsync({
171
- path: { id: taskID },
172
- body: {
173
- agent,
174
- ...(modelRef !== undefined ? { model: modelRef } : {}),
175
- parts: [{ type: 'text', text: prompt }],
176
- },
177
- ...(signal !== undefined ? { signal } : {}),
178
- })
179
- .then(
180
- () => undefined,
181
- (error: unknown) => {
182
- throw new Error(
183
- `promptAsync rejected: ${error instanceof Error ? error.message : String(error)}`,
184
- )
185
- },
186
- ),
187
- new Promise<never>((_, reject) => {
188
- timer = setTimeout(
189
- () => reject(new Error(`promptAsync timed out after ${timeoutMs}ms`)),
190
- timeoutMs,
191
- )
192
- timer.unref?.()
193
- }),
194
- ])
195
- } finally {
196
- if (timer !== undefined) clearTimeout(timer)
197
- }
198
- }
199
-
200
- /**
201
- * Build the task()-shaped function over a V1 session client.
202
- * The child session MUST already exist with id == input.taskID (created by
203
- * the tool wrapper before manager.launch so both share one board record).
204
- */
205
- export function createSessionTaskFn(options: SessionTaskAdapterOptions): NativeTaskFn {
206
- const {
207
- client,
208
- board,
209
- outputDir = DELEGATION_DEFAULTS.outputDir,
210
- promptTimeoutMs = 60_000,
211
- settleTimeoutMs = DELEGATION_DEFAULTS.timeoutMs,
212
- } = options
213
- return async ({ agent, prompt, model, signal, taskID }) => {
214
- await promptAccept(client, taskID, agent, prompt, model, signal, promptTimeoutMs)
215
- const terminal = await board.waitForTerminal(taskID, settleTimeoutMs)
216
- const md = await readDelegationReport(outputDir, terminal)
217
- const content = extractOutputSection(md) || terminal.resultSummary || ''
218
- return { content }
219
- }
220
- }
221
-
222
- // ─── Native toolset ────────────────────────────────────────────────────
223
-
224
- export interface NativeDelegateWiring {
225
- board: BackgroundJobBoard
226
- client: DelegationClient
227
- outputDir?: string
228
- env?: Record<string, string | undefined>
229
- isRootSession: (sessionID: string) => boolean
230
- registerChildSession?: (sessionID: string, parentID: string) => void
231
- registerReadOnlySession?: (
232
- sessionID: string,
233
- info: { agent: string; readOnlyFlag?: boolean },
234
- ) => void
235
- isReadOnlyAgent?: (agent: string) => boolean
236
- /** Per-agent model override in `provider/model` form (preset/routing). */
237
- agentModel?: (agent: string) => string | undefined
238
- managerOptions?: Partial<
239
- Omit<DelegateManagerOptions, 'board' | 'task' | 'parentSessionID' | 'env'>
240
- >
241
- stepCap?: StepCapTracker
242
- }
243
-
244
- export interface NativeDelegateToolset {
245
- pantheon_delegate: {
246
- description: string
247
- args: typeof delegateArgs
248
- execute(args: z.infer<z.ZodObject<typeof delegateArgs>>, ctx: ToolContextLike): Promise<string>
249
- }
250
- pantheon_delegation_read: {
251
- description: string
252
- args: typeof readArgs
253
- execute(args: z.infer<z.ZodObject<typeof readArgs>>, ctx: ToolContextLike): Promise<string>
254
- }
255
- pantheon_delegation_list: {
256
- description: string
257
- args: typeof listArgs
258
- execute(args: z.infer<z.ZodObject<typeof listArgs>>, ctx: ToolContextLike): Promise<string>
259
- }
260
- }
261
-
262
- function firstLine(text: string, max = 60): string {
263
- const line =
264
- (text ?? '')
265
- .split('\n')
266
- .map((l) => l.trim())
267
- .find((l) => l !== '') ?? ''
268
- return line.length > max ? `${line.slice(0, max - 1)}…` : line
269
- }
270
-
271
- export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeDelegateToolset {
272
- const {
273
- board,
274
- client,
275
- outputDir = DELEGATION_DEFAULTS.outputDir,
276
- env = process.env,
277
- isRootSession,
278
- registerChildSession,
279
- registerReadOnlySession,
280
- isReadOnlyAgent,
281
- agentModel,
282
- managerOptions = {},
283
- stepCap,
284
- } = wiring
285
- const task = createSessionTaskFn({
286
- client,
287
- board,
288
- outputDir,
289
- ...(managerOptions.timeoutMs !== undefined
290
- ? { promptTimeoutMs: managerOptions.timeoutMs }
291
- : {}),
292
- })
293
- // One thin manager per parent session; the board is shared.
294
- const managers = new Map<string, DelegateManager>()
295
- function managerFor(parentSessionID: string): DelegateManager {
296
- let manager = managers.get(parentSessionID)
297
- if (!manager) {
298
- manager = createDelegateManager({
299
- ...managerOptions,
300
- board,
301
- task,
302
- parentSessionID,
303
- env,
304
- ...(stepCap !== undefined ? { stepCap } : {}),
305
- })
306
- managers.set(parentSessionID, manager)
307
- }
308
- return manager
309
- }
310
-
311
- return {
312
- pantheon_delegate: {
313
- description:
314
- 'Dispatch a background agent as a child session (native task() semantics: the call returns only after the verified result). Returns one line `[pantheon:<alias>] <agent> — <summary> — <state>`.',
315
- args: delegateArgs,
316
- execute: async (args, ctx) => {
317
- if (!isDelegationEnabled(env)) {
318
- throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
319
- }
320
- if (!isRootSession(ctx.sessionID)) {
321
- throw new Error(
322
- `pantheon_delegate rejected: session ${ctx.sessionID} is a sub-session — only root sessions can delegate`,
323
- )
324
- }
325
- const model = args.model ?? agentModel?.(args.agent)
326
- let childID: string
327
- try {
328
- const modelRef = model !== undefined ? parseModelRef(model) : undefined
329
- const created = await client.session.create({
330
- body: {
331
- parentID: ctx.sessionID,
332
- title: args.description ?? firstLine(args.prompt),
333
- ...(modelRef !== undefined ? { model: modelRef } : {}),
334
- },
335
- })
336
- childID = created.id
337
- } catch (err) {
338
- const reason = err instanceof Error ? err.message : String(err)
339
- return `pantheon_delegate failed: session.create rejected: ${reason}`
340
- }
341
- registerChildSession?.(childID, ctx.sessionID)
342
- if (args.read_only === true || isReadOnlyAgent?.(args.agent) === true) {
343
- const info: { agent: string; readOnlyFlag?: boolean } = { agent: args.agent }
344
- if (args.read_only !== undefined) info.readOnlyFlag = args.read_only
345
- registerReadOnlySession?.(childID, info)
346
- }
347
- const receipt = await managerFor(ctx.sessionID).launch({
348
- agent: args.agent,
349
- prompt: args.prompt,
350
- ...(args.description !== undefined ? { description: args.description } : {}),
351
- ...(args.model !== undefined ? { model: args.model } : {}),
352
- taskID: childID,
353
- })
354
- return receipt.line
355
- },
356
- },
357
- pantheon_delegation_read: {
358
- description:
359
- 'Block until a background delegation finishes, then return its verified report markdown and mark the job reconciled.',
360
- args: readArgs,
361
- execute: async (args, ctx) => {
362
- if (!isDelegationEnabled(env)) {
363
- throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
364
- }
365
- return managerFor(ctx.sessionID).read(args.id)
366
- },
367
- },
368
- pantheon_delegation_list: {
369
- description: 'List background delegations with [pantheon:<alias>] / [native] tags per line.',
370
- args: listArgs,
371
- execute: async (_args, ctx) => {
372
- if (!isDelegationEnabled(env)) {
373
- throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
374
- }
375
- const lines = await managerFor(ctx.sessionID).list()
376
- return lines.length > 0 ? lines.join('\n') : 'No delegations.'
377
- },
378
- },
379
- }
380
- }
381
-
382
- // ─── Plugin native branch (WS1, PR #94) ─────────────────────────────────
383
- //
384
- // Extracted verbatim from the `delegateMode === 'native'` branch in
385
- // src/plugin.ts so the real mounting (model lowercasing, read-only set,
386
- // conditional managerOptions, finalizeDelegation passthrough) is unit
387
- // testable with fakes — importing plugin.ts itself would pull module-level
388
- // singletons (board, timers, logger). The plugin calls this helper; the
389
- // wiring test calls the same helper with the same shapes.
390
-
391
- /** Raw materials the plugin owns for the native branch. */
392
- export interface PluginNativeDelegationWiring {
393
- board: BackgroundJobBoard
394
- client: DelegationClient
395
- isRootSession: (sessionID: string) => boolean
396
- registerChildSession: (sessionID: string, parentID: string) => void
397
- registerReadOnlySession: (
398
- sessionID: string,
399
- info: { agent: string; readOnlyFlag?: boolean },
400
- ) => void
401
- readOnlyAgents: ReadonlySet<string>
402
- agentModels: Record<string, string | undefined>
403
- stepCap: StepCapTracker
404
- wallClockTimeoutMs?: number
405
- finalizeDelegation: DelegationToolset['finalizeDelegation']
406
- }
407
-
408
- /** Native toolset + the legacy completion observer (never kill-switched). */
409
- export type PluginNativeDelegation = NativeDelegateToolset & {
410
- finalizeDelegation: DelegationToolset['finalizeDelegation']
411
- }
412
-
413
- export function buildPluginNativeDelegation(
414
- wiring: PluginNativeDelegationWiring,
415
- ): PluginNativeDelegation {
416
- const {
417
- board,
418
- client,
419
- isRootSession,
420
- registerChildSession,
421
- registerReadOnlySession,
422
- readOnlyAgents,
423
- agentModels,
424
- stepCap,
425
- wallClockTimeoutMs,
426
- finalizeDelegation,
427
- } = wiring
428
- return {
429
- ...createNativeDelegateTools({
430
- board,
431
- client,
432
- isRootSession,
433
- registerChildSession,
434
- registerReadOnlySession,
435
- isReadOnlyAgent: (agent) => readOnlyAgents.has(agent.toLowerCase()),
436
- agentModel: (agent) => agentModels[agent.toLowerCase()],
437
- stepCap,
438
- ...(wallClockTimeoutMs !== undefined
439
- ? { managerOptions: { timeoutMs: wallClockTimeoutMs } }
440
- : {}),
441
- }),
442
- finalizeDelegation,
443
- }
444
- }