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

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 (56) hide show
  1. package/AGENTS.md +14 -63
  2. package/CHANGELOG.md +65 -0
  3. package/README.md +1 -2
  4. package/README.pt-BR.md +1 -1
  5. package/commands/pantheon.md +1 -35
  6. package/package.json +1 -1
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +2 -5
  9. package/src/agents/zeus.md +4 -26
  10. package/src/instructions/memory-protocol.instructions.md +6 -3
  11. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  12. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  13. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  14. package/src/mcp/toon_codec.py +2 -4
  15. package/src/pantheon/background-job-board.ts +4 -4
  16. package/src/pantheon/delegate-manager.ts +95 -10
  17. package/src/pantheon/delegation-classifier.ts +3 -4
  18. package/src/pantheon/delegation-finalize.ts +69 -7
  19. package/src/pantheon/delegation.ts +16 -4
  20. package/src/pantheon/presets.d.mts +0 -20
  21. package/src/pantheon/presets.mjs +0 -64
  22. package/src/plugin-v2.ts +4 -6
  23. package/src/plugin.ts +6 -6
  24. package/src/plugins/pantheon-hooks.ts +1 -1
  25. package/src/plugins/tui/dist/tui.d.ts +43 -36
  26. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  27. package/src/plugins/tui/dist/tui.js +102 -38
  28. package/src/plugins/tui/package-lock.json +2 -2
  29. package/src/plugins/tui/package.json +1 -1
  30. package/src/plugins/tui/src/index.tsx +168 -67
  31. package/src/routing.yml +0 -16
  32. package/scripts/dev-tui.sh +0 -48
  33. package/scripts/generate-preset-docs.mjs +0 -300
  34. package/scripts/generate-prompts.sh +0 -222
  35. package/scripts/hash_verify.py +0 -192
  36. package/scripts/install/component.mjs +0 -24
  37. package/scripts/install/migrate-config.mjs +0 -302
  38. package/scripts/test-context-compression.sh +0 -169
  39. package/src/pantheon/auto-wake.ts +0 -85
  40. package/src/pantheon/chat-reminders.ts +0 -9
  41. package/src/pantheon/comment-checker.ts +0 -48
  42. package/src/pantheon/compaction-assert.ts +0 -160
  43. package/src/pantheon/compaction-handoff.ts +0 -304
  44. package/src/pantheon/cost-tracker.ts +0 -108
  45. package/src/pantheon/dispatch-guard.ts +0 -109
  46. package/src/pantheon/permission-eval.ts +0 -284
  47. package/src/pantheon/preemptive-compact.ts +0 -76
  48. package/src/pantheon/provider-capabilities.ts +0 -221
  49. package/src/pantheon/provider-stream-tracker.ts +0 -182
  50. package/src/pantheon/retry-policy.ts +0 -212
  51. package/src/pantheon/token-opt.ts +0 -689
  52. package/src/pantheon/toon-codec.ts +0 -481
  53. package/src/pantheon/v2-events.ts +0 -148
  54. package/src/pantheon/v2-hooks.ts +0 -287
  55. package/src/pantheon/v2-tool-definitions.ts +0 -331
  56. package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
@@ -23,13 +23,16 @@
23
23
  * @module delegate-manager
24
24
  */
25
25
 
26
+ import { join } from 'node:path'
26
27
  import { z } from 'zod'
27
28
  import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
28
29
  import type { DelegationToolset } from './delegation.ts'
29
30
  import {
31
+ capReportOutput,
30
32
  DELEGATION_DEFAULTS,
31
33
  type DelegationClient,
32
34
  readDelegationReport,
35
+ resolveReadMaxChars,
33
36
  } from './delegation-finalize.ts'
34
37
  import { createNativeProbe, type NativeProbeFn, type ProbeClient } from './native-probe.ts'
35
38
  import { classifyNativeResult, type NativeTaskStatus } from './native-task-status.ts'
@@ -66,10 +69,14 @@ export type NativeTaskFn = (input: NativeTaskInput) => Promise<NativeTaskResult>
66
69
  /** Minimal launch receipt: id, agent, state, one short line. */
67
70
  export interface ManagerReceipt {
68
71
  id: string
72
+ /** Board alias of the job (e.g. `apo-1`) — report files are keyed by it. */
73
+ alias: string
69
74
  agent: string
70
75
  state: 'reconciled' | 'error'
71
76
  status: NativeTaskStatus
72
77
  line: string
78
+ /** Verified child output (when the task completed with content). */
79
+ output?: string
73
80
  }
74
81
 
75
82
  export interface LaunchArgs {
@@ -109,6 +116,15 @@ export interface DelegateManagerOptions {
109
116
  nativeProbe?: NativeProbeFn
110
117
  /** V1 client used by the lazy capability probe. */
111
118
  probeClient?: ProbeClient
119
+ /** Base directory for delegation reports (default `.pantheon/delegations`). */
120
+ outputDir?: string
121
+ /**
122
+ * Inline cap (chars) for report content returned to the caller. Oversized
123
+ * reports keep head+tail with an explicit `[TRUNCATED …]` marker pointing
124
+ * at the full report file. Default: PANTHEON_DELEGATION_READ_MAX_CHARS
125
+ * or 50_000.
126
+ */
127
+ readMaxChars?: number
112
128
  }
113
129
 
114
130
  export interface DelegateManager {
@@ -139,7 +155,9 @@ export function resolveDelegateMode(
139
155
 
140
156
  const LINE_MAX = 160
141
157
 
142
- /** Remove terminal escapes and control characters from untrusted child output. */
158
+ /** Remove terminal escapes and control characters from untrusted child output.
159
+ * UTF-8 text (accents, emoji) is PRESERVED — only ANSI sequences, C0/C1
160
+ * controls and DEL are stripped; receipt/report lines stay human-readable. */
143
161
  export function sanitizeReceiptText(text: string): string {
144
162
  let clean = ''
145
163
  let skippingAnsi = false
@@ -154,7 +172,8 @@ export function sanitizeReceiptText(text: string): string {
154
172
  skippingAnsi = true
155
173
  continue
156
174
  }
157
- if (char === '\n' || char === '\t' || (code >= 0x20 && code <= 0x7e)) clean += char
175
+ const isControl = code < 0x20 || (code >= 0x7f && code <= 0x9f)
176
+ if (!isControl || char === '\n' || char === '\t') clean += char
158
177
  }
159
178
  return clean
160
179
  }
@@ -219,7 +238,13 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
219
238
  stepCap,
220
239
  nativeProbe,
221
240
  probeClient,
241
+ outputDir = DELEGATION_DEFAULTS.outputDir,
242
+ readMaxChars,
222
243
  } = options
244
+ const inlineMaxChars = readMaxChars ?? resolveReadMaxChars(env)
245
+
246
+ /** Report file path for a job alias (the durable full report). */
247
+ const reportPathFor = (alias: string): string => join(outputDir, parentSessionID, `${alias}.md`)
223
248
 
224
249
  // Strict native mode: foregroundFallback and models are overridden.
225
250
  // The user-facing options are ignored when PANTHEON_DELEGATE_MODE=native.
@@ -310,7 +335,14 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
310
335
  LINE_MAX,
311
336
  )
312
337
  : boardLine(settled ?? job)
313
- return { id: taskID, agent: job.agent, state: failed ? 'error' : 'reconciled', status, line }
338
+ return {
339
+ id: taskID,
340
+ alias,
341
+ agent: job.agent,
342
+ state: failed ? 'error' : 'reconciled',
343
+ status,
344
+ line,
345
+ }
314
346
  }
315
347
 
316
348
  /** Apply a terminal transition best-effort: a racing hook may have gotten there first. */
@@ -354,7 +386,11 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
354
386
  await applyTerminal(taskID, 'completed', {
355
387
  resultSummary: firstLine(outcome.result.content, 500),
356
388
  })
357
- return await settle(taskID, record.alias)
389
+ const settled = await settle(taskID, record.alias)
390
+ // The task() call already blocked until completion — return the verified
391
+ // output in the receipt (the parent reads the full report via the alias
392
+ // when needed). The tool layer caps it inline.
393
+ return { ...settled, output: outcome.result.content }
358
394
  } finally {
359
395
  pendingByAgent.set(key, Math.max(0, pending(args.agent) - 1))
360
396
  }
@@ -403,7 +439,17 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
403
439
  },
404
440
  ok ? undefined : (outcome as { ok: false; error?: string }).error,
405
441
  )
406
- return { id, agent: args.agent, state: ok ? 'reconciled' : 'error', status, line }
442
+ return {
443
+ id,
444
+ alias: id,
445
+ agent: args.agent,
446
+ state: ok ? 'reconciled' : 'error',
447
+ status,
448
+ line,
449
+ ...(ok
450
+ ? { output: (outcome as { ok: true; result: NativeTaskResult }).result.content ?? '' }
451
+ : {}),
452
+ }
407
453
  }
408
454
 
409
455
  return {
@@ -416,8 +462,10 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
416
462
  const maxSteps = stepCap.maxStepsFor(args.agent)
417
463
  if (stepCap.isCapped(args.agent)) {
418
464
  const line = cappedSummary(args.agent, maxSteps ?? DEFAULT_MAX_STEPS).slice(0, LINE_MAX)
465
+ const cappedId = `cap-${Date.now()}`
419
466
  return {
420
- id: `cap-${Date.now()}`,
467
+ id: cappedId,
468
+ alias: cappedId,
421
469
  agent: args.agent,
422
470
  state: 'reconciled',
423
471
  status: 'ESCALATE',
@@ -432,8 +480,10 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
432
480
  if (isNativeMode) {
433
481
  const unavailableStatus = await ensureNativeAvailable()
434
482
  if (unavailableStatus !== undefined) {
483
+ const probeId = `probe-${parentSessionID}`
435
484
  return {
436
- id: `probe-${parentSessionID}`,
485
+ id: probeId,
486
+ alias: probeId,
437
487
  agent: args.agent,
438
488
  state: 'error',
439
489
  status: unavailableStatus,
@@ -464,6 +514,13 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
464
514
  )
465
515
  if (terminal.state !== 'reconciled') await board.markReconciled(terminal.taskID)
466
516
  await board.deleteSignal(terminal.alias)
517
+ // Full report first (same contract as the legacy engine): the md file
518
+ // written by finalizeDelegation carries the complete child output. The
519
+ // one-line resultSummary is only a fallback when no report was written.
520
+ const md = await readDelegationReport(outputDir, terminal)
521
+ if (md !== undefined) {
522
+ return capReportOutput(md, inlineMaxChars, reportPathFor(terminal.alias))
523
+ }
467
524
  if (terminal.state === 'error' && !terminal.resultSummary) {
468
525
  return `# Delegation Report — ${terminal.alias}\n\nERROR: ${sanitizeReceiptText(terminal.lastStatusError ?? 'failed')}\n`
469
526
  }
@@ -640,6 +697,7 @@ export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeD
640
697
  ? {}
641
698
  : { promptTimeoutMs: managerOptions.timeoutMs }),
642
699
  })
700
+ const inlineMaxChars = managerOptions.readMaxChars ?? resolveReadMaxChars(env)
643
701
  const managers = new Map<string, DelegateManager>()
644
702
  const managerFor = (parentSessionID: string): DelegateManager => {
645
703
  const existing = managers.get(parentSessionID)
@@ -652,6 +710,7 @@ export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeD
652
710
  parentSessionID,
653
711
  env,
654
712
  ...(stepCap === undefined ? {} : { stepCap }),
713
+ ...(wiring.outputDir === undefined ? {} : { outputDir: wiring.outputDir }),
655
714
  nativeProbe,
656
715
  probeClient: client,
657
716
  })
@@ -702,7 +761,21 @@ export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeD
702
761
  ...(args.model === undefined ? {} : { model: args.model }),
703
762
  taskID: childID,
704
763
  })
705
- return `status: ${receipt.status}\n${receipt.line}`
764
+ // The launch blocked until the child finished — return the verified
765
+ // output inline (capped head+tail with a pointer to the full report),
766
+ // not just a status line. Foreground receipts carry no report file,
767
+ // so the marker points at pantheon_delegation_read instead.
768
+ const header = `status: ${receipt.status}\n${receipt.line}`
769
+ if (receipt.output === undefined || receipt.output.trim() === '') return header
770
+ const reportPath =
771
+ receipt.alias === receipt.id
772
+ ? undefined
773
+ : join(
774
+ wiring.outputDir ?? DELEGATION_DEFAULTS.outputDir,
775
+ ctx.sessionID,
776
+ `${receipt.alias}.md`,
777
+ )
778
+ return `${header}\n\n${capReportOutput(receipt.output, inlineMaxChars, reportPath)}`
706
779
  },
707
780
  },
708
781
  pantheon_delegation_read: {
@@ -739,6 +812,10 @@ export interface PluginNativeDelegationWiring {
739
812
  agentModels: Record<string, string | undefined>
740
813
  stepCap: StepCapTracker
741
814
  wallClockTimeoutMs?: number
815
+ /** Base directory for delegation reports (default `.pantheon/delegations`). */
816
+ outputDir?: string
817
+ /** Inline cap (chars) for delegation report content (default env or 50_000). */
818
+ readMaxChars?: number
742
819
  finalizeDelegation: DelegationToolset['finalizeDelegation']
743
820
  }
744
821
  export type PluginNativeDelegation = NativeDelegateToolset & {
@@ -751,15 +828,23 @@ export function buildPluginNativeDelegation(
751
828
  ...createNativeDelegateTools({
752
829
  board: wiring.board,
753
830
  client: wiring.client,
831
+ ...(wiring.outputDir === undefined ? {} : { outputDir: wiring.outputDir }),
754
832
  isRootSession: wiring.isRootSession,
755
833
  registerChildSession: wiring.registerChildSession,
756
834
  registerReadOnlySession: wiring.registerReadOnlySession,
757
835
  isReadOnlyAgent: (agent) => wiring.readOnlyAgents.has(agent.toLowerCase()),
758
836
  agentModel: (agent) => wiring.agentModels[agent.toLowerCase()],
759
837
  stepCap: wiring.stepCap,
760
- ...(wiring.wallClockTimeoutMs === undefined
838
+ ...(wiring.wallClockTimeoutMs === undefined && wiring.readMaxChars === undefined
761
839
  ? {}
762
- : { managerOptions: { timeoutMs: wiring.wallClockTimeoutMs } }),
840
+ : {
841
+ managerOptions: {
842
+ ...(wiring.wallClockTimeoutMs === undefined
843
+ ? {}
844
+ : { timeoutMs: wiring.wallClockTimeoutMs }),
845
+ ...(wiring.readMaxChars === undefined ? {} : { readMaxChars: wiring.readMaxChars }),
846
+ },
847
+ }),
763
848
  }),
764
849
  finalizeDelegation: wiring.finalizeDelegation,
765
850
  }
@@ -8,8 +8,7 @@
8
8
  * - `timeout` (timeout-with-partial / timeout-empty / timeout-exhausted)
9
9
  * - `error` — provider or infrastructure failure
10
10
  *
11
- * Integrates with the existing `dispatch-guard.ts` classification for
12
- * empty-response detection and provides a structured `DelegationResult`
11
+ * Provides empty-response detection and a structured `DelegationResult`
13
12
  * that replaces raw-text returns.
14
13
  *
15
14
  * @module delegation-classifier
@@ -17,7 +16,7 @@
17
16
 
18
17
  // ─── Types ─────────────────────────────────────────────────────────────
19
18
 
20
- /** Empty-response mode classification (matches dispatch-guard). */
19
+ /** Empty-response mode classification (empty-mode1 / empty-mode2). */
21
20
  export type EmptyMode = 'empty-mode1' | 'empty-mode2'
22
21
 
23
22
  /** Timeout sub-type based on partial output and retry history. */
@@ -82,7 +81,7 @@ const NO_OUTPUT_RE = /_No output captured\._/i
82
81
 
83
82
  /**
84
83
  * Classify whether a response is empty, empty-with-tokens, or has content.
85
- * Matches the dispatch-guard classification (empty-mode1, empty-mode2).
84
+ * Classifies empty-mode1 (no text/tokens) vs empty-mode2 (tokens, no text).
86
85
  *
87
86
  * @param text The response text (may be null/undefined).
88
87
  * @param tokens Optional token counts for mode2 detection.
@@ -157,6 +157,13 @@ export interface DelegationOptions {
157
157
  /** Injectable clock/sleeper for deterministic watchdog tests. */
158
158
  now?: () => number
159
159
  sleep?: (ms: number) => Promise<void>
160
+ /**
161
+ * Inline cap (chars) for report content returned by pantheon_delegation_read.
162
+ * Oversized reports keep head+tail with an explicit `[TRUNCATED …]` marker
163
+ * pointing at the full report file. Default: PANTHEON_DELEGATION_READ_MAX_CHARS
164
+ * or 50_000.
165
+ */
166
+ readMaxChars?: number
160
167
  /**
161
168
  * Kill-switch: when false, all delegation tools (delegate/read/list) throw
162
169
  * immediately. Mirrors `PANTHEON_DELEGATION=off` from the native manager.
@@ -276,6 +283,62 @@ function summarizeOutput(output: string): string {
276
283
  return firstLine.length > 120 ? `${firstLine.slice(0, 120)}…` : firstLine
277
284
  }
278
285
 
286
+ // ─── Report inline cap ─────────────────────────────────────────────────
287
+ //
288
+ // Delegation reports are returned INLINE to the orchestrator (tool result).
289
+ // A very large child output must be capped BEFORE it floods the parent
290
+ // context — but a hard mid-content cut loses the report header AND the
291
+ // conclusion. The cap keeps head + tail with an explicit marker pointing at
292
+ // the full report (the durable `.pantheon/delegations/<parent>/<alias>.md`
293
+ // file or the read tool), mirroring how comparable agent hosts surface
294
+ // oversized tool output.
295
+
296
+ /** Default inline cap for delegation report content (chars). */
297
+ export const DELEGATION_READ_MAX_CHARS = 50_000
298
+ /** Lower bound for PANTHEON_DELEGATION_READ_MAX_CHARS. */
299
+ export const DELEGATION_READ_MAX_CHARS_MIN = 1_000
300
+
301
+ /** Resolve the inline report cap from the environment (default 50k, min 1k). */
302
+ export function resolveReadMaxChars(env: Record<string, string | undefined> = process.env): number {
303
+ const raw = env.PANTHEON_DELEGATION_READ_MAX_CHARS?.trim()
304
+ if (raw === undefined || raw === '') return DELEGATION_READ_MAX_CHARS
305
+ const value = Number(raw)
306
+ return Number.isSafeInteger(value) && value >= DELEGATION_READ_MAX_CHARS_MIN
307
+ ? value
308
+ : DELEGATION_READ_MAX_CHARS
309
+ }
310
+
311
+ /**
312
+ * Cap report markdown for inline return: ≤ `maxChars` passes through
313
+ * untouched; anything larger keeps the head (60% of the budget) + tail (25%)
314
+ * with an explicit `[TRUNCATED …]` marker between them. `reportPath` names
315
+ * the durable full-report file in the marker; without it the marker points
316
+ * at `pantheon_delegation_read`. Pure — no I/O.
317
+ */
318
+ export function capReportOutput(md: string, maxChars: number, reportPath?: string): string {
319
+ if (md.length <= maxChars) return md
320
+ if (maxChars < 200) {
321
+ // Budget too small for a head+tail split — keep what fits after the marker.
322
+ const marker = capReportMarker(md.length, 0, reportPath)
323
+ return `${md.slice(0, Math.max(0, maxChars - marker.length - 1))}\n${marker}`
324
+ }
325
+ const headLen = Math.floor(maxChars * 0.6)
326
+ const tailLen = Math.floor(maxChars * 0.25)
327
+ return `${md.slice(0, headLen)}\n${capReportMarker(md.length, headLen + tailLen, reportPath)}\n${md.slice(md.length - tailLen)}`
328
+ }
329
+
330
+ function capReportMarker(
331
+ totalChars: number,
332
+ keptChars: number,
333
+ reportPath: string | undefined,
334
+ ): string {
335
+ const where =
336
+ reportPath !== undefined
337
+ ? `full report: ${reportPath}`
338
+ : 'read the full report with pantheon_delegation_read'
339
+ return `[TRUNCATED: ${totalChars - keptChars} of ${totalChars} chars hidden — ${where}]`
340
+ }
341
+
279
342
  // ─── Report persistence ────────────────────────────────────────────────
280
343
 
281
344
  /**
@@ -387,13 +450,12 @@ export async function finalizeDelegation(
387
450
  // report above still captures this finalize; the board state stands.
388
451
  }
389
452
 
390
- // P1-1: Delete the auto-wake signal file to prevent leaks. The board
391
- // writes .signal.json on every terminal transition, but the legacy path
392
- // never called deleteSignal — only the native delegate-manager did.
393
- // The consumer (auto-wake.ts consumeWakeSignals) is intentionally NOT
394
- // wired in the event hook: finalizeDelegation already handles the
395
- // terminal transition, so consuming signals would be redundant and risks
396
- // double-processing. deleteSignal on finalize is sufficient.
453
+ // P1-1: Delete the wake signal file to prevent leaks. The board writes
454
+ // .signal.json on every terminal transition, but the legacy path never
455
+ // called deleteSignal — only the native delegate-manager did.
456
+ // finalizeDelegation already handles the terminal transition, so
457
+ // consuming signals would be redundant; deleteSignal on finalize is
458
+ // sufficient.
397
459
  await deps.board.deleteSignal(job.alias)
398
460
 
399
461
  // P1-4: Prune old completed jobs and enforce the hard entry cap. The
@@ -35,6 +35,7 @@
35
35
  * @module delegation
36
36
  */
37
37
 
38
+ import { join } from 'node:path'
38
39
  import { z } from 'zod'
39
40
 
40
41
  import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
@@ -49,6 +50,7 @@ import {
49
50
  readOnlyRegistry,
50
51
  } from './delegation-enforce.ts'
51
52
  import {
53
+ capReportOutput,
52
54
  DELEGATION_DEFAULTS,
53
55
  type DelegationClient,
54
56
  type DelegationDeps,
@@ -57,6 +59,7 @@ import {
57
59
  type FinalizeInput,
58
60
  finalizeDelegation as finalizeDelegationReport,
59
61
  readDelegationReport,
62
+ resolveReadMaxChars,
60
63
  } from './delegation-finalize.ts'
61
64
  import { finalizeIdleChildrenWithoutMd } from './delegation-notify.ts'
62
65
  import { createPantheonLogger } from './logger.ts'
@@ -814,6 +817,10 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
814
817
 
815
818
  const knownChildren = new Set<string>()
816
819
  const timers = new Map<string, NodeJS.Timeout>()
820
+ // Inline cap for report content returned by pantheon_delegation_read —
821
+ // oversized reports keep head+tail with an explicit marker pointing at the
822
+ // full report file (never a silent mid-content cut).
823
+ const inlineMaxChars = options.readMaxChars ?? resolveReadMaxChars()
817
824
  // Fase B3: per-parent-session delegation budget tracking, scoped to this
818
825
  // factory/process instance. Keyed by `${parentSessionID}:${parentAgent}->${targetAgent}`;
819
826
  // it is not restart-persistent because the factory has no reliable store.
@@ -1276,6 +1283,11 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
1276
1283
  if (md === undefined) {
1277
1284
  return `Delegation [${terminal.alias}] reached state ${terminal.state} but no report file was found.`
1278
1285
  }
1286
+ // Classify on the FULL report (markers may sit at either end), but
1287
+ // return the inline-capped markdown so a huge child output cannot
1288
+ // flood the parent context — the marker points at the full file.
1289
+ const reportPath = join(outputDir, terminal.parentSessionID, `${terminal.alias}.md`)
1290
+ const cappedMd = capReportOutput(md, inlineMaxChars, reportPath)
1279
1291
 
1280
1292
  // Classify the result for stuck-agent detection
1281
1293
  const classification = classifyStuckAgent(md)
@@ -1284,15 +1296,15 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
1284
1296
 
1285
1297
  if (classification.status === 'success') {
1286
1298
  await board.markReconciled(job.taskID)
1287
- // Backward compatible: success reports returned as-is
1288
- if (!hasActivity) return md
1289
- return `${md.replace(/\n+$/, '')}${activitySuffix}`
1299
+ // Backward compatible: success reports returned as-is (capped)
1300
+ if (!hasActivity) return cappedMd
1301
+ return `${cappedMd.replace(/\n+$/, '')}${activitySuffix}`
1290
1302
  }
1291
1303
 
1292
1304
  // Non-success: build structured DelegationResult and format
1293
1305
  const delegationResult: DelegationResult = {
1294
1306
  status: classification.status === 'empty' ? 'empty' : classification.status,
1295
- content: md,
1307
+ content: cappedMd,
1296
1308
  retryCount: 0,
1297
1309
  partialResult: classification.partialResult,
1298
1310
  recommendation: classification.recommendation,
@@ -104,26 +104,6 @@ export interface RoutingRetryPolicyOptions {
104
104
  logger?: { warn?: (msg: string) => void }
105
105
  }
106
106
 
107
- /**
108
- * R1: per-error-type retry policy from routing.yml `retry_policy`
109
- * (auth/rate_limit/timeout/other → max retries). Fail-open → null.
110
- */
111
- export declare function loadRoutingRetryPolicy(
112
- options?: RoutingRetryPolicyOptions,
113
- ): Record<string, number> | null
114
-
115
- export interface RoutingCooldownResult {
116
- allowed_fails: number
117
- cooldown_time_seconds: number
118
- }
119
-
120
- /**
121
- * R1: provider cooldown config from routing.yml `cooldown`. Fail-open → null.
122
- */
123
- export declare function loadRoutingCooldown(
124
- options?: RoutingRetryPolicyOptions,
125
- ): RoutingCooldownResult | null
126
-
127
107
  /**
128
108
  * R4: per-agent step caps from routing.yml `agents.<name>.max_steps`.
129
109
  * Fail-open → {} (no agent capped).
@@ -213,70 +213,6 @@ function loadRoutingYaml(routingPath) {
213
213
  return routing && typeof routing === 'object' ? routing : null
214
214
  }
215
215
 
216
- const RETRY_ERROR_TYPES = new Set(['auth', 'rate_limit', 'timeout', 'other'])
217
-
218
- /**
219
- * Load the R1 per-error-type retry policy from routing.yml (`retry_policy:`).
220
- * Fail-open: missing/unparseable config yields null (the caller falls back
221
- * to DEFAULT_RETRY_POLICY) and warns via the optional logger.
222
- *
223
- * @param {object} [opts]
224
- * @param {string} [opts.routingPath]
225
- * @param {{warn?: Function}} [opts.logger]
226
- * @returns {Record<string, number>|null} error-type → max retries
227
- */
228
- export function loadRoutingRetryPolicy({ routingPath, logger = console } = {}) {
229
- try {
230
- const routing = loadRoutingYaml(routingPath)
231
- const policy = routing?.retry_policy
232
- if (!policy || typeof policy !== 'object' || Array.isArray(policy)) return null
233
- const out = {}
234
- for (const [type, max] of Object.entries(policy)) {
235
- if (RETRY_ERROR_TYPES.has(type) && Number.isSafeInteger(max) && max >= 0) {
236
- out[type] = max
237
- }
238
- }
239
- return Object.keys(out).length > 0 ? out : null
240
- } catch (err) {
241
- const reason = err instanceof Error ? err.message : String(err)
242
- logger.warn?.(`presets: routing.yml retry_policy unavailable (${reason})`)
243
- return null
244
- }
245
- }
246
-
247
- /**
248
- * Load the R1 provider cooldown config from routing.yml (`cooldown:`).
249
- * Fail-open: missing/unparseable config yields null (caller falls back to
250
- * DEFAULT_COOLDOWN).
251
- *
252
- * @param {object} [opts]
253
- * @param {string} [opts.routingPath]
254
- * @param {{warn?: Function}} [opts.logger]
255
- * @returns {{allowed_fails: number, cooldown_time_seconds: number}|null}
256
- */
257
- export function loadRoutingCooldown({ routingPath, logger = console } = {}) {
258
- try {
259
- const routing = loadRoutingYaml(routingPath)
260
- const cooldown = routing?.cooldown
261
- if (!cooldown || typeof cooldown !== 'object' || Array.isArray(cooldown)) return null
262
- const allowedFails = cooldown.allowed_fails
263
- const cooldownTimeSeconds = cooldown.cooldown_time_seconds
264
- if (
265
- !Number.isSafeInteger(allowedFails) ||
266
- allowedFails < 1 ||
267
- !Number.isSafeInteger(cooldownTimeSeconds) ||
268
- cooldownTimeSeconds < 1
269
- ) {
270
- return null
271
- }
272
- return { allowed_fails: allowedFails, cooldown_time_seconds: cooldownTimeSeconds }
273
- } catch (err) {
274
- const reason = err instanceof Error ? err.message : String(err)
275
- logger.warn?.(`presets: routing.yml cooldown unavailable (${reason})`)
276
- return null
277
- }
278
- }
279
-
280
216
  /**
281
217
  * Load the R4 per-agent step caps from routing.yml
282
218
  * (`agents.<name>.max_steps`). Fail-open: missing config yields {}.
package/src/plugin-v2.ts CHANGED
@@ -681,12 +681,10 @@ function createV2ToolDefinitionsFromContext(_context: PluginContext): V2ToolDef[
681
681
  name,
682
682
  description: toolDescriptions[name] ?? `Pantheon tool: ${name}`,
683
683
  input: { type: 'object' as const, properties: {} },
684
- // FOLLOW-UP: connect the real wrappers from
685
- // src/pantheon/v2-tool-definitions.ts (createV2ToolDefinitions with
686
- // costCommand/goalTools) once V1 infrastructure is resolvable in the V2
687
- // standalone context. The before/after factories in v2-hooks.ts:216-282
688
- // are likewise unwired. Until then, placeholders resolve to the object
689
- // shape so the beta host never sees a bare string.
684
+ // FOLLOW-UP: wire the real tool wrappers (costCommand/goalTools) once V1
685
+ // infrastructure is resolvable in the V2 standalone context. Until then,
686
+ // placeholders resolve to the object shape so the beta host never sees a
687
+ // bare string.
690
688
  execute: async (_input: Record<string, unknown>, _context: unknown): Promise<V2ToolResult> => {
691
689
  return { output: `${name}: ${v2UnavailableMessage}` }
692
690
  },
package/src/plugin.ts CHANGED
@@ -66,10 +66,10 @@ import { activePresetCandidates, createVisionHandler } from './pantheon/vision.t
66
66
  const log = createPantheonLogger({ module: 'pantheon-plugin' })
67
67
 
68
68
  // R4: per-agent step caps from routing.yml `agents.<name>.max_steps`
69
- // (fail-open → {} = no agent capped). R1 retry_policy/cooldown are NOT
70
- // wired here — the plugin has no retry path (opencode cannot intercept task
71
- // completion); zeus waves load them via presets.mjs loaders and pass them
72
- // to zeusDelegateWithRetry explicitly.
69
+ // (fail-open → {} = no agent capped). The R1 retry_policy/cooldown block and
70
+ // its presets.mjs loaders were removed together with their only consumer
71
+ // (zeus-delegate-with-retry.ts); the plugin has no retry path (opencode cannot
72
+ // intercept task completion).
73
73
  const stepCapTracker = new StepCapTracker(loadRoutingMaxSteps({ logger: log }))
74
74
 
75
75
  // O5: permission.task glob rules from routing.yml `permission.task`
@@ -517,8 +517,8 @@ const plugin: Plugin = async (input: PluginInput) => {
517
517
 
518
518
  // Wave 4 (PR #46): /cost — delegation cost + token visibility. Reads
519
519
  // opencode.db read-only via the single node:sqlite backend.
520
- // Fully wired (unlike dispatch-guard, which is manual-orchestration-only
521
- // because opencode 1.18.x cannot intercept task completion via hooks).
520
+ // Fully wired. (opencode 1.18.x cannot intercept task completion via hooks,
521
+ // so empty-result handling lives in delegation-classifier.ts.)
522
522
  const costCommand = createCostCommand()
523
523
  const modelCommand = createModelCommand()
524
524
 
@@ -783,7 +783,7 @@ function delegationAgent(tool: string, args?: unknown): string {
783
783
  * panel tracks the child end-to-end. Fire-and-forget and silent: a mirror
784
784
  * failure must never surface into the tool call or the transcript.
785
785
  * registerLaunchIfAbsent keeps it idempotent against any other dispatcher
786
- * (v2-events, plugin double-load, repeated after-hook firings).
786
+ * (plugin double-load, repeated after-hook firings).
787
787
  */
788
788
  function mirrorNativeBackgroundDispatch(
789
789
  input: { tool: string; sessionID: string },