pantheon-opencode 1.3.5 → 1.3.6

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 (68) hide show
  1. package/README.md +33 -1
  2. package/package.json +3 -3
  3. package/scripts/_pantheon_paths.py +0 -1
  4. package/scripts/build-agents-md.mjs +4 -2
  5. package/scripts/code_mode_server.py +307 -21
  6. package/scripts/doctor.mjs +26 -13
  7. package/scripts/hooks/format-multi-language.sh +6 -3
  8. package/scripts/hooks/log-session-start.sh +6 -0
  9. package/scripts/hooks/on-subagent-delegation-start.sh +22 -6
  10. package/scripts/hooks/validate-post-conditions.sh +1 -1
  11. package/scripts/hooks/validate-tool-safety.sh +5 -2
  12. package/scripts/install/agents-md.mjs +3 -1
  13. package/scripts/install/cli-ui.mjs +24 -4
  14. package/scripts/install/interactive.mjs +18 -7
  15. package/scripts/install/migrate-config.mjs +13 -19
  16. package/scripts/install/migrate.mjs +8 -5
  17. package/scripts/install/model-picker.mjs +4 -1
  18. package/scripts/install/opencode.mjs +2 -0
  19. package/scripts/install/shared.mjs +6 -4
  20. package/scripts/install/state.mjs +9 -6
  21. package/scripts/mcp_persistence_server.py +23 -22
  22. package/scripts/mcp_resources_server.py +60 -1
  23. package/scripts/memory_mcp_server.py +80 -4
  24. package/scripts/postinstall.mjs +27 -25
  25. package/scripts/prune.mjs +1 -1
  26. package/scripts/redaction-gate.mjs +3 -1
  27. package/scripts/uninstall.mjs +0 -2
  28. package/scripts/validation-policy.mjs +6 -1
  29. package/scripts/version-check.mjs +9 -4
  30. package/src/agents/aphrodite.md +1 -1
  31. package/src/agents/demeter.md +1 -1
  32. package/src/agents/hephaestus.md +1 -1
  33. package/src/agents/hermes.md +1 -1
  34. package/src/agents/themis.md +4 -0
  35. package/src/mcp/code_mode_server.py +304 -18
  36. package/src/mcp/eval_store.py +233 -0
  37. package/src/mcp/mcp_persistence_server.py +23 -22
  38. package/src/mcp/mcp_resources_server.py +58 -0
  39. package/src/mcp/memory_mcp_server.py +80 -3
  40. package/src/pantheon/auto-wake.ts +2 -4
  41. package/src/pantheon/background-job-board.ts +17 -4
  42. package/src/pantheon/comment-checker.ts +48 -0
  43. package/src/pantheon/delegation-classifier.ts +307 -0
  44. package/src/pantheon/delegation-enforce.ts +21 -21
  45. package/src/pantheon/delegation-finalize.ts +44 -4
  46. package/src/pantheon/delegation.ts +403 -23
  47. package/src/pantheon/dispatch-guard.ts +2 -0
  48. package/src/pantheon/file-persistence.ts +7 -3
  49. package/src/pantheon/permission-eval.ts +284 -0
  50. package/src/pantheon/permission-globs.ts +86 -0
  51. package/src/pantheon/presets.d.mts +41 -0
  52. package/src/pantheon/presets.mjs +134 -0
  53. package/src/pantheon/retry-policy.ts +212 -0
  54. package/src/pantheon/session-guard.ts +1 -3
  55. package/src/pantheon/step-cap.ts +110 -0
  56. package/src/pantheon/vision.ts +7 -5
  57. package/src/pantheon/zeus-delegate-with-retry.ts +81 -12
  58. package/src/plugin.ts +28 -3
  59. package/src/plugins/hook-runner.ts +1 -1
  60. package/src/plugins/pantheon-hooks.ts +1 -1
  61. package/src/plugins/tui/dist/tui.d.ts +1 -1
  62. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  63. package/src/plugins/tui/dist/tui.js +19 -14
  64. package/src/plugins/tui/dist/tui.tsx +52 -18
  65. package/src/plugins/tui/package.json +1 -1
  66. package/src/plugins/tui/src/index.tsx +52 -18
  67. package/src/plugins/tui/tsdown.config.ts +1 -1
  68. package/src/routing.yml +42 -0
@@ -677,25 +677,25 @@ function parseConfig(raw: string): UsageBarConfig {
677
677
  const root = asTable(toml.parse(raw))
678
678
  const cfg = defaultConfig()
679
679
 
680
- const ui = asTable(root['ui'])
681
- cfg.showBars = bool(ui['show_bars'], cfg.showBars)
682
- cfg.showStatus = bool(ui['show_status'], cfg.showStatus)
683
- const rawWidth = ui['bar_width']
680
+ const ui = asTable(root.ui)
681
+ cfg.showBars = bool(ui.show_bars, cfg.showBars)
682
+ cfg.showStatus = bool(ui.show_status, cfg.showStatus)
683
+ const rawWidth = ui.bar_width
684
684
  if (typeof rawWidth === 'number' && Number.isFinite(rawWidth) && rawWidth >= 1)
685
685
  cfg.barWidth = Math.min(40, Math.floor(rawWidth))
686
686
 
687
687
  for (const id of ['anthropic', 'openai', 'opencodego'] as ProviderId[]) {
688
688
  const t = asTable(root[id])
689
689
  const p = cfg.providers[id]
690
- p.enabled = bool(t['enabled'], p.enabled)
691
- p.show['5h'] = bool(t['show_5h'], p.show['5h'])
692
- p.show['7d'] = bool(t['show_7d'], p.show['7d'])
693
- p.show['1m'] = bool(t['show_1m'], p.show['1m'])
694
- p.show.model = bool(t['show_model'], p.show.model)
690
+ p.enabled = bool(t.enabled, p.enabled)
691
+ p.show['5h'] = bool(t.show_5h, p.show['5h'])
692
+ p.show['7d'] = bool(t.show_7d, p.show['7d'])
693
+ p.show['1m'] = bool(t.show_1m, p.show['1m'])
694
+ p.show.model = bool(t.show_model, p.show.model)
695
695
  // Assign only when present — keeps `exactOptionalPropertyTypes` happy.
696
- const credentialsPath = str(t['credentials_path'])
696
+ const credentialsPath = str(t.credentials_path)
697
697
  if (credentialsPath !== undefined) p.credentialsPath = credentialsPath
698
- const codexAuthPath = str(t['codex_auth_path'])
698
+ const codexAuthPath = str(t.codex_auth_path)
699
699
  if (codexAuthPath !== undefined) p.codexAuthPath = codexAuthPath
700
700
  }
701
701
  return cfg
@@ -942,7 +942,7 @@ export type DelegationEntry = {
942
942
  taskID?: string
943
943
  /** Agent name, e.g. "apollo". */
944
944
  agent: string
945
- state: 'running' | 'completed' | 'error' | 'cancelled'
945
+ state: 'running' | 'completed' | 'error' | 'startup_failed' | 'startup_unknown' | 'cancelled'
946
946
  /** Epoch ms of the `Started` header. */
947
947
  startedAt: number
948
948
  /** Epoch ms of the `Finalized` header — null while still running. */
@@ -1052,6 +1052,8 @@ export function parseDelegationMarkdown(
1052
1052
  normalized !== 'running' &&
1053
1053
  normalized !== 'completed' &&
1054
1054
  normalized !== 'error' &&
1055
+ normalized !== 'startup_failed' &&
1056
+ normalized !== 'startup_unknown' &&
1055
1057
  normalized !== 'cancelled'
1056
1058
  ) {
1057
1059
  return null
@@ -1221,7 +1223,12 @@ export type DelegationActivity =
1221
1223
 
1222
1224
  export function delegationActivity(entry: DelegationEntry): DelegationActivity {
1223
1225
  if (entry.state === 'completed') return 'completed'
1224
- if (entry.state === 'error') return 'error'
1226
+ if (
1227
+ entry.state === 'error' ||
1228
+ entry.state === 'startup_failed' ||
1229
+ entry.state === 'startup_unknown'
1230
+ )
1231
+ return 'error'
1225
1232
  if (entry.state === 'cancelled') return 'cancelled'
1226
1233
  if (entry.read) return 'reading'
1227
1234
  if (entry.alias.startsWith('live-') && entry.taskID === undefined) return 'delegating'
@@ -1274,7 +1281,12 @@ export function mergeChildDelegationSources(
1274
1281
  })
1275
1282
 
1276
1283
  for (const liveEntry of live) {
1277
- if (liveEntry.alias === null && liveEntry.taskID === null && Date.now() - liveEntry.startedAt > 30_000) continue
1284
+ if (
1285
+ liveEntry.alias === null &&
1286
+ liveEntry.taskID === null &&
1287
+ Date.now() - liveEntry.startedAt > 30_000
1288
+ )
1289
+ continue
1278
1290
  const incoming = toDelegationEntry(liveEntry)
1279
1291
  const index =
1280
1292
  (incoming.taskID !== undefined ? byTask.get(incoming.taskID) : undefined) ??
@@ -1292,8 +1304,26 @@ export function mergeChildDelegationSources(
1292
1304
 
1293
1305
  const existing = result[index]
1294
1306
  if (existing === undefined) continue
1295
- const finalized = existing.source === 'md' && existing.state !== 'running'
1296
- if (finalized) continue
1307
+ // Any terminal state is authoritative — a child session that went idle
1308
+ // correctly derives 'completed' via childStatusToState before the MD
1309
+ // report is written. The live tool-part channel stays 'running' until
1310
+ // pantheon_delegation_read fires, so allowing it to overwrite a
1311
+ // terminal state would flip completed → running (the bug this fixes).
1312
+ //
1313
+ // When the existing entry IS terminal, we still absorb live metadata
1314
+ // (alias, agent, read) but preserve the terminal state.
1315
+ if (existing.state !== 'running') {
1316
+ if (liveEntry.alias !== null && existing.alias !== incoming.alias) {
1317
+ existing.alias = incoming.alias
1318
+ }
1319
+ if (incoming.agent !== 'agent' && existing.agent !== incoming.agent) {
1320
+ existing.agent = incoming.agent
1321
+ }
1322
+ if (incoming.read && !existing.read) {
1323
+ existing.read = true
1324
+ }
1325
+ continue
1326
+ }
1297
1327
  result[index] = {
1298
1328
  ...existing,
1299
1329
  alias: liveEntry.alias !== null ? incoming.alias : existing.alias,
@@ -2220,7 +2250,9 @@ function View(props: {
2220
2250
  }
2221
2251
  const withStatus = children.map((c) => ({ ...c, status: resolveStatus(c.id) }))
2222
2252
  const childEntries = childrenToDelegationEntries(withStatus, md, now())
2223
- for (const [k, v] of props.liveStore.map) if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 30_000) props.liveStore.map.delete(k)
2253
+ for (const [k, v] of props.liveStore.map)
2254
+ if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 30_000)
2255
+ props.liveStore.map.delete(k)
2224
2256
  // The event channel is optimistic: it renders a job even while the
2225
2257
  // child session/report is still being created. Filter by parent so a
2226
2258
  // different focused session never leaks rows into this sidebar.
@@ -2488,7 +2520,9 @@ const tui: TuiPlugin = (api, _options, _meta) => {
2488
2520
  order: 900,
2489
2521
  slots: {
2490
2522
  sidebar_content(_ctx, props) {
2491
- return <View api={api} sessionID={props.session_id} version={version()} liveStore={liveStore} />
2523
+ return (
2524
+ <View api={api} sessionID={props.session_id} version={version()} liveStore={liveStore} />
2525
+ )
2492
2526
  },
2493
2527
  },
2494
2528
  })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon-tui",
3
3
  "type": "module",
4
- "version": "1.3.5",
4
+ "version": "1.3.6",
5
5
  "description": "Pantheon TUI plugin — sidebar (version, preset, sessions, real-time delegations) + AI subscription usage gauges incl. OpenCode Go/Zen dollar limits (vendored, MIT)",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -677,25 +677,25 @@ function parseConfig(raw: string): UsageBarConfig {
677
677
  const root = asTable(toml.parse(raw))
678
678
  const cfg = defaultConfig()
679
679
 
680
- const ui = asTable(root['ui'])
681
- cfg.showBars = bool(ui['show_bars'], cfg.showBars)
682
- cfg.showStatus = bool(ui['show_status'], cfg.showStatus)
683
- const rawWidth = ui['bar_width']
680
+ const ui = asTable(root.ui)
681
+ cfg.showBars = bool(ui.show_bars, cfg.showBars)
682
+ cfg.showStatus = bool(ui.show_status, cfg.showStatus)
683
+ const rawWidth = ui.bar_width
684
684
  if (typeof rawWidth === 'number' && Number.isFinite(rawWidth) && rawWidth >= 1)
685
685
  cfg.barWidth = Math.min(40, Math.floor(rawWidth))
686
686
 
687
687
  for (const id of ['anthropic', 'openai', 'opencodego'] as ProviderId[]) {
688
688
  const t = asTable(root[id])
689
689
  const p = cfg.providers[id]
690
- p.enabled = bool(t['enabled'], p.enabled)
691
- p.show['5h'] = bool(t['show_5h'], p.show['5h'])
692
- p.show['7d'] = bool(t['show_7d'], p.show['7d'])
693
- p.show['1m'] = bool(t['show_1m'], p.show['1m'])
694
- p.show.model = bool(t['show_model'], p.show.model)
690
+ p.enabled = bool(t.enabled, p.enabled)
691
+ p.show['5h'] = bool(t.show_5h, p.show['5h'])
692
+ p.show['7d'] = bool(t.show_7d, p.show['7d'])
693
+ p.show['1m'] = bool(t.show_1m, p.show['1m'])
694
+ p.show.model = bool(t.show_model, p.show.model)
695
695
  // Assign only when present — keeps `exactOptionalPropertyTypes` happy.
696
- const credentialsPath = str(t['credentials_path'])
696
+ const credentialsPath = str(t.credentials_path)
697
697
  if (credentialsPath !== undefined) p.credentialsPath = credentialsPath
698
- const codexAuthPath = str(t['codex_auth_path'])
698
+ const codexAuthPath = str(t.codex_auth_path)
699
699
  if (codexAuthPath !== undefined) p.codexAuthPath = codexAuthPath
700
700
  }
701
701
  return cfg
@@ -942,7 +942,7 @@ export type DelegationEntry = {
942
942
  taskID?: string
943
943
  /** Agent name, e.g. "apollo". */
944
944
  agent: string
945
- state: 'running' | 'completed' | 'error' | 'cancelled'
945
+ state: 'running' | 'completed' | 'error' | 'startup_failed' | 'startup_unknown' | 'cancelled'
946
946
  /** Epoch ms of the `Started` header. */
947
947
  startedAt: number
948
948
  /** Epoch ms of the `Finalized` header — null while still running. */
@@ -1052,6 +1052,8 @@ export function parseDelegationMarkdown(
1052
1052
  normalized !== 'running' &&
1053
1053
  normalized !== 'completed' &&
1054
1054
  normalized !== 'error' &&
1055
+ normalized !== 'startup_failed' &&
1056
+ normalized !== 'startup_unknown' &&
1055
1057
  normalized !== 'cancelled'
1056
1058
  ) {
1057
1059
  return null
@@ -1221,7 +1223,12 @@ export type DelegationActivity =
1221
1223
 
1222
1224
  export function delegationActivity(entry: DelegationEntry): DelegationActivity {
1223
1225
  if (entry.state === 'completed') return 'completed'
1224
- if (entry.state === 'error') return 'error'
1226
+ if (
1227
+ entry.state === 'error' ||
1228
+ entry.state === 'startup_failed' ||
1229
+ entry.state === 'startup_unknown'
1230
+ )
1231
+ return 'error'
1225
1232
  if (entry.state === 'cancelled') return 'cancelled'
1226
1233
  if (entry.read) return 'reading'
1227
1234
  if (entry.alias.startsWith('live-') && entry.taskID === undefined) return 'delegating'
@@ -1274,7 +1281,12 @@ export function mergeChildDelegationSources(
1274
1281
  })
1275
1282
 
1276
1283
  for (const liveEntry of live) {
1277
- if (liveEntry.alias === null && liveEntry.taskID === null && Date.now() - liveEntry.startedAt > 30_000) continue
1284
+ if (
1285
+ liveEntry.alias === null &&
1286
+ liveEntry.taskID === null &&
1287
+ Date.now() - liveEntry.startedAt > 30_000
1288
+ )
1289
+ continue
1278
1290
  const incoming = toDelegationEntry(liveEntry)
1279
1291
  const index =
1280
1292
  (incoming.taskID !== undefined ? byTask.get(incoming.taskID) : undefined) ??
@@ -1292,8 +1304,26 @@ export function mergeChildDelegationSources(
1292
1304
 
1293
1305
  const existing = result[index]
1294
1306
  if (existing === undefined) continue
1295
- const finalized = existing.source === 'md' && existing.state !== 'running'
1296
- if (finalized) continue
1307
+ // Any terminal state is authoritative — a child session that went idle
1308
+ // correctly derives 'completed' via childStatusToState before the MD
1309
+ // report is written. The live tool-part channel stays 'running' until
1310
+ // pantheon_delegation_read fires, so allowing it to overwrite a
1311
+ // terminal state would flip completed → running (the bug this fixes).
1312
+ //
1313
+ // When the existing entry IS terminal, we still absorb live metadata
1314
+ // (alias, agent, read) but preserve the terminal state.
1315
+ if (existing.state !== 'running') {
1316
+ if (liveEntry.alias !== null && existing.alias !== incoming.alias) {
1317
+ existing.alias = incoming.alias
1318
+ }
1319
+ if (incoming.agent !== 'agent' && existing.agent !== incoming.agent) {
1320
+ existing.agent = incoming.agent
1321
+ }
1322
+ if (incoming.read && !existing.read) {
1323
+ existing.read = true
1324
+ }
1325
+ continue
1326
+ }
1297
1327
  result[index] = {
1298
1328
  ...existing,
1299
1329
  alias: liveEntry.alias !== null ? incoming.alias : existing.alias,
@@ -2220,7 +2250,9 @@ function View(props: {
2220
2250
  }
2221
2251
  const withStatus = children.map((c) => ({ ...c, status: resolveStatus(c.id) }))
2222
2252
  const childEntries = childrenToDelegationEntries(withStatus, md, now())
2223
- for (const [k, v] of props.liveStore.map) if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 30_000) props.liveStore.map.delete(k)
2253
+ for (const [k, v] of props.liveStore.map)
2254
+ if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 30_000)
2255
+ props.liveStore.map.delete(k)
2224
2256
  // The event channel is optimistic: it renders a job even while the
2225
2257
  // child session/report is still being created. Filter by parent so a
2226
2258
  // different focused session never leaks rows into this sidebar.
@@ -2488,7 +2520,9 @@ const tui: TuiPlugin = (api, _options, _meta) => {
2488
2520
  order: 900,
2489
2521
  slots: {
2490
2522
  sidebar_content(_ctx, props) {
2491
- return <View api={api} sessionID={props.session_id} version={version()} liveStore={liveStore} />
2523
+ return (
2524
+ <View api={api} sessionID={props.session_id} version={version()} liveStore={liveStore} />
2525
+ )
2492
2526
  },
2493
2527
  },
2494
2528
  })
@@ -1,5 +1,5 @@
1
- import { defineConfig } from 'tsdown'
2
1
  import solid from 'rolldown-plugin-solid'
2
+ import { defineConfig } from 'tsdown'
3
3
 
4
4
  export default defineConfig({
5
5
  entry: { tui: 'src/index.tsx', server: 'src/server.ts' },
package/src/routing.yml CHANGED
@@ -151,6 +151,7 @@ presets:
151
151
  agents:
152
152
  zeus:
153
153
  role: Central orchestrator
154
+ max_steps: 40 # R4: step budget — at max_steps the agent is forced to summarize-and-stop. Tracker is permanent-per-process (no reset wiring).
154
155
  reasoning_effort: medium
155
156
  user_invocable: true
156
157
  can_delegate: true
@@ -189,6 +190,7 @@ agents:
189
190
  - incremental-implementation
190
191
  athena:
191
192
  role: Strategic planner
193
+ max_steps: 15 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
192
194
  reasoning_effort: high
193
195
  user_invocable: true
194
196
  can_delegate: true
@@ -208,6 +210,7 @@ agents:
208
210
  - memory-bank
209
211
  apollo:
210
212
  role: Investigation scout
213
+ max_steps: 25 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
211
214
  reasoning_effort: low
212
215
  user_invocable: false
213
216
  can_delegate: false
@@ -225,6 +228,7 @@ agents:
225
228
  - auto-continue
226
229
  hermes:
227
230
  role: Backend specialist
231
+ max_steps: 60 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
228
232
  reasoning_effort: medium
229
233
  user_invocable: true
230
234
  can_delegate: true
@@ -247,6 +251,7 @@ agents:
247
251
  - incremental-implementation
248
252
  aphrodite:
249
253
  role: Frontend specialist
254
+ max_steps: 60 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
250
255
  reasoning_effort: medium
251
256
  user_invocable: true
252
257
  can_delegate: false
@@ -267,6 +272,7 @@ agents:
267
272
  - file-prompts
268
273
  demeter:
269
274
  role: Database specialist
275
+ max_steps: 50 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
270
276
  reasoning_effort: medium
271
277
  user_invocable: true
272
278
  can_delegate: false
@@ -286,6 +292,7 @@ agents:
286
292
  - incremental-implementation
287
293
  themis:
288
294
  role: Quality gate
295
+ max_steps: 25 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
289
296
  reasoning_effort: high
290
297
  user_invocable: true
291
298
  can_delegate: false
@@ -307,6 +314,7 @@ agents:
307
314
  - context-compression
308
315
  prometheus:
309
316
  role: Infrastructure specialist
317
+ max_steps: 30 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
310
318
  reasoning_effort: medium
311
319
  user_invocable: true
312
320
  can_delegate: false
@@ -325,6 +333,7 @@ agents:
325
333
  - incremental-implementation
326
334
  hephaestus:
327
335
  role: AI pipelines
336
+ max_steps: 50 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
328
337
  reasoning_effort: medium
329
338
  user_invocable: true
330
339
  can_delegate: false
@@ -343,6 +352,7 @@ agents:
343
352
  - auto-continue
344
353
  nyx:
345
354
  role: Observability
355
+ max_steps: 25 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
346
356
  reasoning_effort: low
347
357
  user_invocable: true
348
358
  can_delegate: false
@@ -361,6 +371,7 @@ agents:
361
371
  - auto-continue
362
372
  gaia:
363
373
  role: Remote sensing domain specialist
374
+ max_steps: 25 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
364
375
  reasoning_effort: low
365
376
  user_invocable: false
366
377
  can_delegate: false
@@ -375,6 +386,7 @@ agents:
375
386
  - auto-continue
376
387
  iris:
377
388
  role: GitHub operations
389
+ max_steps: 20 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
378
390
  reasoning_effort: low
379
391
  user_invocable: true
380
392
  can_delegate: false
@@ -392,6 +404,7 @@ agents:
392
404
  - artifact-management
393
405
  mnemosyne:
394
406
  role: Memory bank
407
+ max_steps: 20 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
395
408
  reasoning_effort: low
396
409
  user_invocable: true
397
410
  can_delegate: false
@@ -415,6 +428,7 @@ agents:
415
428
  - session-goal
416
429
  talos:
417
430
  role: Hotfix agent
431
+ max_steps: 15 # R4: step budget — at max_steps the agent is forced to summarize-and-stop
418
432
  reasoning_effort: low
419
433
  user_invocable: true
420
434
  can_delegate: false
@@ -573,6 +587,34 @@ background_delegation:
573
587
  read_only_agents: [apollo, gaia]
574
588
  session_max: 3
575
589
  retry_count: 1
590
+ # ── R1: Per-error-type retry + provider cooldown (LiteLLM pattern) ──────
591
+ # retry_policy: max retries per error class before escalating. Exponential
592
+ # backoff (base 1s, doubling, capped 30s) applies between retries. auth
593
+ # errors are NEVER retried (0) — a credential problem will not fix itself.
594
+ # Enforcement: zeus-delegate-with-retry (manual waves) + RetryPolicyEngine.
595
+ retry_policy:
596
+ auth: 0
597
+ rate_limit: 3
598
+ timeout: 2
599
+ other: 1
600
+ # cooldown: after `allowed_fails` consecutive failures for a provider, skip
601
+ # that provider for `cooldown_time_seconds`. A success resets the counter.
602
+ # In-memory tracker (no Redis) — process-wide, shared across retry helpers.
603
+ cooldown:
604
+ allowed_fails: 3
605
+ cooldown_time_seconds: 60
606
+ # ── O5: permission.task — glob rules for which subagents an agent may
607
+ # invoke (last matching rule wins; deny removes the agent from the delegate
608
+ # tool description entirely — prevents circular delegation at the
609
+ # permission layer). Default "*": allow keeps the existing runtime matrix
610
+ # (zeus → anyone; athena/hermes → apollo only). Example deny-first config:
611
+ # permission:
612
+ # task:
613
+ # "*": deny
614
+ # "orchestrator-*": allow
615
+ permission:
616
+ task:
617
+ "*": allow
576
618
  todo_enforcer:
577
619
  enabled: true
578
620
  cooldown_base_ms: 5000 # base backoff between injections