bosun 0.42.5 → 0.43.0

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 (473) hide show
  1. package/.env.example +36 -51
  2. package/README.md +19 -3
  3. package/agent/agent-custom-tools.mjs +138 -26
  4. package/agent/agent-endpoint.mjs +1 -2
  5. package/agent/agent-event-bus.mjs +33 -2
  6. package/agent/agent-hooks.mjs +1 -1
  7. package/agent/agent-launcher.mjs +6210 -0
  8. package/agent/agent-pool.mjs +7 -4018
  9. package/agent/agent-prompt-catalog.mjs +5 -6
  10. package/agent/agent-prompts.mjs +62 -6
  11. package/agent/agent-sdk.mjs +130 -0
  12. package/agent/agent-supervisor.mjs +30 -6
  13. package/agent/auth/_shared.mjs +129 -0
  14. package/agent/auth/anthropic-api-key.mjs +13 -0
  15. package/agent/auth/azure-openai.mjs +17 -0
  16. package/agent/auth/cerebras.mjs +14 -0
  17. package/agent/auth/chatgpt-codex-subscription.mjs +15 -0
  18. package/agent/auth/claude-subscription.mjs +15 -0
  19. package/agent/auth/copilot-oauth.mjs +13 -0
  20. package/agent/auth/deepinfra.mjs +14 -0
  21. package/agent/auth/fireworks.mjs +14 -0
  22. package/agent/auth/gemini-api-key.mjs +14 -0
  23. package/agent/auth/groq.mjs +14 -0
  24. package/agent/auth/index.mjs +85 -0
  25. package/agent/auth/nebius.mjs +14 -0
  26. package/agent/auth/ollama.mjs +14 -0
  27. package/agent/auth/openai-api-key.mjs +13 -0
  28. package/agent/auth/openai-compatible.mjs +15 -0
  29. package/agent/auth/openrouter.mjs +14 -0
  30. package/agent/auth/perplexity.mjs +14 -0
  31. package/agent/auth/sambanova.mjs +14 -0
  32. package/agent/auth/together.mjs +14 -0
  33. package/agent/auth/xai.mjs +14 -0
  34. package/agent/autofix-prompts.mjs +2 -2
  35. package/agent/autofix.mjs +2 -2
  36. package/agent/bosun-skills.mjs +215 -86
  37. package/agent/fleet-coordinator.mjs +161 -32
  38. package/agent/harness/agent-loop.mjs +26 -0
  39. package/agent/harness/event-contract.mjs +125 -0
  40. package/agent/harness/followup-queue.mjs +33 -0
  41. package/agent/harness/message-normalizer.mjs +43 -0
  42. package/agent/harness/module-boundaries.md +73 -0
  43. package/agent/harness/run-contract.mjs +122 -0
  44. package/agent/harness/runtime-config.mjs +132 -0
  45. package/agent/harness/session-state.mjs +80 -0
  46. package/agent/harness/steering-queue.mjs +35 -0
  47. package/agent/harness/tool-runner.mjs +95 -0
  48. package/agent/harness/turn-runner.mjs +135 -0
  49. package/agent/harness-agent-service.mjs +852 -0
  50. package/agent/harness-executor-config.mjs +384 -0
  51. package/agent/hook-library.mjs +141 -2
  52. package/agent/hook-profiles.mjs +15 -2
  53. package/agent/internal-harness-control-plane.mjs +672 -0
  54. package/agent/internal-harness-profile.mjs +519 -0
  55. package/agent/internal-harness-runtime.mjs +1219 -0
  56. package/agent/lineage-graph.mjs +141 -0
  57. package/agent/primary-agent.mjs +602 -706
  58. package/agent/provider-auth-manager.mjs +830 -0
  59. package/agent/provider-auth-state.mjs +440 -0
  60. package/agent/provider-capabilities.mjs +116 -0
  61. package/agent/provider-kernel.mjs +596 -0
  62. package/agent/provider-message-transform.mjs +583 -0
  63. package/agent/provider-model-catalog.mjs +163 -0
  64. package/agent/provider-registry.mjs +657 -0
  65. package/agent/provider-runtime-discovery.mjs +147 -0
  66. package/agent/provider-session.mjs +767 -0
  67. package/agent/providers/_shared.mjs +397 -0
  68. package/agent/providers/anthropic-messages.mjs +64 -0
  69. package/agent/providers/azure-openai-responses.mjs +69 -0
  70. package/agent/providers/cerebras.mjs +66 -0
  71. package/agent/providers/claude-subscription-shim.mjs +68 -0
  72. package/agent/providers/copilot-oauth.mjs +66 -0
  73. package/agent/providers/deepinfra.mjs +66 -0
  74. package/agent/providers/fireworks.mjs +66 -0
  75. package/agent/providers/gemini-generate-content.mjs +66 -0
  76. package/agent/providers/groq.mjs +66 -0
  77. package/agent/providers/index.mjs +208 -0
  78. package/agent/providers/nebius.mjs +66 -0
  79. package/agent/providers/ollama.mjs +66 -0
  80. package/agent/providers/openai-codex-subscription.mjs +75 -0
  81. package/agent/providers/openai-compatible.mjs +65 -0
  82. package/agent/providers/openai-responses.mjs +67 -0
  83. package/agent/providers/openrouter.mjs +66 -0
  84. package/agent/providers/perplexity.mjs +66 -0
  85. package/agent/providers/provider-contract.mjs +138 -0
  86. package/agent/providers/provider-errors.mjs +63 -0
  87. package/agent/providers/provider-model-pricing.mjs +246 -0
  88. package/agent/providers/provider-stream-normalizer.mjs +7 -0
  89. package/agent/providers/provider-usage-normalizer.mjs +48 -0
  90. package/agent/providers/sambanova.mjs +66 -0
  91. package/agent/providers/together.mjs +66 -0
  92. package/agent/providers/xai.mjs +66 -0
  93. package/agent/query-engine.mjs +260 -0
  94. package/agent/retry-queue.mjs +1 -0
  95. package/agent/review-agent.mjs +1 -1
  96. package/agent/session-contract.mjs +127 -0
  97. package/agent/session-manager.mjs +1859 -0
  98. package/agent/session-replay.mjs +617 -0
  99. package/agent/session-snapshot-store.mjs +379 -0
  100. package/agent/skills/agent-coordination.md +6 -0
  101. package/agent/skills/background-task-execution.md +6 -0
  102. package/agent/skills/bosun-agent-api.md +6 -0
  103. package/agent/skills/code-quality-anti-patterns.md +7 -0
  104. package/agent/skills/commit-conventions.md +6 -0
  105. package/agent/skills/custom-tool-creation.md +6 -0
  106. package/agent/skills/error-recovery.md +6 -0
  107. package/agent/skills/pr-workflow.md +6 -0
  108. package/agent/skills/tdd-pattern.md +6 -0
  109. package/agent/subagent-contract.mjs +104 -0
  110. package/agent/subagent-control.mjs +633 -0
  111. package/agent/subagent-pool.mjs +260 -0
  112. package/agent/thread-contract.mjs +88 -0
  113. package/agent/thread-registry.mjs +552 -0
  114. package/agent/tool-approval-manager.mjs +259 -0
  115. package/agent/tool-builtin-catalog.mjs +855 -0
  116. package/agent/tool-contract.mjs +101 -0
  117. package/agent/tool-event-contract.mjs +99 -0
  118. package/agent/tool-execution-ledger.mjs +32 -0
  119. package/agent/tool-network-policy.mjs +86 -0
  120. package/agent/tool-orchestrator.mjs +382 -0
  121. package/agent/tool-output-truncation.mjs +70 -0
  122. package/agent/tool-registry.mjs +200 -0
  123. package/agent/tool-retry-policy.mjs +57 -0
  124. package/agent/tool-runtime-context.mjs +220 -0
  125. package/bench/harness-load-bench.mjs +281 -0
  126. package/bench/harness-parity-bench.mjs +214 -0
  127. package/bench/swebench/bosun-swebench.mjs +21 -6
  128. package/bosun-tui.mjs +59 -13
  129. package/bosun.config.example.json +55 -2
  130. package/bosun.schema.json +598 -5
  131. package/cli.mjs +656 -160
  132. package/config/config-doctor.mjs +80 -26
  133. package/config/config-editor.mjs +417 -0
  134. package/config/config.mjs +489 -144
  135. package/config/repo-config.mjs +125 -49
  136. package/config/repo-root.mjs +33 -1
  137. package/desktop/main.mjs +554 -115
  138. package/desktop/package.json +1 -1
  139. package/git/diff-stats.mjs +7 -5
  140. package/git/git-editor-fix.mjs +2 -42
  141. package/github/github-app-auth.mjs +6 -0
  142. package/github/github-oauth-portal.mjs +20 -0
  143. package/infra/anomaly-detector.mjs +122 -22
  144. package/infra/approval-projection-store.mjs +75 -0
  145. package/infra/config-reload-bus.mjs +33 -0
  146. package/infra/container-runner.mjs +37 -4
  147. package/infra/error-detector.mjs +110 -35
  148. package/infra/event-schema.mjs +353 -0
  149. package/infra/guardrails.mjs +383 -0
  150. package/infra/heartbeat-monitor.mjs +432 -0
  151. package/infra/library-manager.mjs +367 -19
  152. package/infra/live-event-projector.mjs +197 -0
  153. package/infra/maintenance.mjs +202 -51
  154. package/infra/monitor.mjs +1749 -2027
  155. package/infra/preflight.mjs +107 -6
  156. package/infra/presence.mjs +33 -9
  157. package/infra/projection-contract.mjs +27 -0
  158. package/infra/provider-usage-ledger.mjs +73 -0
  159. package/infra/replay-reader.mjs +140 -0
  160. package/infra/runtime-accumulator.mjs +303 -8
  161. package/infra/runtime-metrics.mjs +156 -0
  162. package/infra/session-projection-store.mjs +169 -0
  163. package/infra/session-telemetry-runtime.mjs +580 -0
  164. package/infra/session-telemetry.mjs +338 -0
  165. package/infra/session-tracker.mjs +1613 -228
  166. package/infra/startup-service.mjs +0 -2
  167. package/infra/storage-janitor.mjs +1046 -0
  168. package/infra/subagent-projection-store.mjs +89 -0
  169. package/infra/test-runtime.mjs +53 -20
  170. package/infra/trace-export.mjs +103 -0
  171. package/infra/tui-bridge.mjs +607 -5
  172. package/infra/update-check.mjs +7 -8
  173. package/infra/windows-hidden-child-processes.mjs +99 -0
  174. package/infra/worktree-recovery-state.mjs +20 -7
  175. package/kanban/kanban-adapter.mjs +702 -310
  176. package/kanban/repo-mirror-projection-store.mjs +871 -0
  177. package/lib/agent-configuration-guide.mjs +280 -0
  178. package/lib/hot-path-runtime.mjs +1061 -0
  179. package/lib/integrations-registry.mjs +294 -0
  180. package/lib/log-tail.mjs +101 -0
  181. package/lib/logger.mjs +21 -25
  182. package/lib/mojibake-repair.mjs +40 -0
  183. package/lib/repo-map.mjs +137 -24
  184. package/lib/request-json-api.mjs +59 -0
  185. package/lib/safe-box.mjs +56 -0
  186. package/lib/session-insights.mjs +3 -1
  187. package/lib/skill-markdown-safety.mjs +394 -0
  188. package/lib/state-ledger-sqlite.mjs +4462 -0
  189. package/lib/vault-keychain.mjs +259 -0
  190. package/lib/vault.mjs +374 -0
  191. package/lib/workflow-flowchart-utils.mjs +326 -0
  192. package/monitor-tail-sanitizer.mjs +1 -2
  193. package/native/bosun-telemetry/Cargo.toml +9 -0
  194. package/native/bosun-telemetry/src/export.rs +151 -0
  195. package/native/bosun-telemetry/src/main.rs +76 -0
  196. package/native/bosun-telemetry/src/metrics.rs +114 -0
  197. package/native/bosun-telemetry/src/session_telemetry.rs +178 -0
  198. package/native/bosun-unified-exec/Cargo.lock +107 -0
  199. package/native/bosun-unified-exec/Cargo.toml +8 -0
  200. package/native/bosun-unified-exec/src/async_watcher.rs +145 -0
  201. package/native/bosun-unified-exec/src/head_tail_buffer.rs +241 -0
  202. package/native/bosun-unified-exec/src/main.rs +86 -0
  203. package/native/bosun-unified-exec/src/process_manager.rs +308 -0
  204. package/native/bosun-unified-exec/src/tool_orchestrator.rs +187 -0
  205. package/package.json +230 -59
  206. package/postinstall.mjs +182 -13
  207. package/server/bosun-mcp-server.mjs +348 -10
  208. package/server/routes/harness-agent-bridge.mjs +128 -0
  209. package/server/routes/harness-approvals.mjs +290 -0
  210. package/server/routes/harness-events.mjs +469 -0
  211. package/server/routes/harness-providers.mjs +385 -0
  212. package/server/routes/harness-sessions.mjs +2230 -0
  213. package/server/routes/harness-subagents.mjs +138 -0
  214. package/server/routes/harness-surface-payload.mjs +74 -0
  215. package/server/setup-web-server.mjs +468 -39
  216. package/server/ui-server.mjs +13041 -4418
  217. package/setup.mjs +206 -298
  218. package/shared-workspaces.json +1 -1
  219. package/shell/anthropic-native-adapter.mjs +1218 -0
  220. package/shell/auth-resolver.mjs +247 -0
  221. package/shell/claude-shell.mjs +85 -2
  222. package/shell/codex-config-file.mjs +9 -0
  223. package/shell/codex-config.mjs +192 -249
  224. package/shell/codex-model-profiles.mjs +76 -12
  225. package/shell/codex-sdk-import.mjs +7 -0
  226. package/shell/codex-shell.mjs +708 -170
  227. package/shell/context-compaction.mjs +898 -0
  228. package/shell/copilot-shell.mjs +359 -109
  229. package/shell/gemini-native-adapter.mjs +411 -0
  230. package/shell/gemini-shell.mjs +121 -13
  231. package/shell/mcp-client.mjs +401 -0
  232. package/shell/mcp-registry.mjs +72 -0
  233. package/shell/message-pruner.mjs +248 -0
  234. package/shell/openai-native-adapter.mjs +1975 -0
  235. package/shell/opencode-providers.mjs +16 -531
  236. package/shell/opencode-shell.mjs +180 -9
  237. package/shell/provider-transform.mjs +386 -0
  238. package/shell/pwsh-runtime.mjs +9 -2
  239. package/shell/retry-fetch.mjs +244 -0
  240. package/shell/session-resume.mjs +97 -0
  241. package/shell/session-store.mjs +215 -0
  242. package/shell/shell-adapter-registry.mjs +346 -0
  243. package/shell/shell-session-compat.mjs +442 -0
  244. package/shell/smooth-stream.mjs +233 -0
  245. package/shell/stop-condition.mjs +238 -0
  246. package/shell/tool-call-repair.mjs +345 -0
  247. package/shell/tool-executor.mjs +571 -0
  248. package/task/pipeline.mjs +3 -1
  249. package/task/task-assessment.mjs +312 -6
  250. package/task/task-claims.mjs +312 -48
  251. package/task/task-cli.mjs +90 -14
  252. package/task/task-complexity.mjs +6 -6
  253. package/task/task-context.mjs +37 -0
  254. package/task/task-debt-ledger.mjs +110 -0
  255. package/task/task-executor.mjs +1104 -119
  256. package/task/task-replanner.mjs +553 -0
  257. package/task/task-simulate-cli.mjs +1481 -0
  258. package/task/task-store.mjs +996 -68
  259. package/telegram/executor-health-region-cache.mjs +75 -0
  260. package/telegram/get-telegram-chat-id.mjs +0 -0
  261. package/telegram/harness-api-client.mjs +124 -0
  262. package/telegram/sticky-menu-state.mjs +384 -0
  263. package/telegram/telegram-bot.mjs +548 -865
  264. package/telegram/telegram-sentinel.mjs +25 -14
  265. package/telegram/telegram-surface-runtime.mjs +53 -0
  266. package/tools/generate-demo-defaults.mjs +23 -4
  267. package/tools/harness-hotpath-bench.mjs +246 -0
  268. package/tools/import-check.mjs +279 -234
  269. package/tools/install-git-hooks.mjs +96 -20
  270. package/tools/native-rust.mjs +124 -0
  271. package/tools/packed-cli-smoke.mjs +147 -0
  272. package/tools/prepublish-check.mjs +53 -1
  273. package/tools/run-workflow-guaranteed-suite.mjs +56 -0
  274. package/tools/site-serve.mjs +112 -0
  275. package/tools/sync-demo-ui.mjs +188 -0
  276. package/tools/syntax-check.mjs +32 -28
  277. package/tools/test-kanban-enhancement.mjs +7 -7
  278. package/tools/test-shared-state-integration.mjs +5 -19
  279. package/tools/vite-windows-realpath-shim.mjs +274 -0
  280. package/tools/vitest-esbuild-shim.mjs +45 -0
  281. package/tools/vitest-full-suite.mjs +310 -0
  282. package/tools/vitest-runner.mjs +505 -11
  283. package/tools/workflow-orphan-worktree-recovery.mjs +24 -7
  284. package/tui/CommandPalette.js +87 -0
  285. package/tui/app.mjs +463 -37
  286. package/tui/components/status-header.mjs +43 -1
  287. package/tui/lib/command-palette.mjs +191 -0
  288. package/tui/lib/connection-target.mjs +577 -0
  289. package/tui/lib/header-config.mjs +0 -2
  290. package/tui/lib/navigation.mjs +8 -3
  291. package/tui/lib/ws-bridge.mjs +141 -51
  292. package/tui/screens/agents-screen-helpers.mjs +87 -3
  293. package/tui/screens/agents.mjs +1074 -202
  294. package/tui/screens/connection-setup.mjs +363 -0
  295. package/tui/screens/harness-approvals.mjs +7 -0
  296. package/tui/screens/harness-sessions.mjs +109 -0
  297. package/tui/screens/harness-subagents.mjs +18 -0
  298. package/tui/screens/harness-telemetry.mjs +67 -0
  299. package/tui/screens/logs.mjs +325 -0
  300. package/tui/screens/settings-screen-helpers.mjs +75 -0
  301. package/tui/screens/settings.mjs +397 -0
  302. package/tui/screens/status.mjs +130 -5
  303. package/tui/screens/telemetry-screen-helpers.mjs +158 -0
  304. package/tui/screens/telemetry.mjs +246 -0
  305. package/tui/screens/workflows.mjs +984 -0
  306. package/ui/app.js +746 -189
  307. package/ui/app.monolith.js +2 -3
  308. package/ui/assets/toastui-editor-all.min.js +24 -0
  309. package/ui/components/agent-selector.js +706 -49
  310. package/ui/components/charts.js +16 -12
  311. package/ui/components/chat-view.js +536 -35
  312. package/ui/components/commit-graph.js +648 -0
  313. package/ui/components/context-menu.js +89 -0
  314. package/ui/components/diff-viewer.js +169 -53
  315. package/ui/components/forms.js +13 -2
  316. package/ui/components/kanban-board.js +541 -92
  317. package/ui/components/session-list.js +303 -66
  318. package/ui/components/shared.js +9 -1
  319. package/ui/components/task-markdown.js +272 -0
  320. package/ui/components/workspace-executor-settings.js +142 -0
  321. package/ui/components/workspace-switcher.js +35 -79
  322. package/ui/demo-defaults.js +17278 -7297
  323. package/ui/demo.html +7058 -5338
  324. package/ui/index.html +189 -112
  325. package/ui/modules/agent-events.js +309 -36
  326. package/ui/modules/api.js +236 -13
  327. package/ui/modules/chat-turn-groups.js +101 -0
  328. package/ui/modules/harness-client.js +56 -0
  329. package/ui/modules/icon-utils.js +9 -1
  330. package/ui/modules/icons.js +26 -2
  331. package/ui/modules/repo-area-contention.js +97 -0
  332. package/ui/modules/router.js +2 -0
  333. package/ui/modules/session-api.js +158 -14
  334. package/ui/modules/session-insights-worker.js +28 -0
  335. package/ui/modules/session-insights.js +173 -4
  336. package/ui/modules/session-surface.js +221 -0
  337. package/ui/modules/settings-schema.js +148 -27
  338. package/ui/modules/state.js +122 -13
  339. package/ui/modules/streaming.js +196 -60
  340. package/ui/modules/structured-values.js +47 -0
  341. package/ui/modules/task-hierarchy.js +374 -0
  342. package/ui/modules/worktree-recovery.js +10 -1
  343. package/ui/setup.html +3327 -2538
  344. package/ui/styles/components.css +983 -99
  345. package/ui/styles/kanban.css +229 -0
  346. package/ui/styles/layout.css +578 -106
  347. package/ui/styles/toastui-editor-dark.css +1 -0
  348. package/ui/styles/toastui-editor-viewer.css +6 -0
  349. package/ui/styles/toastui-editor.css +6 -0
  350. package/ui/styles/variables.css +14 -2
  351. package/ui/styles/workspace-switcher.css +22 -0
  352. package/ui/styles.css +20 -5
  353. package/ui/tabs/agents.js +1222 -86
  354. package/ui/tabs/chat.js +588 -146
  355. package/ui/tabs/context-compression-lab.js +962 -0
  356. package/ui/tabs/control.js +347 -61
  357. package/ui/tabs/dashboard.js +372 -105
  358. package/ui/tabs/guardrails.js +1140 -0
  359. package/ui/tabs/infra.js +91 -12
  360. package/ui/tabs/integrations.js +388 -0
  361. package/ui/tabs/library.js +161 -21
  362. package/ui/tabs/logs.js +410 -52
  363. package/ui/tabs/manual-flows.js +268 -52
  364. package/ui/tabs/settings.js +2117 -105
  365. package/ui/tabs/tasks.js +2851 -303
  366. package/ui/tabs/telemetry.js +246 -8
  367. package/ui/tabs/workflow-canvas-utils.mjs +172 -15
  368. package/ui/tabs/workflows.js +2632 -348
  369. package/ui/tui/App.js +127 -119
  370. package/ui/tui/HelpScreen.js +201 -0
  371. package/ui/tui/SettingsScreen.js +388 -0
  372. package/ui/tui/TasksScreen.js +30 -7
  373. package/ui/tui/TelemetryScreen.js +155 -0
  374. package/ui/tui/WorkflowsScreen.js +350 -0
  375. package/ui/tui/config-events.js +13 -0
  376. package/ui/tui/constants.js +1 -1
  377. package/ui/tui/logs-screen-helpers.js +292 -0
  378. package/ui/tui/tasks-screen-helpers.js +52 -0
  379. package/ui/tui/telemetry-helpers.js +158 -0
  380. package/ui/tui/useWorkflows.js +126 -6
  381. package/ui/tui/workflows-screen-helpers.js +220 -0
  382. package/ui/vendor/preact-jsx-runtime.js +5 -0
  383. package/utils.mjs +2 -2
  384. package/voice/vision-session-state.mjs +257 -0
  385. package/voice/voice-action-dispatcher.mjs +57 -12
  386. package/voice/voice-agents-sdk.mjs +1 -1
  387. package/voice/voice-auth-manager.mjs +102 -123
  388. package/voice/voice-tool-definitions.mjs +7 -7
  389. package/voice/voice-tools.mjs +837 -101
  390. package/workflow/action-approval.mjs +415 -0
  391. package/workflow/approval-queue.mjs +1254 -0
  392. package/workflow/credential-store.mjs +553 -0
  393. package/workflow/cron-scheduler.mjs +512 -0
  394. package/workflow/declarative-workflows.mjs +21 -2
  395. package/workflow/delegation-runtime.mjs +557 -0
  396. package/workflow/execution-ledger.mjs +1317 -33
  397. package/workflow/harness-approval-node.mjs +237 -0
  398. package/workflow/harness-output-contract.mjs +86 -0
  399. package/workflow/harness-session-node.mjs +160 -0
  400. package/workflow/harness-subagent-node.mjs +483 -0
  401. package/workflow/harness-tool-node.mjs +176 -0
  402. package/workflow/heavy-runner-pool.mjs +546 -0
  403. package/workflow/manual-flows.mjs +969 -28
  404. package/workflow/mcp-discovery-proxy.mjs +363 -143
  405. package/workflow/mcp-registry.mjs +507 -65
  406. package/workflow/meeting-workflow-service.mjs +24 -12
  407. package/workflow/pipeline-workflows.mjs +44 -2
  408. package/workflow/pipeline.mjs +72 -28
  409. package/workflow/project-detection.mjs +31 -6
  410. package/workflow/research-evidence-sidecar.mjs +1246 -0
  411. package/workflow/run-evaluator.mjs +2155 -0
  412. package/workflow/workflow-cli.mjs +229 -2
  413. package/workflow/workflow-contract.mjs +130 -2
  414. package/workflow/workflow-engine.mjs +5636 -331
  415. package/workflow/workflow-migration.mjs +0 -1
  416. package/workflow/workflow-nodes/actions.mjs +15526 -0
  417. package/workflow/workflow-nodes/agent.mjs +1863 -0
  418. package/workflow/workflow-nodes/conditions.mjs +307 -0
  419. package/workflow/workflow-nodes/definitions.mjs +210 -29
  420. package/workflow/workflow-nodes/flow.mjs +749 -0
  421. package/workflow/workflow-nodes/loop.mjs +449 -0
  422. package/workflow/workflow-nodes/meetings.mjs +456 -0
  423. package/workflow/workflow-nodes/notifications.mjs +169 -0
  424. package/workflow/workflow-nodes/transforms.mjs +83 -30
  425. package/workflow/workflow-nodes/triggers.mjs +1405 -0
  426. package/workflow/workflow-nodes/validation.mjs +722 -0
  427. package/workflow/workflow-nodes.mjs +43 -14965
  428. package/workflow/workflow-serializer.mjs +294 -0
  429. package/workflow/workflow-templates.mjs +304 -9
  430. package/workflow-templates/_helpers.mjs +1 -3
  431. package/workflow-templates/agents.mjs +235 -27
  432. package/workflow-templates/bosun-native.mjs +3 -1
  433. package/workflow-templates/code-quality.mjs +1 -1
  434. package/workflow-templates/continuation-loop.mjs +21 -5
  435. package/workflow-templates/coverage.mjs +6 -2
  436. package/workflow-templates/github.mjs +2565 -177
  437. package/workflow-templates/reliability.mjs +463 -35
  438. package/workflow-templates/research-evidence.mjs +389 -0
  439. package/workflow-templates/security.mjs +75 -62
  440. package/workflow-templates/sub-workflows.mjs +11 -3
  441. package/workflow-templates/task-batch.mjs +102 -20
  442. package/workflow-templates/task-lifecycle.mjs +333 -298
  443. package/workspace/command-diagnostics.mjs +111 -0
  444. package/workspace/context-cache.mjs +1308 -124
  445. package/workspace/context-indexer.mjs +915 -9
  446. package/workspace/context-injector.mjs +144 -0
  447. package/workspace/execution-journal.mjs +255 -0
  448. package/workspace/scope-locks.mjs +481 -0
  449. package/workspace/shared-knowledge.mjs +496 -91
  450. package/workspace/shared-state-manager.mjs +344 -1
  451. package/workspace/shared-workspace-cli.mjs +0 -0
  452. package/workspace/shared-workspace-registry.mjs +2 -2
  453. package/workspace/skillbook-store.mjs +681 -0
  454. package/workspace/workspace-manager.mjs +14 -8
  455. package/workspace/workspace-monitor.mjs +4 -4
  456. package/workspace/worktree-manager.mjs +146 -55
  457. package/workspace/worktree-setup.mjs +822 -0
  458. package/agent/rotate-agent-logs.sh +0 -134
  459. package/git/sdk-conflict-resolver.mjs +0 -971
  460. package/infra/sync-engine.mjs +0 -1160
  461. package/kanban/ve-kanban.mjs +0 -664
  462. package/kanban/ve-kanban.ps1 +0 -1365
  463. package/kanban/ve-kanban.sh +0 -18
  464. package/kanban/ve-orchestrator.mjs +0 -340
  465. package/kanban/ve-orchestrator.ps1 +0 -6762
  466. package/kanban/ve-orchestrator.sh +0 -18
  467. package/kanban/vibe-kanban-wrapper.mjs +0 -41
  468. package/kanban/vk-error-resolver.mjs +0 -474
  469. package/kanban/vk-log-stream.mjs +0 -932
  470. package/task/task-archiver.mjs +0 -813
  471. package/tools/publish.mjs +0 -239
  472. package/ui/components/chat-view.js.bak +0 -1
  473. package/ui/tabs/infra.js.bak +0 -1
@@ -10,6 +10,7 @@ const _visionSessionState = new Map();
10
10
  const MAX_TRACE_TURNS = 12;
11
11
  const MAX_TURN_EVENTS = 40;
12
12
  const MAX_TURN_FINGERPRINTS = 32;
13
+ const MAX_MULTIMODAL_FALLBACK_HISTORY = 8;
13
14
  const SECRET_KEY_PATTERN = /(token|key|secret|password|authorization|credential|cookie|client_secret|access_token)/i;
14
15
 
15
16
  function getSessionKey(sessionId) {
@@ -20,6 +21,47 @@ function nowIso() {
20
21
  return new Date().toISOString();
21
22
  }
22
23
 
24
+ function cloneValue(value) {
25
+ if (value == null) return value;
26
+ return JSON.parse(JSON.stringify(value));
27
+ }
28
+
29
+ function uniqueStrings(values) {
30
+ return [...new Set(
31
+ (Array.isArray(values) ? values : [values])
32
+ .map((entry) => String(entry ?? "").trim())
33
+ .filter(Boolean),
34
+ )];
35
+ }
36
+
37
+ function buildProfileSlug(value, fallback = "session") {
38
+ const src = String(value || "").trim().toLowerCase();
39
+ let result = "";
40
+ for (const ch of src) {
41
+ if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") || ch === "." || ch === "_") {
42
+ result += ch;
43
+ } else if (result.length > 0 && result[result.length - 1] !== "-") {
44
+ result += "-";
45
+ }
46
+ }
47
+ while (result.endsWith("-")) result = result.slice(0, -1);
48
+ while (result.startsWith("-")) result = result.slice(1);
49
+ return result || fallback;
50
+ }
51
+
52
+ function normalizeText(value) {
53
+ return String(value ?? "").trim();
54
+ }
55
+
56
+ function normalizeBoolean(value, fallback = false) {
57
+ if (value === true || value === false) return value;
58
+ const normalized = normalizeText(value).toLowerCase();
59
+ if (!normalized) return fallback;
60
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
61
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
62
+ return fallback;
63
+ }
64
+
23
65
  function redactSecretLikeText(value) {
24
66
  const raw = String(value || "").trim();
25
67
  if (!raw) return raw;
@@ -128,6 +170,87 @@ function annotateTurnFromEvent(turn, event) {
128
170
  }
129
171
  }
130
172
 
173
+ function normalizeBrowserWorkerIsolation(input = {}, fallback = {}) {
174
+ const source = {
175
+ ...(fallback && typeof fallback === "object" ? fallback : {}),
176
+ ...(input && typeof input === "object" ? input : {}),
177
+ };
178
+ const sessionId = getSessionKey(source.sessionId || source.ownerSessionId || fallback?.sessionId);
179
+ const parentSessionId = getSessionKey(source.parentSessionId || fallback?.parentSessionId) || null;
180
+ const rootSessionId = getSessionKey(source.rootSessionId || fallback?.rootSessionId) || parentSessionId || sessionId || null;
181
+ const profileScope = normalizeText(source.profileScope || fallback?.profileScope || "isolated-subagent") || "isolated-subagent";
182
+ const profileId = normalizeText(
183
+ source.profileId
184
+ || fallback?.profileId
185
+ || `${buildProfileSlug(rootSessionId || "root", "root")}--${buildProfileSlug(sessionId || "session")}`,
186
+ ) || `${buildProfileSlug(rootSessionId || "root", "root")}--${buildProfileSlug(sessionId || "session")}`;
187
+ const multimodalFallback = normalizeMultimodalFallback(source.multimodalFallback, fallback?.multimodalFallback);
188
+ return {
189
+ workerId: normalizeText(source.workerId || fallback?.workerId || `browser-worker:${profileId}`) || `browser-worker:${profileId}`,
190
+ sessionId: sessionId || null,
191
+ ownerSessionId: sessionId || null,
192
+ parentSessionId,
193
+ rootSessionId,
194
+ profileId,
195
+ profileDir: normalizeText(source.profileDir || fallback?.profileDir || `.bosun/.cache/browser-workers/${profileId}`) || `.bosun/.cache/browser-workers/${profileId}`,
196
+ profileScope,
197
+ status: normalizeText(source.status || fallback?.status || "attached") || "attached",
198
+ requestedCapabilities: uniqueStrings(source.requestedCapabilities || fallback?.requestedCapabilities),
199
+ toolHints: uniqueStrings(source.toolHints || fallback?.toolHints),
200
+ multimodalFallback,
201
+ metadata: sanitizeTraceValue(source.metadata || fallback?.metadata || {}),
202
+ assignedAt: normalizeText(source.assignedAt || fallback?.assignedAt || "") || nowIso(),
203
+ updatedAt: nowIso(),
204
+ releasedAt: normalizeText(source.releasedAt || fallback?.releasedAt || "") || null,
205
+ };
206
+ }
207
+
208
+ function normalizeMultimodalFallback(input = {}, fallback = {}) {
209
+ const source = {
210
+ ...(fallback && typeof fallback === "object" ? fallback : {}),
211
+ ...(input && typeof input === "object" ? input : {}),
212
+ };
213
+ const history = Array.isArray(source.history)
214
+ ? source.history
215
+ .filter((entry) => entry && typeof entry === "object")
216
+ .map((entry) => ({
217
+ at: normalizeText(entry.at || entry.timestamp || "") || nowIso(),
218
+ reason: normalizeText(entry.reason || "") || null,
219
+ summary: normalizeText(entry.summary || entry.description || "") || null,
220
+ source: normalizeText(entry.source || "") || null,
221
+ }))
222
+ .slice(-MAX_MULTIMODAL_FALLBACK_HISTORY)
223
+ : [];
224
+ return {
225
+ enabled: normalizeBoolean(source.enabled, true),
226
+ mode: normalizeText(source.mode || "vision_summary_to_text") || "vision_summary_to_text",
227
+ available: normalizeBoolean(source.available, history.length > 0 || Boolean(normalizeText(source.summary || source.description || source.lastDescription))),
228
+ reason: normalizeText(source.reason || "") || null,
229
+ summary: normalizeText(source.summary || source.description || source.lastDescription || "") || null,
230
+ source: normalizeText(source.source || "") || null,
231
+ frameHash: normalizeText(source.frameHash || "") || null,
232
+ width: Number.isFinite(Number(source.width)) ? Number(source.width) : null,
233
+ height: Number.isFinite(Number(source.height)) ? Number(source.height) : null,
234
+ updatedAt: normalizeText(source.updatedAt || source.lastUpdatedAt || "") || null,
235
+ history,
236
+ };
237
+ }
238
+
239
+ function buildMultimodalFallbackDescription(state, fallback = {}) {
240
+ const summary = normalizeText(fallback.summary || state?.lastSummary || "");
241
+ const source = normalizeText(fallback.source || state?.lastFrameSource || "") || "screen";
242
+ const width = Number.isFinite(Number(fallback.width)) ? Number(fallback.width) : state?.lastFrameWidth;
243
+ const height = Number.isFinite(Number(fallback.height)) ? Number(fallback.height) : state?.lastFrameHeight;
244
+ const dimension = width && height ? ` (${width}x${height})` : "";
245
+ if (summary) {
246
+ return `[${source}${dimension}] ${summary}`;
247
+ }
248
+ if (state?.lastFrameHash) {
249
+ return `Visual context from ${source}${dimension} is available for text fallback.`;
250
+ }
251
+ return "";
252
+ }
253
+
131
254
  export function getVisionSessionState(sessionId) {
132
255
  const key = getSessionKey(sessionId);
133
256
  if (!key) return null;
@@ -143,6 +266,20 @@ export function getVisionSessionState(sessionId) {
143
266
  lastFrameSource: "screen",
144
267
  lastFrameWidth: null,
145
268
  lastFrameHeight: null,
269
+ browserWorker: null,
270
+ multimodalFallback: {
271
+ enabled: true,
272
+ mode: "vision_summary_to_text",
273
+ available: false,
274
+ reason: null,
275
+ summary: null,
276
+ source: null,
277
+ frameHash: null,
278
+ width: null,
279
+ height: null,
280
+ updatedAt: null,
281
+ history: [],
282
+ },
146
283
  voiceTurnTrace: null,
147
284
  });
148
285
  }
@@ -155,6 +292,126 @@ export function clearVisionSessionState(sessionId) {
155
292
  return _visionSessionState.delete(key);
156
293
  }
157
294
 
295
+ export function ensureBrowserWorkerIsolation(sessionId, options = {}) {
296
+ const state = getVisionSessionState(sessionId);
297
+ if (!state) return null;
298
+ state.browserWorker = normalizeBrowserWorkerIsolation({
299
+ sessionId,
300
+ ...options,
301
+ }, state.browserWorker || {});
302
+ if (!state.multimodalFallback || typeof state.multimodalFallback !== "object") {
303
+ state.multimodalFallback = normalizeMultimodalFallback();
304
+ }
305
+ if (options.multimodalFallback || state.browserWorker?.multimodalFallback) {
306
+ state.multimodalFallback = normalizeMultimodalFallback(
307
+ state.browserWorker?.multimodalFallback || options.multimodalFallback,
308
+ state.multimodalFallback,
309
+ );
310
+ }
311
+ return cloneValue(state.browserWorker);
312
+ }
313
+
314
+ export function getBrowserWorkerIsolation(sessionId) {
315
+ const state = getVisionSessionState(sessionId);
316
+ if (!state?.browserWorker) return null;
317
+ return cloneValue(state.browserWorker);
318
+ }
319
+
320
+ export function listBrowserWorkerIsolations(options = {}) {
321
+ const rootSessionId = getSessionKey(options.rootSessionId);
322
+ const parentSessionId = getSessionKey(options.parentSessionId);
323
+ return [..._visionSessionState.values()]
324
+ .map((state) => state?.browserWorker || null)
325
+ .filter(Boolean)
326
+ .filter((worker) => {
327
+ if (rootSessionId && getSessionKey(worker.rootSessionId) !== rootSessionId) return false;
328
+ if (parentSessionId && getSessionKey(worker.parentSessionId) !== parentSessionId) return false;
329
+ return true;
330
+ })
331
+ .map((worker) => cloneValue(worker));
332
+ }
333
+
334
+ export function releaseBrowserWorkerIsolation(sessionId, reason = "released") {
335
+ const state = getVisionSessionState(sessionId);
336
+ if (!state?.browserWorker) return null;
337
+ const released = normalizeBrowserWorkerIsolation({
338
+ ...state.browserWorker,
339
+ status: "released",
340
+ releasedAt: nowIso(),
341
+ metadata: {
342
+ ...(state.browserWorker.metadata && typeof state.browserWorker.metadata === "object" ? state.browserWorker.metadata : {}),
343
+ releaseReason: normalizeText(reason) || "released",
344
+ },
345
+ }, state.browserWorker);
346
+ state.browserWorker = null;
347
+ return cloneValue(released);
348
+ }
349
+
350
+ export function recordMultimodalFallback(sessionId, input = {}) {
351
+ const state = getVisionSessionState(sessionId);
352
+ if (!state) return null;
353
+ const description = buildMultimodalFallbackDescription(state, input);
354
+ const nextHistoryEntry = {
355
+ at: nowIso(),
356
+ reason: normalizeText(input.reason || "") || null,
357
+ summary: normalizeText(input.summary || input.description || description) || null,
358
+ source: normalizeText(input.source || state.lastFrameSource || "") || null,
359
+ };
360
+ const fallback = normalizeMultimodalFallback({
361
+ ...state.multimodalFallback,
362
+ ...input,
363
+ available: Boolean(description),
364
+ summary: description || normalizeText(input.summary || input.description || ""),
365
+ source: normalizeText(input.source || state.lastFrameSource || ""),
366
+ frameHash: normalizeText(input.frameHash || state.lastFrameHash || ""),
367
+ width: Number.isFinite(Number(input.width)) ? Number(input.width) : state.lastFrameWidth,
368
+ height: Number.isFinite(Number(input.height)) ? Number(input.height) : state.lastFrameHeight,
369
+ updatedAt: nowIso(),
370
+ history: [
371
+ ...(Array.isArray(state.multimodalFallback?.history) ? state.multimodalFallback.history : []),
372
+ nextHistoryEntry,
373
+ ].slice(-MAX_MULTIMODAL_FALLBACK_HISTORY),
374
+ }, state.multimodalFallback);
375
+ state.multimodalFallback = fallback;
376
+ if (state.browserWorker) {
377
+ state.browserWorker = normalizeBrowserWorkerIsolation({
378
+ ...state.browserWorker,
379
+ multimodalFallback: fallback,
380
+ }, state.browserWorker);
381
+ }
382
+ return cloneValue(fallback);
383
+ }
384
+
385
+ export function describeMultimodalFallback(sessionId, options = {}) {
386
+ const state = getVisionSessionState(sessionId);
387
+ if (!state) {
388
+ return {
389
+ sessionId: getSessionKey(sessionId),
390
+ available: false,
391
+ description: "",
392
+ browserWorker: null,
393
+ };
394
+ }
395
+ const fallback = normalizeMultimodalFallback(options, state.multimodalFallback);
396
+ const description = normalizeText(
397
+ options.description
398
+ || options.summary
399
+ || fallback.summary
400
+ || buildMultimodalFallbackDescription(state, fallback),
401
+ );
402
+ return {
403
+ sessionId: getSessionKey(sessionId),
404
+ available: Boolean(description),
405
+ description,
406
+ browserWorker: cloneValue(state.browserWorker),
407
+ fallback: cloneValue({
408
+ ...fallback,
409
+ summary: description || fallback.summary,
410
+ available: Boolean(description),
411
+ }),
412
+ };
413
+ }
414
+
158
415
  export function beginVoiceTurnTrace(sessionId, metadata = {}) {
159
416
  const state = getVisionSessionState(sessionId);
160
417
  if (!state) return null;
@@ -376,6 +376,7 @@ registerAction("agent.delegate", async (params, context) => {
376
376
  model,
377
377
  cwd,
378
378
  sessionId,
379
+ scope: `voice-dispatch:${sessionId}`,
379
380
  sessionType: "voice-dispatch",
380
381
  timeoutMs: 5 * 60 * 1000,
381
382
  });
@@ -554,19 +555,20 @@ registerAction("system.fleet", async () => {
554
555
  registerAction("system.config", async (params) => {
555
556
  const cfg = loadConfig();
556
557
  const key = String(params.key || "").trim();
557
- if (key) {
558
- const value = cfg[key];
559
- return value !== undefined ? { [key]: value } : { error: `Config key "${key}" not found.` };
560
- }
561
- return {
562
- primaryAgent: cfg.primaryAgent,
563
- mode: cfg.mode,
564
- kanbanBackend: cfg.kanbanBackend || cfg.kanban?.backend,
565
- projectName: cfg.projectName,
558
+ const normalizedConfig = {
559
+ primaryAgent: cfg.primaryAgent || getPrimaryAgentName(),
560
+ mode: cfg.mode || "generic",
561
+ kanbanBackend: cfg.kanbanBackend || cfg.kanban?.backend || "internal",
562
+ projectName: cfg.projectName || cfg.project || process.env.PROJECT_NAME || "unknown",
566
563
  autoFixEnabled: cfg.autoFixEnabled,
567
564
  watchEnabled: cfg.watchEnabled,
568
565
  voiceEnabled: cfg.voice?.enabled !== false,
569
566
  };
567
+ if (key) {
568
+ const value = normalizedConfig[key] ?? cfg[key];
569
+ return value !== undefined ? { [key]: value } : { error: `Config key "${key}" not found.` };
570
+ }
571
+ return normalizedConfig;
570
572
  });
571
573
 
572
574
  registerAction("system.health", async () => {
@@ -854,10 +856,54 @@ registerAction("workflow.retry", async (params) => {
854
856
  const currentRun = engine?.getRunDetail ? engine.getRunDetail(runId) : null;
855
857
  if (!currentRun) throw new Error(`Workflow run "${runId}" not found`);
856
858
  const currentStatus = String(currentRun?.status || "").trim().toLowerCase();
857
- if (mode === "from_failed" && currentStatus !== "failed") {
859
+ const retryOptions = typeof engine.getRetryOptions === "function"
860
+ ? engine.getRetryOptions(runId)
861
+ : null;
862
+ const safeInterruptedResume =
863
+ mode === "from_failed" &&
864
+ retryOptions?.guardedState?.code === "create_tasks_pending" &&
865
+ retryOptions?.guardedState?.safeResume === true &&
866
+ retryOptions?.recommendedMode === "from_failed";
867
+ const createTasksPendingGuard = retryOptions?.guardedState?.code === "create_tasks_pending";
868
+ if (mode === "from_failed" && currentStatus !== "failed" && !createTasksPendingGuard) {
858
869
  throw new Error(`retry mode "from_failed" requires a failed run (current=${currentRun?.status || "unknown"})`);
859
870
  }
860
- return engine.retryRun(runId, { mode });
871
+ const resolvedRetry = safeInterruptedResume
872
+ ? {
873
+ mode,
874
+ operatorAction: "resume",
875
+ decisionReason: retryOptions?.recommendedReason || "create_tasks_pending.resume_only",
876
+ blocked: false,
877
+ guardedState: retryOptions?.guardedState || null,
878
+ retryArgs: {
879
+ mode,
880
+ _resumeInterrupted: true,
881
+ ...(retryOptions?.recommendedReason
882
+ ? { _decisionReason: retryOptions.recommendedReason }
883
+ : {}),
884
+ },
885
+ }
886
+ : (
887
+ createTasksPendingGuard && typeof engine.resolveOperatorRetry === "function"
888
+ ? engine.resolveOperatorRetry(runId, mode)
889
+ : null
890
+ );
891
+ if (resolvedRetry?.blocked) {
892
+ throw new Error(resolvedRetry.blockedMessage || "Workflow retry is blocked for this run state.");
893
+ }
894
+ const retryArgs = resolvedRetry?.retryArgs || { mode };
895
+ if (safeInterruptedResume && !resolvedRetry?.retryArgs) {
896
+ retryArgs._resumeInterrupted = true;
897
+ if (retryOptions?.recommendedReason) retryArgs._decisionReason = retryOptions.recommendedReason;
898
+ }
899
+ const result = await engine.retryRun(runId, retryArgs);
900
+ return {
901
+ ...result,
902
+ operatorAction:
903
+ resolvedRetry?.operatorAction || (mode === "from_scratch" ? "restart" : "retry"),
904
+ decisionReason: resolvedRetry?.decisionReason || null,
905
+ guardedState: resolvedRetry?.guardedState || retryOptions?.guardedState || null,
906
+ };
861
907
  });
862
908
 
863
909
  // ── Skill/prompt actions ────────────────────────────────────────────────────
@@ -1241,4 +1287,3 @@ export function getVoiceActionPromptSection() {
1241
1287
  lines.push("");
1242
1288
  return lines.join("\n");
1243
1289
  }
1244
-
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { loadConfig } from "../config/config.mjs";
20
- import { resolveVoiceOAuthToken } from "./voice-auth-manager.mjs";
20
+ import { resolveSharedOAuthToken as resolveVoiceOAuthToken } from "../agent/provider-auth-state.mjs";
21
21
 
22
22
  // ── Module-scope lazy imports ───────────────────────────────────────────────
23
23
 
@@ -1,15 +1,15 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { exec as childExec } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
3
  import { createServer } from "node:http";
5
- import { homedir } from "node:os";
6
- import { dirname, join } from "node:path";
7
-
8
- const VOICE_AUTH_STATE_PATH = join(homedir(), ".bosun", "voice-auth-state.json");
9
- const STATE_TTL_MS = 15_000;
10
-
11
- let _cachedState = null;
12
- let _cachedStateAt = 0;
4
+ import { CredentialStore } from "../workflow/credential-store.mjs";
5
+ import {
6
+ clearSharedOAuthToken,
7
+ getProviderAuthStatePath,
8
+ hasSharedOAuthToken,
9
+ readProviderAuthState,
10
+ resolveSharedOAuthToken,
11
+ saveSharedOAuthToken,
12
+ } from "../agent/provider-auth-state.mjs";
13
13
 
14
14
  function normalizeProvider(provider) {
15
15
  return String(provider || "").trim().toLowerCase();
@@ -32,27 +32,6 @@ function escapeHtml(value) {
32
32
  .replace(/'/g, '&#39;');
33
33
  }
34
34
 
35
- function readStateFile() {
36
- if (!existsSync(VOICE_AUTH_STATE_PATH)) return {};
37
- const raw = readFileSync(VOICE_AUTH_STATE_PATH, "utf8");
38
- const parsed = JSON.parse(raw);
39
- if (!parsed || typeof parsed !== "object") return {};
40
- return parsed;
41
- }
42
-
43
- function getCachedState(forceReload = false) {
44
- const isFresh = !forceReload && _cachedState && Date.now() - _cachedStateAt < STATE_TTL_MS;
45
- if (isFresh) return _cachedState;
46
-
47
- try {
48
- _cachedState = readStateFile();
49
- } catch {
50
- _cachedState = {};
51
- }
52
- _cachedStateAt = Date.now();
53
- return _cachedState;
54
- }
55
-
56
35
  function isExpired(expiresAt) {
57
36
  if (!expiresAt) return false;
58
37
  const ts = Number(new Date(expiresAt).getTime());
@@ -60,6 +39,19 @@ function isExpired(expiresAt) {
60
39
  return ts <= Date.now() + 30_000;
61
40
  }
62
41
 
42
+ function resolveCredentialStore(options = {}) {
43
+ if (options.credentialStore) return options.credentialStore;
44
+ if (!options.configDir) return null;
45
+ try {
46
+ return new CredentialStore({
47
+ configDir: options.configDir,
48
+ secretKey: options.secretKey,
49
+ });
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
63
55
  function getProviderEnvCandidates(provider) {
64
56
  switch (provider) {
65
57
  case "openai":
@@ -89,98 +81,22 @@ function getProviderEnvCandidates(provider) {
89
81
  }
90
82
  }
91
83
 
92
- function getStateTokenCandidates(provider, state) {
93
- const byProvider = state?.providers?.[provider] || state?.[provider] || {};
94
- return [
95
- {
96
- token: byProvider?.accessToken,
97
- expiresAt: byProvider?.expiresAt,
98
- source: "state",
99
- },
100
- {
101
- token: byProvider?.access_token,
102
- expiresAt: byProvider?.expires_at,
103
- source: "state",
104
- },
105
- ];
106
- }
107
-
108
84
  export function resolveVoiceOAuthToken(provider, forceReload = false) {
109
85
  const normalizedProvider = normalizeProvider(provider);
110
86
  if (!normalizedProvider) return null;
111
-
112
- const envToken = getProviderEnvCandidates(normalizedProvider)
113
- .map((token) => normalizeEnvValue(token))
114
- .find(Boolean);
115
- if (envToken) {
116
- return {
117
- token: envToken,
118
- source: "env",
119
- provider: normalizedProvider,
120
- };
121
- }
122
-
123
- const state = getCachedState(forceReload);
124
- const candidates = getStateTokenCandidates(normalizedProvider, state);
125
- for (const candidate of candidates) {
126
- const token = String(candidate?.token || "").trim();
127
- if (!token) continue;
128
- if (isExpired(candidate?.expiresAt)) continue;
129
- return {
130
- token,
131
- source: candidate.source,
132
- provider: normalizedProvider,
133
- expiresAt: candidate?.expiresAt || null,
134
- };
135
- }
136
-
137
- return null;
87
+ return resolveSharedOAuthToken(normalizedProvider, forceReload);
138
88
  }
139
89
 
140
90
  export function hasVoiceOAuthToken(provider, forceReload = false) {
141
- return Boolean(resolveVoiceOAuthToken(provider, forceReload));
91
+ return hasSharedOAuthToken(provider, forceReload);
142
92
  }
143
93
 
144
94
  export function saveVoiceOAuthToken(provider, payload = {}) {
145
- const normalizedProvider = normalizeProvider(provider);
146
- if (!normalizedProvider) {
147
- throw new Error("provider is required");
148
- }
149
-
150
- const token = String(payload?.accessToken || payload?.access_token || "").trim();
151
- if (!token) {
152
- throw new Error("access token is required");
153
- }
154
-
155
- const current = getCachedState(true);
156
- const next = {
157
- ...current,
158
- providers: {
159
- ...(current?.providers || {}),
160
- [normalizedProvider]: {
161
- accessToken: token,
162
- expiresAt: payload?.expiresAt || payload?.expires_at || null,
163
- refreshToken: payload?.refreshToken || payload?.refresh_token || null,
164
- tokenType: payload?.tokenType || payload?.token_type || "Bearer",
165
- updatedAt: new Date().toISOString(),
166
- },
167
- },
168
- };
169
-
170
- mkdirSync(dirname(VOICE_AUTH_STATE_PATH), { recursive: true });
171
- writeFileSync(VOICE_AUTH_STATE_PATH, JSON.stringify(next, null, 2));
172
- _cachedState = next;
173
- _cachedStateAt = Date.now();
174
-
175
- return {
176
- ok: true,
177
- path: VOICE_AUTH_STATE_PATH,
178
- provider: normalizedProvider,
179
- };
95
+ return saveSharedOAuthToken(provider, payload);
180
96
  }
181
97
 
182
98
  export function getVoiceAuthStatePath() {
183
- return VOICE_AUTH_STATE_PATH;
99
+ return getProviderAuthStatePath();
184
100
  }
185
101
 
186
102
  // ── Generic OAuth PKCE provider registry ─────────────────────────────────────
@@ -286,6 +202,80 @@ const OAUTH_PROVIDERS = {
286
202
  },
287
203
  };
288
204
 
205
+ function resolveVoiceProviderCredentialRecord(provider, options = {}) {
206
+ const credentialStore = resolveCredentialStore(options);
207
+ if (!credentialStore) return { record: null, validation: null };
208
+ const candidates = credentialStore.listByProvider(provider);
209
+ const record = candidates.find((entry) => {
210
+ const authMode = normalizeProvider(entry?.lifecycle?.authMode || entry?.metadata?.authMode || "oauth");
211
+ return !authMode || authMode === "oauth";
212
+ }) || null;
213
+ if (!record) return { record: null, validation: null };
214
+ return {
215
+ record,
216
+ validation: credentialStore.validate(record.name, {
217
+ env: options.env || process.env,
218
+ config: options.config || null,
219
+ workflowId: options.workflowId,
220
+ }),
221
+ };
222
+ }
223
+
224
+ export function describeVoiceProviderLifecycle(provider, options = {}) {
225
+ const normalizedProvider = normalizeProvider(provider);
226
+ const cfg = OAUTH_PROVIDERS[normalizedProvider];
227
+ if (!cfg) {
228
+ return {
229
+ provider: normalizedProvider,
230
+ status: "unknown",
231
+ missingActions: [],
232
+ envKeys: [],
233
+ refreshable: false,
234
+ };
235
+ }
236
+
237
+ const token = resolveVoiceOAuthToken(normalizedProvider, options.forceReload === true);
238
+ const loginStatus = _providerPendingLogin.get(normalizedProvider)?.status || "idle";
239
+ const clientIdConfigured = normalizeEnvValue(cfg.clientId).length >= 8;
240
+ const { record, validation } = resolveVoiceProviderCredentialRecord(normalizedProvider, options);
241
+ const refreshable = Boolean(token?.refreshToken || validation?.refreshable);
242
+ const missingActions = [];
243
+
244
+ if (!clientIdConfigured) missingActions.push("configure_client");
245
+ if (!token?.token && !record?.name) missingActions.push("sign_in");
246
+ if (token?.expiresAt && isExpired(token.expiresAt) && refreshable) missingActions.push("refresh_token");
247
+
248
+ return {
249
+ provider: normalizedProvider,
250
+ status: token?.token
251
+ ? (isExpired(token.expiresAt) ? "expired" : "connected")
252
+ : (loginStatus === "pending" ? "pending" : "idle"),
253
+ hasToken: Boolean(token?.token),
254
+ expiresAt: token?.expiresAt || validation?.expiresAt || null,
255
+ refreshable,
256
+ connectedSource: token?.source || (record?.name ? "credential-store" : null),
257
+ pendingStatus: loginStatus,
258
+ clientIdConfigured,
259
+ requiredEnvKeys: cfg.clientSecret
260
+ ? [`BOSUN_${normalizedProvider.toUpperCase()}_OAUTH_CLIENT_ID`, `BOSUN_${normalizedProvider.toUpperCase()}_OAUTH_CLIENT_SECRET`]
261
+ : [`BOSUN_${normalizedProvider.toUpperCase()}_OAUTH_CLIENT_ID`],
262
+ missingActions,
263
+ credentialName: record?.name || null,
264
+ validationErrors: validation?.errors || [],
265
+ };
266
+ }
267
+
268
+ export function validateVoiceProviderLifecycle(provider, options = {}) {
269
+ const lifecycle = describeVoiceProviderLifecycle(provider, options);
270
+ return {
271
+ ok: lifecycle.missingActions.length === 0 && lifecycle.validationErrors.length === 0,
272
+ status: lifecycle.status,
273
+ missingActions: lifecycle.missingActions,
274
+ validationErrors: lifecycle.validationErrors,
275
+ refreshable: lifecycle.refreshable,
276
+ };
277
+ }
278
+
289
279
  // Module-scope per-provider pending login state (never inside a function — hard rule).
290
280
  const _providerPendingLogin = new Map();
291
281
 
@@ -498,25 +488,14 @@ function _cancelProviderLogin(provider) {
498
488
  }
499
489
 
500
490
  function _logoutProvider(provider) {
501
- const curr = getCachedState(true);
502
- if (!curr?.providers?.[provider]) return { ok: true, wasLoggedIn: false };
503
- const next = {
504
- ...curr,
505
- providers: { ...(curr.providers || {}) },
506
- };
507
- delete next.providers[provider];
508
- mkdirSync(dirname(VOICE_AUTH_STATE_PATH), { recursive: true });
509
- writeFileSync(VOICE_AUTH_STATE_PATH, JSON.stringify(next, null, 2));
510
- _cachedState = next;
511
- _cachedStateAt = Date.now();
512
- return { ok: true, wasLoggedIn: true };
491
+ return clearSharedOAuthToken(provider);
513
492
  }
514
493
 
515
494
  async function _refreshProviderToken(provider) {
516
495
  const cfg = OAUTH_PROVIDERS[provider];
517
496
  if (!cfg) throw new Error(`Unknown OAuth provider: ${provider}`);
518
497
 
519
- const state = getCachedState(true);
498
+ const state = readProviderAuthState(true);
520
499
  const providerData = state?.providers?.[provider] || {};
521
500
  const refreshToken = providerData.refreshToken || providerData.refresh_token;
522
501
  if (!refreshToken) throw new Error(`No refresh token stored for ${provider}`);