pantheon-opencode 1.3.5-beta.60.efa533f → 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 (75) hide show
  1. package/README.md +38 -6
  2. package/package.json +4 -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 +58 -15
  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 +8 -2
  19. package/scripts/install/shared.mjs +8 -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/apollo.md +3 -0
  32. package/src/agents/demeter.md +1 -1
  33. package/src/agents/gaia.md +3 -0
  34. package/src/agents/hephaestus.md +1 -1
  35. package/src/agents/hermes.md +1 -1
  36. package/src/agents/themis.md +4 -0
  37. package/src/agents/zeus.md +5 -1
  38. package/src/mcp/code_mode_server.py +304 -18
  39. package/src/mcp/eval_store.py +233 -0
  40. package/src/mcp/mcp_persistence_server.py +23 -22
  41. package/src/mcp/mcp_resources_server.py +58 -0
  42. package/src/mcp/memory_mcp_server.py +80 -3
  43. package/src/pantheon/auto-wake.ts +2 -4
  44. package/src/pantheon/background-job-board.ts +17 -4
  45. package/src/pantheon/comment-checker.ts +48 -0
  46. package/src/pantheon/delegation-classifier.ts +307 -0
  47. package/src/pantheon/delegation-enforce.ts +67 -3
  48. package/src/pantheon/delegation-finalize.ts +44 -4
  49. package/src/pantheon/delegation.ts +413 -30
  50. package/src/pantheon/dispatch-guard.ts +2 -0
  51. package/src/pantheon/file-persistence.ts +7 -3
  52. package/src/pantheon/goal-loop.ts +13 -2
  53. package/src/pantheon/permission-eval.ts +284 -0
  54. package/src/pantheon/permission-globs.ts +86 -0
  55. package/src/pantheon/presets.d.mts +41 -0
  56. package/src/pantheon/presets.mjs +135 -0
  57. package/src/pantheon/retry-policy.ts +212 -0
  58. package/src/pantheon/session-guard.ts +48 -0
  59. package/src/pantheon/step-cap.ts +110 -0
  60. package/src/pantheon/todo-enforcer.ts +31 -5
  61. package/src/pantheon/todo-preserve.ts +7 -1
  62. package/src/pantheon/vision.ts +7 -5
  63. package/src/pantheon/zeus-delegate-with-retry.ts +401 -0
  64. package/src/plugin.ts +72 -13
  65. package/src/plugins/hook-runner.ts +1 -1
  66. package/src/plugins/pantheon-hooks.ts +1 -1
  67. package/src/plugins/tui/dist/tui.d.ts +1 -1
  68. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  69. package/src/plugins/tui/dist/tui.js +36 -14
  70. package/src/plugins/tui/dist/tui.tsx +71 -16
  71. package/src/plugins/tui/package.json +1 -1
  72. package/src/plugins/tui/src/index.tsx +71 -16
  73. package/src/plugins/tui/tsdown.config.ts +1 -1
  74. package/src/routing.yml +47 -0
  75. package/src/agents/README.md +0 -26
@@ -49,6 +49,20 @@ import { For, Show, createEffect, createMemo, createResource, createSignal, onCl
49
49
  * (the old ContextBar was removed) and we deliberately do NOT hack the
50
50
  * native footer. Track upstream: opencode statusline config.
51
51
  */
52
+ const __tuiPluginOnceKey = "__pantheonPluginsLoaded";
53
+ function pantheonPluginOnce(key) {
54
+ try {
55
+ const g = globalThis;
56
+ if (g[__tuiPluginOnceKey] === void 0) g[__tuiPluginOnceKey] = /* @__PURE__ */ new Set();
57
+ const set = g[__tuiPluginOnceKey];
58
+ if (!(set instanceof Set)) return false;
59
+ if (set.has(key)) return true;
60
+ set.add(key);
61
+ return false;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
52
66
  /** How often the sidebar re-reads .pantheon/active-preset.json so `set-tier`
53
67
  * changes made while opencode is open show up within ~30s. */
54
68
  const PRESET_REFRESH_MS = 3e4;
@@ -441,10 +455,10 @@ function parseConfig(raw) {
441
455
  if (!toml?.parse) return defaultConfig();
442
456
  const root = asTable(toml.parse(raw));
443
457
  const cfg = defaultConfig();
444
- const ui = asTable(root["ui"]);
445
- cfg.showBars = bool(ui["show_bars"], cfg.showBars);
446
- cfg.showStatus = bool(ui["show_status"], cfg.showStatus);
447
- const rawWidth = ui["bar_width"];
458
+ const ui = asTable(root.ui);
459
+ cfg.showBars = bool(ui.show_bars, cfg.showBars);
460
+ cfg.showStatus = bool(ui.show_status, cfg.showStatus);
461
+ const rawWidth = ui.bar_width;
448
462
  if (typeof rawWidth === "number" && Number.isFinite(rawWidth) && rawWidth >= 1) cfg.barWidth = Math.min(40, Math.floor(rawWidth));
449
463
  for (const id of [
450
464
  "anthropic",
@@ -453,14 +467,14 @@ function parseConfig(raw) {
453
467
  ]) {
454
468
  const t = asTable(root[id]);
455
469
  const p = cfg.providers[id];
456
- p.enabled = bool(t["enabled"], p.enabled);
457
- p.show["5h"] = bool(t["show_5h"], p.show["5h"]);
458
- p.show["7d"] = bool(t["show_7d"], p.show["7d"]);
459
- p.show["1m"] = bool(t["show_1m"], p.show["1m"]);
460
- p.show.model = bool(t["show_model"], p.show.model);
461
- const credentialsPath = str(t["credentials_path"]);
470
+ p.enabled = bool(t.enabled, p.enabled);
471
+ p.show["5h"] = bool(t.show_5h, p.show["5h"]);
472
+ p.show["7d"] = bool(t.show_7d, p.show["7d"]);
473
+ p.show["1m"] = bool(t.show_1m, p.show["1m"]);
474
+ p.show.model = bool(t.show_model, p.show.model);
475
+ const credentialsPath = str(t.credentials_path);
462
476
  if (credentialsPath !== void 0) p.credentialsPath = credentialsPath;
463
- const codexAuthPath = str(t["codex_auth_path"]);
477
+ const codexAuthPath = str(t.codex_auth_path);
464
478
  if (codexAuthPath !== void 0) p.codexAuthPath = codexAuthPath;
465
479
  }
466
480
  return cfg;
@@ -751,7 +765,7 @@ function parseDelegationMarkdown(raw, fileAlias, sessionID = "") {
751
765
  const startedAt = started !== void 0 ? Date.parse(started) : NaN;
752
766
  if (agent === void 0 || state === void 0 || Number.isNaN(startedAt)) return null;
753
767
  const normalized = state.toLowerCase();
754
- if (normalized !== "running" && normalized !== "completed" && normalized !== "error" && normalized !== "cancelled") return null;
768
+ if (normalized !== "running" && normalized !== "completed" && normalized !== "error" && normalized !== "startup_failed" && normalized !== "startup_unknown" && normalized !== "cancelled") return null;
755
769
  const finalizedAt = finalized !== void 0 ? Date.parse(finalized) : NaN;
756
770
  return {
757
771
  alias: title ?? (fileAlias !== void 0 ? stripMdSuffix(fileAlias) : "unknown"),
@@ -884,7 +898,7 @@ function delegationElapsed(entry, now) {
884
898
  * state machine testable without booting OpenCode's renderer. */
885
899
  function delegationActivity(entry) {
886
900
  if (entry.state === "completed") return "completed";
887
- if (entry.state === "error") return "error";
901
+ if (entry.state === "error" || entry.state === "startup_failed" || entry.state === "startup_unknown") return "error";
888
902
  if (entry.state === "cancelled") return "cancelled";
889
903
  if (entry.read) return "reading";
890
904
  if (entry.alias.startsWith("live-") && entry.taskID === void 0) return "delegating";
@@ -933,6 +947,7 @@ function mergeChildDelegationSources(children, live) {
933
947
  bySessionAlias.set(key(entry.sessionID, entry.alias), index);
934
948
  });
935
949
  for (const liveEntry of live) {
950
+ if (liveEntry.alias === null && liveEntry.taskID === null && Date.now() - liveEntry.startedAt > 3e4) continue;
936
951
  const incoming = toDelegationEntry(liveEntry);
937
952
  const index = (incoming.taskID !== void 0 ? byTask.get(incoming.taskID) : void 0) ?? (incoming.alias !== "" ? bySessionAlias.get(key(incoming.sessionID, incoming.alias)) : void 0);
938
953
  if (index === void 0) {
@@ -944,7 +959,12 @@ function mergeChildDelegationSources(children, live) {
944
959
  }
945
960
  const existing = result[index];
946
961
  if (existing === void 0) continue;
947
- if (existing.source === "md" && existing.state !== "running") continue;
962
+ if (existing.state !== "running") {
963
+ if (liveEntry.alias !== null && existing.alias !== incoming.alias) existing.alias = incoming.alias;
964
+ if (incoming.agent !== "agent" && existing.agent !== incoming.agent) existing.agent = incoming.agent;
965
+ if (incoming.read && !existing.read) existing.read = true;
966
+ continue;
967
+ }
948
968
  result[index] = {
949
969
  ...existing,
950
970
  alias: liveEntry.alias !== null ? incoming.alias : existing.alias,
@@ -1530,6 +1550,7 @@ function View(props) {
1530
1550
  ...c,
1531
1551
  status: resolveStatus(c.id)
1532
1552
  })), md, now());
1553
+ for (const [k, v] of props.liveStore.map) if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 3e4) props.liveStore.map.delete(k);
1533
1554
  const liveEntries = [...props.liveStore.map.values()].filter((entry) => entry.sessionID === sessionID);
1534
1555
  setChildDelegations(mergeChildDelegationSources(childEntries, liveEntries));
1535
1556
  panelLog.info(`panel: children=${children.length} md=${md.length} events=${eventRefreshCount}`);
@@ -1768,6 +1789,7 @@ function View(props) {
1768
1789
  })();
1769
1790
  }
1770
1791
  const tui = (api, _options, _meta) => {
1792
+ if (pantheonPluginOnce("pantheon:tui")) return;
1771
1793
  const [version, setVersion] = createSignal(null);
1772
1794
  detectVersion(api).then((detected) => setVersion(detected)).catch(() => setVersion(null));
1773
1795
  setupUsageBar(api);
@@ -63,6 +63,24 @@ import {
63
63
  Show,
64
64
  } from 'solid-js'
65
65
 
66
+ // ─── Double-load guard (mirrors src/pantheon/plugin-once.ts) ───────────────
67
+ // Opencode merges global + project plugin paths, so this module can load twice
68
+ // in one process. The second factory invocation becomes a no-op.
69
+ const __tuiPluginOnceKey = '__pantheonPluginsLoaded'
70
+ function pantheonPluginOnce(key: string): boolean {
71
+ try {
72
+ const g = globalThis as unknown as Record<string, unknown>
73
+ if (g[__tuiPluginOnceKey] === undefined) g[__tuiPluginOnceKey] = new Set<string>()
74
+ const set = g[__tuiPluginOnceKey]
75
+ if (!(set instanceof Set)) return false
76
+ if ((set as Set<string>).has(key)) return true
77
+ ;(set as Set<string>).add(key)
78
+ return false
79
+ } catch {
80
+ return false
81
+ }
82
+ }
83
+
66
84
  /* ─── Constants ─────────────────────────────────────────── */
67
85
 
68
86
  /** How often the sidebar re-reads .pantheon/active-preset.json so `set-tier`
@@ -659,25 +677,25 @@ function parseConfig(raw: string): UsageBarConfig {
659
677
  const root = asTable(toml.parse(raw))
660
678
  const cfg = defaultConfig()
661
679
 
662
- const ui = asTable(root['ui'])
663
- cfg.showBars = bool(ui['show_bars'], cfg.showBars)
664
- cfg.showStatus = bool(ui['show_status'], cfg.showStatus)
665
- 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
666
684
  if (typeof rawWidth === 'number' && Number.isFinite(rawWidth) && rawWidth >= 1)
667
685
  cfg.barWidth = Math.min(40, Math.floor(rawWidth))
668
686
 
669
687
  for (const id of ['anthropic', 'openai', 'opencodego'] as ProviderId[]) {
670
688
  const t = asTable(root[id])
671
689
  const p = cfg.providers[id]
672
- p.enabled = bool(t['enabled'], p.enabled)
673
- p.show['5h'] = bool(t['show_5h'], p.show['5h'])
674
- p.show['7d'] = bool(t['show_7d'], p.show['7d'])
675
- p.show['1m'] = bool(t['show_1m'], p.show['1m'])
676
- 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)
677
695
  // Assign only when present — keeps `exactOptionalPropertyTypes` happy.
678
- const credentialsPath = str(t['credentials_path'])
696
+ const credentialsPath = str(t.credentials_path)
679
697
  if (credentialsPath !== undefined) p.credentialsPath = credentialsPath
680
- const codexAuthPath = str(t['codex_auth_path'])
698
+ const codexAuthPath = str(t.codex_auth_path)
681
699
  if (codexAuthPath !== undefined) p.codexAuthPath = codexAuthPath
682
700
  }
683
701
  return cfg
@@ -924,7 +942,7 @@ export type DelegationEntry = {
924
942
  taskID?: string
925
943
  /** Agent name, e.g. "apollo". */
926
944
  agent: string
927
- state: 'running' | 'completed' | 'error' | 'cancelled'
945
+ state: 'running' | 'completed' | 'error' | 'startup_failed' | 'startup_unknown' | 'cancelled'
928
946
  /** Epoch ms of the `Started` header. */
929
947
  startedAt: number
930
948
  /** Epoch ms of the `Finalized` header — null while still running. */
@@ -1034,6 +1052,8 @@ export function parseDelegationMarkdown(
1034
1052
  normalized !== 'running' &&
1035
1053
  normalized !== 'completed' &&
1036
1054
  normalized !== 'error' &&
1055
+ normalized !== 'startup_failed' &&
1056
+ normalized !== 'startup_unknown' &&
1037
1057
  normalized !== 'cancelled'
1038
1058
  ) {
1039
1059
  return null
@@ -1203,7 +1223,12 @@ export type DelegationActivity =
1203
1223
 
1204
1224
  export function delegationActivity(entry: DelegationEntry): DelegationActivity {
1205
1225
  if (entry.state === 'completed') return 'completed'
1206
- 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'
1207
1232
  if (entry.state === 'cancelled') return 'cancelled'
1208
1233
  if (entry.read) return 'reading'
1209
1234
  if (entry.alias.startsWith('live-') && entry.taskID === undefined) return 'delegating'
@@ -1256,6 +1281,12 @@ export function mergeChildDelegationSources(
1256
1281
  })
1257
1282
 
1258
1283
  for (const liveEntry of live) {
1284
+ if (
1285
+ liveEntry.alias === null &&
1286
+ liveEntry.taskID === null &&
1287
+ Date.now() - liveEntry.startedAt > 30_000
1288
+ )
1289
+ continue
1259
1290
  const incoming = toDelegationEntry(liveEntry)
1260
1291
  const index =
1261
1292
  (incoming.taskID !== undefined ? byTask.get(incoming.taskID) : undefined) ??
@@ -1273,8 +1304,26 @@ export function mergeChildDelegationSources(
1273
1304
 
1274
1305
  const existing = result[index]
1275
1306
  if (existing === undefined) continue
1276
- const finalized = existing.source === 'md' && existing.state !== 'running'
1277
- 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
+ }
1278
1327
  result[index] = {
1279
1328
  ...existing,
1280
1329
  alias: liveEntry.alias !== null ? incoming.alias : existing.alias,
@@ -2201,6 +2250,9 @@ function View(props: {
2201
2250
  }
2202
2251
  const withStatus = children.map((c) => ({ ...c, status: resolveStatus(c.id) }))
2203
2252
  const childEntries = childrenToDelegationEntries(withStatus, md, now())
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)
2204
2256
  // The event channel is optimistic: it renders a job even while the
2205
2257
  // child session/report is still being created. Filter by parent so a
2206
2258
  // different focused session never leaks rows into this sidebar.
@@ -2397,6 +2449,7 @@ function View(props: {
2397
2449
  /* ─── Plugin Registration ────────────────────────────────── */
2398
2450
 
2399
2451
  const tui: TuiPlugin = (api, _options, _meta) => {
2452
+ if (pantheonPluginOnce('pantheon:tui')) return
2400
2453
  // Do not await project file/process probes here. OpenCode waits for the TUI
2401
2454
  // factory before it finishes startup, and a Windows host opening a WSL
2402
2455
  // project can leave those probes pending while the project bridge comes up.
@@ -2467,7 +2520,9 @@ const tui: TuiPlugin = (api, _options, _meta) => {
2467
2520
  order: 900,
2468
2521
  slots: {
2469
2522
  sidebar_content(_ctx, props) {
2470
- 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
+ )
2471
2526
  },
2472
2527
  },
2473
2528
  })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon-tui",
3
3
  "type": "module",
4
- "version": "1.3.5-beta.60.efa533f",
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": {
@@ -63,6 +63,24 @@ import {
63
63
  Show,
64
64
  } from 'solid-js'
65
65
 
66
+ // ─── Double-load guard (mirrors src/pantheon/plugin-once.ts) ───────────────
67
+ // Opencode merges global + project plugin paths, so this module can load twice
68
+ // in one process. The second factory invocation becomes a no-op.
69
+ const __tuiPluginOnceKey = '__pantheonPluginsLoaded'
70
+ function pantheonPluginOnce(key: string): boolean {
71
+ try {
72
+ const g = globalThis as unknown as Record<string, unknown>
73
+ if (g[__tuiPluginOnceKey] === undefined) g[__tuiPluginOnceKey] = new Set<string>()
74
+ const set = g[__tuiPluginOnceKey]
75
+ if (!(set instanceof Set)) return false
76
+ if ((set as Set<string>).has(key)) return true
77
+ ;(set as Set<string>).add(key)
78
+ return false
79
+ } catch {
80
+ return false
81
+ }
82
+ }
83
+
66
84
  /* ─── Constants ─────────────────────────────────────────── */
67
85
 
68
86
  /** How often the sidebar re-reads .pantheon/active-preset.json so `set-tier`
@@ -659,25 +677,25 @@ function parseConfig(raw: string): UsageBarConfig {
659
677
  const root = asTable(toml.parse(raw))
660
678
  const cfg = defaultConfig()
661
679
 
662
- const ui = asTable(root['ui'])
663
- cfg.showBars = bool(ui['show_bars'], cfg.showBars)
664
- cfg.showStatus = bool(ui['show_status'], cfg.showStatus)
665
- 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
666
684
  if (typeof rawWidth === 'number' && Number.isFinite(rawWidth) && rawWidth >= 1)
667
685
  cfg.barWidth = Math.min(40, Math.floor(rawWidth))
668
686
 
669
687
  for (const id of ['anthropic', 'openai', 'opencodego'] as ProviderId[]) {
670
688
  const t = asTable(root[id])
671
689
  const p = cfg.providers[id]
672
- p.enabled = bool(t['enabled'], p.enabled)
673
- p.show['5h'] = bool(t['show_5h'], p.show['5h'])
674
- p.show['7d'] = bool(t['show_7d'], p.show['7d'])
675
- p.show['1m'] = bool(t['show_1m'], p.show['1m'])
676
- 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)
677
695
  // Assign only when present — keeps `exactOptionalPropertyTypes` happy.
678
- const credentialsPath = str(t['credentials_path'])
696
+ const credentialsPath = str(t.credentials_path)
679
697
  if (credentialsPath !== undefined) p.credentialsPath = credentialsPath
680
- const codexAuthPath = str(t['codex_auth_path'])
698
+ const codexAuthPath = str(t.codex_auth_path)
681
699
  if (codexAuthPath !== undefined) p.codexAuthPath = codexAuthPath
682
700
  }
683
701
  return cfg
@@ -924,7 +942,7 @@ export type DelegationEntry = {
924
942
  taskID?: string
925
943
  /** Agent name, e.g. "apollo". */
926
944
  agent: string
927
- state: 'running' | 'completed' | 'error' | 'cancelled'
945
+ state: 'running' | 'completed' | 'error' | 'startup_failed' | 'startup_unknown' | 'cancelled'
928
946
  /** Epoch ms of the `Started` header. */
929
947
  startedAt: number
930
948
  /** Epoch ms of the `Finalized` header — null while still running. */
@@ -1034,6 +1052,8 @@ export function parseDelegationMarkdown(
1034
1052
  normalized !== 'running' &&
1035
1053
  normalized !== 'completed' &&
1036
1054
  normalized !== 'error' &&
1055
+ normalized !== 'startup_failed' &&
1056
+ normalized !== 'startup_unknown' &&
1037
1057
  normalized !== 'cancelled'
1038
1058
  ) {
1039
1059
  return null
@@ -1203,7 +1223,12 @@ export type DelegationActivity =
1203
1223
 
1204
1224
  export function delegationActivity(entry: DelegationEntry): DelegationActivity {
1205
1225
  if (entry.state === 'completed') return 'completed'
1206
- 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'
1207
1232
  if (entry.state === 'cancelled') return 'cancelled'
1208
1233
  if (entry.read) return 'reading'
1209
1234
  if (entry.alias.startsWith('live-') && entry.taskID === undefined) return 'delegating'
@@ -1256,6 +1281,12 @@ export function mergeChildDelegationSources(
1256
1281
  })
1257
1282
 
1258
1283
  for (const liveEntry of live) {
1284
+ if (
1285
+ liveEntry.alias === null &&
1286
+ liveEntry.taskID === null &&
1287
+ Date.now() - liveEntry.startedAt > 30_000
1288
+ )
1289
+ continue
1259
1290
  const incoming = toDelegationEntry(liveEntry)
1260
1291
  const index =
1261
1292
  (incoming.taskID !== undefined ? byTask.get(incoming.taskID) : undefined) ??
@@ -1273,8 +1304,26 @@ export function mergeChildDelegationSources(
1273
1304
 
1274
1305
  const existing = result[index]
1275
1306
  if (existing === undefined) continue
1276
- const finalized = existing.source === 'md' && existing.state !== 'running'
1277
- 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
+ }
1278
1327
  result[index] = {
1279
1328
  ...existing,
1280
1329
  alias: liveEntry.alias !== null ? incoming.alias : existing.alias,
@@ -2201,6 +2250,9 @@ function View(props: {
2201
2250
  }
2202
2251
  const withStatus = children.map((c) => ({ ...c, status: resolveStatus(c.id) }))
2203
2252
  const childEntries = childrenToDelegationEntries(withStatus, md, now())
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)
2204
2256
  // The event channel is optimistic: it renders a job even while the
2205
2257
  // child session/report is still being created. Filter by parent so a
2206
2258
  // different focused session never leaks rows into this sidebar.
@@ -2397,6 +2449,7 @@ function View(props: {
2397
2449
  /* ─── Plugin Registration ────────────────────────────────── */
2398
2450
 
2399
2451
  const tui: TuiPlugin = (api, _options, _meta) => {
2452
+ if (pantheonPluginOnce('pantheon:tui')) return
2400
2453
  // Do not await project file/process probes here. OpenCode waits for the TUI
2401
2454
  // factory before it finishes startup, and a Windows host opening a WSL
2402
2455
  // project can leave those probes pending while the project bridge comes up.
@@ -2467,7 +2520,9 @@ const tui: TuiPlugin = (api, _options, _meta) => {
2467
2520
  order: 900,
2468
2521
  slots: {
2469
2522
  sidebar_content(_ctx, props) {
2470
- 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
+ )
2471
2526
  },
2472
2527
  },
2473
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
@@ -457,6 +471,11 @@ intent_gate:
457
471
  - search
458
472
  - lookup
459
473
  - documentation
474
+ - procure
475
+ - busque
476
+ - encontre
477
+ - localize
478
+ - pesquise
460
479
  target: apollo
461
480
  investigate:
462
481
  patterns:
@@ -568,6 +587,34 @@ background_delegation:
568
587
  read_only_agents: [apollo, gaia]
569
588
  session_max: 3
570
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
571
618
  todo_enforcer:
572
619
  enabled: true
573
620
  cooldown_base_ms: 5000 # base backoff between injections
@@ -1,26 +0,0 @@
1
- # Agent Reference — Pantheon
2
-
3
- This directory contains the definitions for all 14 Pantheon agents. Each agent is defined in a `.agent.md` file with YAML frontmatter specifying tools, model tier, capabilities, and delegation rules.
4
-
5
- ## Agents
6
-
7
- | Agent | File | Role |
8
- |-------|------|------|
9
- | @zeus | [zeus.agent.md](zeus.agent.md) | Central orchestrator |
10
- | @athena | [athena.agent.md](athena.agent.md) | Strategic planner |
11
- | @apollo | [apollo.agent.md](apollo.agent.md) | Investigation scout |
12
- | @hermes | [hermes.agent.md](hermes.agent.md) | Backend specialist |
13
- | @aphrodite | [aphrodite.agent.md](aphrodite.agent.md) | Frontend specialist |
14
- | @demeter | [demeter.agent.md](demeter.agent.md) | Database specialist |
15
- | @themis | [themis.agent.md](themis.agent.md) | Quality & security gate |
16
- | @prometheus | [prometheus.agent.md](prometheus.agent.md) | Infrastructure specialist |
17
- | @hephaestus | [hephaestus.agent.md](hephaestus.agent.md) | AI pipelines |
18
- | @nyx | [nyx.agent.md](nyx.agent.md) | Observability |
19
- | @gaia | [gaia.agent.md](gaia.agent.md) | Remote sensing |
20
- | @iris | [iris.agent.md](iris.agent.md) | GitHub operations |
21
- | @mnemosyne | [mnemosyne.agent.md](mnemosyne.agent.md) | Memory bank |
22
- | @talos | [talos.agent.md](talos.agent.md) | Hotfix agent |
23
-
24
- ## Full Reference
25
-
26
- See [AGENTS.md](../../AGENTS.md) for the complete agent reference with roles, capabilities, and invocation patterns.