pantheon-opencode 1.4.2 → 1.5.0-beta.1

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 (98) hide show
  1. package/.pantheon/code-mode/.gitkeep +0 -0
  2. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
  3. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
  4. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
  5. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
  6. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
  7. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
  8. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
  9. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
  10. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
  11. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
  12. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
  13. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
  14. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
  15. package/.pantheon/code-mode/README.md +93 -0
  16. package/.pantheon/code-mode/checkpoint-session.py +7 -0
  17. package/.pantheon/code-mode/checkpoint-session.sh +11 -0
  18. package/.pantheon/code-mode/checkpoint_session.py +481 -0
  19. package/.pantheon/code-mode/compress-inline.py +450 -0
  20. package/.pantheon/code-mode/eval-llm-judge.py +255 -0
  21. package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
  22. package/.pantheon/code-mode/eval-run.py +197 -0
  23. package/.pantheon/code-mode/eval-static.py +321 -0
  24. package/.pantheon/code-mode/example-sync.sh +5 -0
  25. package/.pantheon/code-mode/session-end-save.py +186 -0
  26. package/.pantheon/code-mode/session-end-save.sh +18 -0
  27. package/.pantheon/code-mode/test-allowed.sh +5 -0
  28. package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
  29. package/.pantheon/code-mode/test-env.sh +7 -0
  30. package/.pantheon/code-mode/test-prlimit.sh +9 -0
  31. package/.pantheon/code-mode/test-timeout.sh +6 -0
  32. package/README.md +158 -1189
  33. package/README.pt-BR.md +222 -0
  34. package/bin/pantheon-init.mjs +30 -11
  35. package/opencode.json +1 -0
  36. package/package.json +14 -3
  37. package/plugin.json +15 -0
  38. package/scripts/_pantheon_paths.py +48 -7
  39. package/scripts/ci-validate-yaml.py +7 -2
  40. package/scripts/code_mode_server.py +33 -2
  41. package/scripts/doctor.mjs +135 -127
  42. package/scripts/generate-preset-docs.mjs +127 -66
  43. package/scripts/generate-prompts.sh +2 -2
  44. package/scripts/install/cli-ui.mjs +8 -1
  45. package/scripts/install/config-migration.mjs +401 -0
  46. package/scripts/install/health-check.mjs +3 -3
  47. package/scripts/install/model-picker.mjs +72 -23
  48. package/scripts/install/opencode-version.mjs +47 -0
  49. package/scripts/install/opencode.mjs +261 -88
  50. package/scripts/install/plugin.mjs +112 -42
  51. package/scripts/install/shared.mjs +13 -158
  52. package/scripts/install-mcp.mjs +44 -232
  53. package/scripts/mcp_persistence_server.py +344 -15
  54. package/scripts/mcp_resources_server.py +144 -40
  55. package/scripts/memory_mcp_server.py +2 -2
  56. package/scripts/recover-zenodo-deposition.mjs +146 -0
  57. package/scripts/sync-tui.mjs +32 -7
  58. package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
  59. package/scripts/uninstall.mjs +298 -954
  60. package/scripts/validate-zenodo-release.mjs +154 -0
  61. package/scripts/zenodo-artifact.mjs +21 -0
  62. package/scripts/zenodo-release-state.mjs +53 -0
  63. package/src/mcp/_pantheon_paths.py +48 -7
  64. package/src/mcp/code_mode_server.py +33 -2
  65. package/src/mcp/eval_store.py +1 -1
  66. package/src/mcp/mcp_persistence_server.py +344 -15
  67. package/src/mcp/mcp_resources_server.py +144 -40
  68. package/src/mcp/pantheon_vision_server.py +1 -1
  69. package/src/mcp/toon_codec.py +491 -0
  70. package/src/pantheon/background-job-board.ts +67 -1
  71. package/src/pantheon/cost-command.ts +6 -2
  72. package/src/pantheon/delegate-manager.ts +384 -0
  73. package/src/pantheon/delegate-task-adapter.ts +444 -0
  74. package/src/pantheon/delegation-notify.ts +35 -20
  75. package/src/pantheon/model-command.ts +86 -32
  76. package/src/pantheon/token-opt.ts +555 -0
  77. package/src/pantheon/toon-codec.ts +481 -0
  78. package/src/pantheon/tui-version-check.ts +1 -3
  79. package/src/pantheon/v2-bridge.ts +129 -0
  80. package/src/pantheon/v2-events.ts +131 -0
  81. package/src/pantheon/v2-hooks.ts +287 -0
  82. package/src/pantheon/v2-tool-definitions.ts +331 -0
  83. package/src/plugin-v2/index.ts +21 -0
  84. package/src/plugin-v2.ts +836 -0
  85. package/src/plugin.ts +57 -10
  86. package/src/plugins/tui/dist/server.d.ts +1 -1
  87. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  88. package/src/plugins/tui/dist/server.js +1 -1
  89. package/src/plugins/tui/dist/server.js.map +1 -1
  90. package/src/plugins/tui/dist/tui.d.ts +10 -7
  91. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  92. package/src/plugins/tui/dist/tui.js +11 -8
  93. package/src/plugins/tui/dist/tui.js.map +1 -1
  94. package/src/plugins/tui/dist/tui.tsx +22 -16
  95. package/src/plugins/tui/package-lock.json +51 -51
  96. package/src/plugins/tui/package.json +7 -7
  97. package/src/plugins/tui/src/index.tsx +22 -16
  98. package/src/skills/README.md +0 -2
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Delegate Manager (WS1, PR #94) — thin manager over the native task() call.
3
+ *
4
+ * Relaunch of the 1.4.2 delegation as a THIN manager: launch / monitor /
5
+ * reconcile over an injected `task()`-shaped function. No monolithic
6
+ * scheduler: no timers of its own beyond wait/timeout bounds, no idle-event
7
+ * plumbing, no hidden queues. Completion is recorded ONLY after the result
8
+ * is verified (non-empty content); anything else becomes `error`.
9
+ *
10
+ * - Minimal records: every launch returns `{ id, agent, state, line }`
11
+ * (id, agent, state, one short line). Full reports only via `read()`.
12
+ * - Auto-reconcile + signal delete on the spot after every terminal state.
13
+ * - Entry cap + aggressive prune of completed/reconciled jobs.
14
+ * - Concurrency limit (read live from the board) + foreground fallback.
15
+ * - Model failover across an ordered model list.
16
+ * - Step budgets reuse StepCapTracker (routing.yml max_steps, already
17
+ * extended beyond zeus) + summarize-and-stop cut.
18
+ * - Kill-switch: `PANTHEON_DELEGATION=off` disables launch/read/list.
19
+ *
20
+ * The V1 fire-and-forget path (delegation.ts) is untouched; the plugin wires
21
+ * this manager in front of it behind `PANTHEON_DELEGATE_MODE`.
22
+ *
23
+ * @module delegate-manager
24
+ */
25
+
26
+ import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
27
+ import {
28
+ buildStopInstruction,
29
+ cappedSummary,
30
+ DEFAULT_MAX_STEPS,
31
+ type StepCapTracker,
32
+ } from './step-cap.ts'
33
+
34
+ // ─── Types ─────────────────────────────────────────────────────────────
35
+
36
+ /** Input shape for the native task() call (injected — fake in tests). */
37
+ export interface NativeTaskInput {
38
+ agent: string
39
+ prompt: string
40
+ model?: string
41
+ signal?: AbortSignal
42
+ /** Board taskID assigned by the manager (== child session id on V1 hosts). */
43
+ taskID: string
44
+ parentSessionID: string
45
+ }
46
+
47
+ /** Result shape for the native task() call. */
48
+ export interface NativeTaskResult {
49
+ content?: string | null
50
+ tokensInput?: number
51
+ tokensOutput?: number
52
+ }
53
+
54
+ /** task()-shaped function: run one agent turn and return its result. */
55
+ export type NativeTaskFn = (input: NativeTaskInput) => Promise<NativeTaskResult>
56
+
57
+ /** Minimal launch receipt: id, agent, state, one short line. */
58
+ export interface ManagerReceipt {
59
+ id: string
60
+ agent: string
61
+ state: 'reconciled' | 'error'
62
+ line: string
63
+ }
64
+
65
+ export interface LaunchArgs {
66
+ agent: string
67
+ prompt: string
68
+ description?: string
69
+ model?: string
70
+ /**
71
+ * Board taskID to register. The V1 session adapter supplies the child
72
+ * session id here so the idle hook and the manager share one record.
73
+ * Defaults to a generated `native-<ts>-<seq>` id.
74
+ */
75
+ taskID?: string
76
+ }
77
+
78
+ export interface DelegateManagerOptions {
79
+ board: BackgroundJobBoard
80
+ task: NativeTaskFn
81
+ parentSessionID: string
82
+ /** Env mapping (default: process.env). Kill-switch reads PANTHEON_DELEGATION. */
83
+ env?: Record<string, string | undefined>
84
+ /** Ordered model list for failover (default: single attempt, inherited model). */
85
+ models?: string[]
86
+ /** Per-attempt wall-clock budget in ms (default: 120_000). */
87
+ timeoutMs?: number
88
+ /** read() wait budget in ms (default: 60_000). */
89
+ readTimeoutMs?: number
90
+ /** Run inline without a board record when concurrency is full (default: true). */
91
+ foregroundFallback?: boolean
92
+ /** Hard cap on board entries; oldest terminal jobs evicted first (default: 50). */
93
+ maxEntries?: number
94
+ /** Aggressive prune: completed/reconciled jobs kept (default: 10). */
95
+ keepCompleted?: number
96
+ /** Per-agent step budgets (routing.yml max_steps). */
97
+ stepCap?: StepCapTracker
98
+ }
99
+
100
+ export interface DelegateManager {
101
+ launch(args: LaunchArgs): Promise<ManagerReceipt>
102
+ read(idOrAlias: string): Promise<string>
103
+ list(): Promise<string[]>
104
+ recover(): Promise<void>
105
+ }
106
+
107
+ // ─── Kill-switch ───────────────────────────────────────────────────────
108
+
109
+ /** False only when `PANTHEON_DELEGATION=off` (case-insensitive). */
110
+ export function isDelegationEnabled(
111
+ env: Record<string, string | undefined> = process.env,
112
+ ): boolean {
113
+ return (env.PANTHEON_DELEGATION ?? '').trim().toLowerCase() !== 'off'
114
+ }
115
+
116
+ // ─── Helpers ───────────────────────────────────────────────────────────
117
+
118
+ const LINE_MAX = 160
119
+
120
+ function firstLine(text: string, max = 80): string {
121
+ const line =
122
+ (text ?? '')
123
+ .split('\n')
124
+ .map((l) => l.trim())
125
+ .find((l) => l !== '') ?? ''
126
+ return line.length > max ? `${line.slice(0, max - 1)}…` : line
127
+ }
128
+
129
+ function shortState(state: BackgroundJobRecord['state']): string {
130
+ switch (state) {
131
+ case 'completed':
132
+ return 'OK'
133
+ case 'error':
134
+ return 'ERR'
135
+ case 'cancelled':
136
+ return 'CAN'
137
+ case 'running':
138
+ return 'RUN'
139
+ default:
140
+ return 'REC'
141
+ }
142
+ }
143
+
144
+ function boardLine(job: BackgroundJobRecord): string {
145
+ const desc = firstLine(job.description, 80)
146
+ return `[pantheon:${job.alias}] ${job.agent} — ${desc} — ${shortState(job.state)}`.slice(
147
+ 0,
148
+ LINE_MAX,
149
+ )
150
+ }
151
+
152
+ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
153
+ let timer: ReturnType<typeof setTimeout> | undefined
154
+ const guard = new Promise<never>((_, reject) => {
155
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
156
+ timer.unref?.()
157
+ })
158
+ return Promise.race([promise, guard]).finally(() => {
159
+ if (timer !== undefined) clearTimeout(timer)
160
+ })
161
+ }
162
+
163
+ // ─── Factory ───────────────────────────────────────────────────────────
164
+
165
+ export function createDelegateManager(options: DelegateManagerOptions): DelegateManager {
166
+ const {
167
+ board,
168
+ task,
169
+ parentSessionID,
170
+ env = process.env,
171
+ models = [],
172
+ timeoutMs = 120_000,
173
+ readTimeoutMs = 60_000,
174
+ foregroundFallback = true,
175
+ maxEntries = 50,
176
+ keepCompleted = 10,
177
+ stepCap,
178
+ } = options
179
+
180
+ // Synchronous in-flight reservations per agent: launch() must decide
181
+ // background vs foreground BEFORE its first await, otherwise concurrent
182
+ // launches without interleaved awaits would all see an empty board.
183
+ const pendingByAgent = new Map<string, number>()
184
+ let seq = 0
185
+ const foregroundRecent: { agent: string; line: string; at: number }[] = []
186
+
187
+ function pending(agent: string): number {
188
+ return pendingByAgent.get(agent.toLowerCase()) ?? 0
189
+ }
190
+
191
+ function guardEnabled(): void {
192
+ if (!isDelegationEnabled(env)) {
193
+ throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
194
+ }
195
+ }
196
+
197
+ async function runTask(
198
+ agent: string,
199
+ prompt: string,
200
+ explicitModel: string | undefined,
201
+ taskID: string,
202
+ ): Promise<{ ok: true; result: NativeTaskResult } | { ok: false; error: string }> {
203
+ const attempts: (string | undefined)[] =
204
+ explicitModel !== undefined ? [explicitModel] : models.length > 0 ? models : [undefined]
205
+ let lastError = 'no model attempts configured'
206
+ for (const model of attempts) {
207
+ try {
208
+ const controller = new AbortController()
209
+ const result = await withTimeout(
210
+ task({
211
+ agent,
212
+ prompt,
213
+ signal: controller.signal,
214
+ taskID,
215
+ parentSessionID,
216
+ ...(model !== undefined ? { model } : {}),
217
+ }),
218
+ timeoutMs,
219
+ `task(${agent})`,
220
+ )
221
+ return { ok: true, result }
222
+ } catch (err) {
223
+ lastError = err instanceof Error ? err.message : String(err)
224
+ }
225
+ }
226
+ return { ok: false, error: lastError }
227
+ }
228
+
229
+ async function settle(
230
+ taskID: string,
231
+ alias: string,
232
+ verifyError?: string,
233
+ ): Promise<ManagerReceipt> {
234
+ const job = board.get(taskID)
235
+ if (!job) throw new Error(`job ${taskID} vanished after terminal state`)
236
+ // The receipt reports the VERIFIED outcome, not the raw terminal state:
237
+ // a `completed` the manager could not verify stays `error` for the
238
+ // caller ("complete ONLY after verified result"). Reconcile is only the
239
+ // acknowledgment that the result was consumed.
240
+ const terminalState = job.state
241
+ const failed = verifyError !== undefined || terminalState === 'error'
242
+ // Auto-reconcile + delete the signal file on the spot.
243
+ if (job.state !== 'reconciled') await board.markReconciled(taskID)
244
+ await board.deleteSignal(alias)
245
+ // Teto de entradas + prune agressivo de completed.
246
+ await board.pruneCompleted(keepCompleted)
247
+ await board.enforceEntryCap(maxEntries)
248
+ const settled = board.get(taskID)
249
+ const line = failed
250
+ ? `[pantheon:${alias}] ${job.agent} — ERROR: ${firstLine(verifyError ?? settled?.lastStatusError ?? job.lastStatusError ?? 'failed', 90)}`.slice(
251
+ 0,
252
+ LINE_MAX,
253
+ )
254
+ : boardLine(settled ?? job)
255
+ return { id: taskID, agent: job.agent, state: failed ? 'error' : 'reconciled', line }
256
+ }
257
+
258
+ /** Apply a terminal transition best-effort: a racing hook may have gotten there first. */
259
+ async function applyTerminal(
260
+ taskID: string,
261
+ state: 'completed' | 'error',
262
+ fields: { resultSummary?: string; error?: string },
263
+ ): Promise<void> {
264
+ try {
265
+ await board.updateStatus({ taskID, state, ...fields })
266
+ } catch (err) {
267
+ const msg = err instanceof Error ? err.message : String(err)
268
+ if (!msg.startsWith('Invalid state transition')) throw err
269
+ // Lost the race to another terminal writer (e.g. the V1 idle hook) —
270
+ // the record stands; verification still governs the receipt.
271
+ }
272
+ }
273
+
274
+ async function launchBackground(args: LaunchArgs, prompt: string): Promise<ManagerReceipt> {
275
+ const key = args.agent.toLowerCase()
276
+ pendingByAgent.set(key, pending(args.agent) + 1)
277
+ seq += 1
278
+ const taskID = args.taskID ?? `native-${Date.now()}-${seq}`
279
+ try {
280
+ const record = await board.registerLaunch({
281
+ taskID,
282
+ parentSessionID,
283
+ agent: args.agent,
284
+ description: args.description ?? firstLine(args.prompt, 120),
285
+ })
286
+ const outcome = await runTask(args.agent, prompt, args.model, taskID)
287
+ if (!outcome.ok) {
288
+ await applyTerminal(taskID, 'error', { error: outcome.error })
289
+ return await settle(taskID, record.alias, outcome.error)
290
+ }
291
+ if (typeof outcome.result.content !== 'string' || outcome.result.content.trim() === '') {
292
+ const verifyError = 'task result empty — not verified, never completed'
293
+ await applyTerminal(taskID, 'error', { error: verifyError })
294
+ return await settle(taskID, record.alias, verifyError)
295
+ }
296
+ await applyTerminal(taskID, 'completed', {
297
+ resultSummary: firstLine(outcome.result.content, 500),
298
+ })
299
+ return await settle(taskID, record.alias)
300
+ } finally {
301
+ pendingByAgent.set(key, Math.max(0, pending(args.agent) - 1))
302
+ }
303
+ }
304
+
305
+ async function launchForeground(args: LaunchArgs, prompt: string): Promise<ManagerReceipt> {
306
+ seq += 1
307
+ const id = args.taskID ?? `fg-${Date.now()}-${seq}`
308
+ const outcome = await runTask(args.agent, prompt, args.model, id)
309
+ const ok =
310
+ outcome.ok &&
311
+ typeof outcome.result.content === 'string' &&
312
+ outcome.result.content.trim() !== ''
313
+ const detail = ok
314
+ ? firstLine((outcome as { ok: true; result: NativeTaskResult }).result.content ?? '', 90)
315
+ : `ERROR: ${(outcome as { ok: false; error: string }).error ?? 'empty result — not verified'}`
316
+ const line = `[native] ${args.agent} — ${detail} — ${ok ? 'OK' : 'ERR'} (foreground)`.slice(
317
+ 0,
318
+ LINE_MAX,
319
+ )
320
+ foregroundRecent.push({ agent: args.agent, line, at: Date.now() })
321
+ if (foregroundRecent.length > 20) foregroundRecent.splice(0, foregroundRecent.length - 20)
322
+ return { id, agent: args.agent, state: ok ? 'reconciled' : 'error', line }
323
+ }
324
+
325
+ return {
326
+ async launch(args: LaunchArgs): Promise<ManagerReceipt> {
327
+ guardEnabled()
328
+ // R4 step budgets (routing.yml max_steps, every agent): capped agents
329
+ // skip dispatch with a stop summary — no session, no board job.
330
+ let prompt = args.prompt
331
+ if (stepCap !== undefined) {
332
+ const maxSteps = stepCap.maxStepsFor(args.agent)
333
+ if (stepCap.isCapped(args.agent)) {
334
+ const line = cappedSummary(args.agent, maxSteps ?? DEFAULT_MAX_STEPS).slice(0, LINE_MAX)
335
+ return { id: `cap-${Date.now()}`, agent: args.agent, state: 'reconciled', line }
336
+ }
337
+ const rec = stepCap.recordStep(args.agent)
338
+ if (rec.capped && rec.maxSteps !== undefined) {
339
+ prompt = `${args.prompt}${buildStopInstruction(args.agent, rec.maxSteps)}`
340
+ }
341
+ }
342
+ // Synchronous routing decision: background when a slot is free,
343
+ // foreground fallback when full, clean rejection otherwise.
344
+ const free =
345
+ board.canDispatch(args.agent) && pending(args.agent) < board.maxConcurrentPerAgent
346
+ if (free) return launchBackground(args, prompt)
347
+ if (foregroundFallback) return launchForeground(args, prompt)
348
+ throw new Error(
349
+ `pantheon_delegate rejected: concurrency limit reached for agent "${args.agent}"`,
350
+ )
351
+ },
352
+
353
+ async read(idOrAlias: string): Promise<string> {
354
+ guardEnabled()
355
+ const job = board.resolve(parentSessionID, idOrAlias) ?? board.get(idOrAlias)
356
+ if (!job) throw new Error(`delegation not found: ${idOrAlias}`)
357
+ const terminal = await withTimeout(
358
+ board.waitForTerminal(job.taskID, readTimeoutMs),
359
+ readTimeoutMs + 5_000,
360
+ `read(${idOrAlias})`,
361
+ )
362
+ if (terminal.state !== 'reconciled') await board.markReconciled(terminal.taskID)
363
+ await board.deleteSignal(terminal.alias)
364
+ if (terminal.state === 'error' && !terminal.resultSummary) {
365
+ return `# Delegation Report — ${terminal.alias}\n\nERROR: ${terminal.lastStatusError ?? 'failed'}\n`
366
+ }
367
+ return (
368
+ `# Delegation Report — ${terminal.alias}\n\n` +
369
+ `${terminal.resultSummary ?? terminal.lastStatusError ?? terminal.state}\n`
370
+ )
371
+ },
372
+
373
+ async list(): Promise<string[]> {
374
+ guardEnabled()
375
+ const lines = board.list(parentSessionID).map((job) => boardLine(job))
376
+ for (const fg of foregroundRecent) lines.push(fg.line)
377
+ return lines
378
+ },
379
+
380
+ async recover(): Promise<void> {
381
+ await board.recoverRunningJobs()
382
+ },
383
+ }
384
+ }