@polderlabs/bizar 3.23.1 → 4.2.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 (361) hide show
  1. package/README.md +83 -16
  2. package/bizar-dash/.bizar/activity.log +3 -0
  3. package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
  4. package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
  5. package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
  6. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
  7. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
  8. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
  9. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
  10. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
  11. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
  12. package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
  13. package/bizar-dash/.bizar/graph/graph.html +307 -0
  14. package/bizar-dash/.bizar/graph/graph.json +40267 -0
  15. package/bizar-dash/.bizar/graph/manifest.json +762 -0
  16. package/bizar-dash/.graphifyignore +40 -0
  17. package/bizar-dash/.obsidian/README.md +14 -0
  18. package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
  19. package/bizar-dash/.obsidian/vault.json +7 -0
  20. package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
  21. package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
  22. package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
  23. package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
  24. package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
  25. package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
  26. package/bizar-dash/CHANGELOG.md +474 -0
  27. package/bizar-dash/DESIGN.md +136 -0
  28. package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
  29. package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
  30. package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
  31. package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
  32. package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
  33. package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
  34. package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
  35. package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
  36. package/bizar-dash/dist/index.html +40 -0
  37. package/bizar-dash/dist/mobile.html +22 -0
  38. package/bizar-dash/plans/debug-plan/comments.json +1 -0
  39. package/bizar-dash/plans/debug-plan/meta.json +10 -0
  40. package/bizar-dash/plans/debug-plan/plan.json +174 -0
  41. package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
  42. package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
  43. package/bizar-dash/src/cli.mjs +522 -0
  44. package/bizar-dash/src/server/activity-log.mjs +174 -0
  45. package/bizar-dash/src/server/agents-store.mjs +465 -0
  46. package/bizar-dash/src/server/api.mjs +138 -0
  47. package/bizar-dash/src/server/artifact-template.html +810 -0
  48. package/bizar-dash/src/server/artifacts-store.mjs +733 -0
  49. package/bizar-dash/src/server/auth.mjs +381 -0
  50. package/bizar-dash/src/server/background-store.mjs +447 -0
  51. package/bizar-dash/src/server/bg-poller.mjs +305 -0
  52. package/bizar-dash/src/server/bg-retry.mjs +574 -0
  53. package/bizar-dash/src/server/browser.mjs +40 -0
  54. package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
  55. package/bizar-dash/src/server/dialog-poller.mjs +105 -0
  56. package/bizar-dash/src/server/dialog-store.mjs +177 -0
  57. package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
  58. package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
  59. package/bizar-dash/src/server/memory-git.mjs +329 -0
  60. package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
  61. package/bizar-dash/src/server/memory-schema.mjs +145 -0
  62. package/bizar-dash/src/server/memory-secrets.mjs +84 -0
  63. package/bizar-dash/src/server/memory-store.mjs +471 -0
  64. package/bizar-dash/src/server/mod-security.mjs +398 -0
  65. package/bizar-dash/src/server/mods-loader.mjs +1067 -0
  66. package/bizar-dash/src/server/notifications-store.mjs +243 -0
  67. package/bizar-dash/src/server/obsidian-store.mjs +318 -0
  68. package/bizar-dash/src/server/opencode-runner.mjs +279 -0
  69. package/bizar-dash/src/server/pair-store.mjs +138 -0
  70. package/bizar-dash/src/server/projects-store.mjs +364 -0
  71. package/bizar-dash/src/server/providers-store.mjs +789 -0
  72. package/bizar-dash/src/server/routes/_shared.mjs +415 -0
  73. package/bizar-dash/src/server/routes/activity.mjs +110 -0
  74. package/bizar-dash/src/server/routes/agents.mjs +134 -0
  75. package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
  76. package/bizar-dash/src/server/routes/auth.mjs +69 -0
  77. package/bizar-dash/src/server/routes/background.mjs +165 -0
  78. package/bizar-dash/src/server/routes/chat.mjs +485 -0
  79. package/bizar-dash/src/server/routes/config.mjs +129 -0
  80. package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
  81. package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
  82. package/bizar-dash/src/server/routes/fs.mjs +429 -0
  83. package/bizar-dash/src/server/routes/history.mjs +78 -0
  84. package/bizar-dash/src/server/routes/memory.mjs +658 -0
  85. package/bizar-dash/src/server/routes/misc.mjs +89 -0
  86. package/bizar-dash/src/server/routes/mods.mjs +359 -0
  87. package/bizar-dash/src/server/routes/notifications.mjs +54 -0
  88. package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
  89. package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
  90. package/bizar-dash/src/server/routes/overview.mjs +91 -0
  91. package/bizar-dash/src/server/routes/pair.mjs +67 -0
  92. package/bizar-dash/src/server/routes/projects.mjs +175 -0
  93. package/bizar-dash/src/server/routes/providers.mjs +47 -0
  94. package/bizar-dash/src/server/routes/schedules.mjs +104 -0
  95. package/bizar-dash/src/server/routes/settings.mjs +51 -0
  96. package/bizar-dash/src/server/routes/skills.mjs +76 -0
  97. package/bizar-dash/src/server/routes/tasks.mjs +468 -0
  98. package/bizar-dash/src/server/routes/themes.mjs +51 -0
  99. package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
  100. package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
  101. package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
  102. package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
  103. package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
  104. package/bizar-dash/src/server/schedules-runner.mjs +369 -0
  105. package/bizar-dash/src/server/schedules-store.mjs +268 -0
  106. package/bizar-dash/src/server/search-store.mjs +220 -0
  107. package/bizar-dash/src/server/serve-info.mjs +684 -0
  108. package/bizar-dash/src/server/server.mjs +796 -0
  109. package/bizar-dash/src/server/settings-store.mjs +45 -0
  110. package/bizar-dash/src/server/skills-store.mjs +300 -0
  111. package/bizar-dash/src/server/state.mjs +437 -0
  112. package/bizar-dash/src/server/tailscale-store.mjs +113 -0
  113. package/bizar-dash/src/server/task-delegator.mjs +824 -0
  114. package/bizar-dash/src/server/tasks-store.mjs +508 -0
  115. package/bizar-dash/src/server/tui.mjs +923 -0
  116. package/bizar-dash/src/server/update-store.mjs +168 -0
  117. package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
  118. package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
  119. package/bizar-dash/src/server/watcher.mjs +81 -0
  120. package/bizar-dash/src/server/yaml.mjs +238 -0
  121. package/bizar-dash/src/web/App.tsx +718 -0
  122. package/bizar-dash/src/web/MobileApp.tsx +408 -0
  123. package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
  124. package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
  125. package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
  126. package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
  127. package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
  128. package/bizar-dash/src/web/components/Button.tsx +56 -0
  129. package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
  130. package/bizar-dash/src/web/components/Card.tsx +40 -0
  131. package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
  132. package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
  133. package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
  134. package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
  135. package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
  136. package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
  137. package/bizar-dash/src/web/components/Modal.tsx +215 -0
  138. package/bizar-dash/src/web/components/Notifications.tsx +241 -0
  139. package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
  140. package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
  141. package/bizar-dash/src/web/components/Spinner.tsx +19 -0
  142. package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
  143. package/bizar-dash/src/web/components/Tag.tsx +28 -0
  144. package/bizar-dash/src/web/components/Toast.tsx +150 -0
  145. package/bizar-dash/src/web/components/Topbar.tsx +368 -0
  146. package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
  147. package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
  148. package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
  149. package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
  150. package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
  151. package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
  152. package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
  153. package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
  154. package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
  155. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
  156. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
  157. package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
  158. package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
  159. package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
  160. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
  161. package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
  162. package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
  163. package/bizar-dash/src/web/components/chat/index.ts +20 -0
  164. package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
  165. package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
  166. package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
  167. package/bizar-dash/src/web/index.html +37 -0
  168. package/bizar-dash/src/web/lib/api.ts +193 -0
  169. package/bizar-dash/src/web/lib/markdown.tsx +59 -0
  170. package/bizar-dash/src/web/lib/types.ts +668 -0
  171. package/bizar-dash/src/web/lib/utils.ts +114 -0
  172. package/bizar-dash/src/web/lib/ws.ts +176 -0
  173. package/bizar-dash/src/web/main.tsx +34 -0
  174. package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
  175. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
  176. package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
  177. package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
  178. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
  179. package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
  180. package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
  181. package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
  182. package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
  183. package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
  184. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
  185. package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
  186. package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
  187. package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
  188. package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
  189. package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
  190. package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
  191. package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
  192. package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
  193. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
  194. package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
  195. package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
  196. package/bizar-dash/src/web/mobile.html +20 -0
  197. package/bizar-dash/src/web/mobile.tsx +13 -0
  198. package/bizar-dash/src/web/styles/chat.css +198 -0
  199. package/bizar-dash/src/web/styles/glyphs.css +960 -0
  200. package/bizar-dash/src/web/styles/main.css +8855 -0
  201. package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
  202. package/bizar-dash/src/web/styles/mobile.css +1960 -0
  203. package/bizar-dash/src/web/views/Activity.tsx +1203 -0
  204. package/bizar-dash/src/web/views/Agents.tsx +701 -0
  205. package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
  206. package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
  207. package/bizar-dash/src/web/views/Chat.tsx +190 -0
  208. package/bizar-dash/src/web/views/Config.tsx +2063 -0
  209. package/bizar-dash/src/web/views/History.tsx +335 -0
  210. package/bizar-dash/src/web/views/ModView.tsx +207 -0
  211. package/bizar-dash/src/web/views/Mods.tsx +693 -0
  212. package/bizar-dash/src/web/views/Overview.tsx +812 -0
  213. package/bizar-dash/src/web/views/Providers.tsx +302 -0
  214. package/bizar-dash/src/web/views/Schedules.tsx +802 -0
  215. package/bizar-dash/src/web/views/Settings.tsx +1787 -0
  216. package/bizar-dash/src/web/views/Skills.tsx +431 -0
  217. package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
  218. package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
  219. package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
  220. package/bizar-dash/templates/mod/FORMAT.md +76 -0
  221. package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
  222. package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
  223. package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
  224. package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
  225. package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
  226. package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  227. package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
  228. package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
  229. package/bizar-dash/tests/memory-cli.test.mjs +542 -0
  230. package/bizar-dash/tests/memory-config.test.mjs +422 -0
  231. package/bizar-dash/tests/memory-git.test.mjs +246 -0
  232. package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
  233. package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
  234. package/bizar-dash/tests/memory-schema.test.mjs +198 -0
  235. package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
  236. package/bizar-dash/tests/memory-store.test.mjs +390 -0
  237. package/bizar-dash/tests/memory-sync.test.mjs +141 -0
  238. package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
  239. package/bizar-dash/tests/mod-security.test.mjs +273 -0
  240. package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
  241. package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
  242. package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
  243. package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
  244. package/bizar-dash/tests/path-safe.test.mjs +108 -0
  245. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
  246. package/bizar-dash/tests/smoke-v2.mjs +212 -0
  247. package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
  248. package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
  249. package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
  250. package/bizar-dash/tests/yaml.test.mjs +128 -0
  251. package/bizar-dash/tsconfig.json +23 -0
  252. package/bizar-dash/vite.config.ts +27 -0
  253. package/cli/atomic.mjs +73 -0
  254. package/cli/banner.mjs +1 -1
  255. package/cli/bin.mjs +44 -47
  256. package/cli/bootstrap.mjs +1 -1
  257. package/cli/copy.mjs +25 -16
  258. package/cli/dev-link.mjs +17 -1
  259. package/cli/doctor.mjs +4 -4
  260. package/cli/doctor.test.mjs +2 -2
  261. package/cli/init.mjs +135 -1
  262. package/cli/install.mjs +31 -39
  263. package/cli/memory-constants.mjs +59 -0
  264. package/cli/memory.mjs +1436 -0
  265. package/cli/service.mjs +5 -6
  266. package/cli/utils.mjs +6 -3
  267. package/config/AGENTS.md +7 -7
  268. package/config/agents/_shared/AGENT_BASELINE.md +59 -61
  269. package/config/opencode.json +13 -38
  270. package/config/skills/memory-protocol/SKILL.md +105 -0
  271. package/config/skills/obsidian/SKILL.md +270 -65
  272. package/install.sh +59 -19
  273. package/package.json +49 -14
  274. package/packages/sdk/LICENSE +21 -0
  275. package/packages/sdk/README.md +80 -0
  276. package/packages/sdk/src/client.ts +183 -0
  277. package/packages/sdk/src/errors.ts +120 -0
  278. package/packages/sdk/src/events.ts +153 -0
  279. package/packages/sdk/src/index.ts +63 -0
  280. package/packages/sdk/src/types.ts +176 -0
  281. package/packages/sdk/src/version.ts +4 -0
  282. package/packages/sdk/tests/client.test.ts +217 -0
  283. package/packages/sdk/tests/errors.test.ts +108 -0
  284. package/packages/sdk/tests/events.test.ts +139 -0
  285. package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
  286. package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
  287. package/packages/sdk/tsconfig.json +25 -0
  288. package/packages/sdk/vitest.config.ts +9 -0
  289. package/plugins/bizar/LICENSE +21 -0
  290. package/plugins/bizar/README.md +448 -0
  291. package/plugins/bizar/dist/index.js +29901 -0
  292. package/plugins/bizar/index.ts +1491 -0
  293. package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
  294. package/plugins/bizar/src/background-state.ts +551 -0
  295. package/plugins/bizar/src/background.ts +1250 -0
  296. package/plugins/bizar/src/commands-impl.ts +464 -0
  297. package/plugins/bizar/src/commands.ts +1168 -0
  298. package/plugins/bizar/src/dashboard-client.ts +233 -0
  299. package/plugins/bizar/src/event-stream.ts +606 -0
  300. package/plugins/bizar/src/fingerprint.ts +120 -0
  301. package/plugins/bizar/src/handoff.ts +79 -0
  302. package/plugins/bizar/src/http-client.ts +467 -0
  303. package/plugins/bizar/src/key-rotation.ts +218 -0
  304. package/plugins/bizar/src/logger.ts +144 -0
  305. package/plugins/bizar/src/loop.ts +176 -0
  306. package/plugins/bizar/src/opencode-runner.ts +390 -0
  307. package/plugins/bizar/src/options.ts +421 -0
  308. package/plugins/bizar/src/plan-fs.ts +323 -0
  309. package/plugins/bizar/src/reasoning-clean.ts +454 -0
  310. package/plugins/bizar/src/report.ts +178 -0
  311. package/plugins/bizar/src/research-prompt.ts +35 -0
  312. package/plugins/bizar/src/serve-info.ts +228 -0
  313. package/plugins/bizar/src/serve.ts +496 -0
  314. package/plugins/bizar/src/settings.ts +349 -0
  315. package/plugins/bizar/src/state.ts +298 -0
  316. package/plugins/bizar/src/tools/bg-collect.ts +104 -0
  317. package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
  318. package/plugins/bizar/src/tools/bg-kill.ts +87 -0
  319. package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
  320. package/plugins/bizar/src/tools/bg-status.ts +99 -0
  321. package/plugins/bizar/src/tools/open-kb.ts +191 -0
  322. package/plugins/bizar/src/tools/plan-action.ts +767 -0
  323. package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
  324. package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
  325. package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
  326. package/plugins/bizar/tests/background-state.test.ts +277 -0
  327. package/plugins/bizar/tests/background.test.ts +402 -0
  328. package/plugins/bizar/tests/block.test.ts +195 -0
  329. package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
  330. package/plugins/bizar/tests/commands-impl.test.ts +442 -0
  331. package/plugins/bizar/tests/commands.test.ts +584 -0
  332. package/plugins/bizar/tests/config.test.ts +122 -0
  333. package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
  334. package/plugins/bizar/tests/dispose.test.ts +336 -0
  335. package/plugins/bizar/tests/event-stream.test.ts +409 -0
  336. package/plugins/bizar/tests/event.test.ts +262 -0
  337. package/plugins/bizar/tests/fingerprint.test.ts +162 -0
  338. package/plugins/bizar/tests/http-client.test.ts +404 -0
  339. package/plugins/bizar/tests/init-helpers.test.ts +203 -0
  340. package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
  341. package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
  342. package/plugins/bizar/tests/key-rotation.test.ts +396 -0
  343. package/plugins/bizar/tests/loop.test.ts +397 -0
  344. package/plugins/bizar/tests/options.test.ts +276 -0
  345. package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
  346. package/plugins/bizar/tests/serve.test.ts +339 -0
  347. package/plugins/bizar/tests/settings.test.ts +351 -0
  348. package/plugins/bizar/tests/stall-think.test.ts +750 -0
  349. package/plugins/bizar/tests/state.test.ts +276 -0
  350. package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
  351. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
  352. package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
  353. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
  354. package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
  355. package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
  356. package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
  357. package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
  358. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
  359. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
  360. package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
  361. package/plugins/bizar/tsconfig.json +29 -0
@@ -0,0 +1,824 @@
1
+ /**
2
+ * src/server/task-delegator.mjs
3
+ *
4
+ * v3.5.2 — Odin task delegation.
5
+ *
6
+ * Accepts a single "natural language" task from the user. Splits it into
7
+ * 1-5 subtasks via heuristic rules (an LLM call would replace this in
8
+ * v3.3), assigns each subtask to the best-fit agent based on tags /
9
+ * keywords / category, and dispatches them to the background agent
10
+ * infrastructure when available.
11
+ *
12
+ * v3.5.1 — agents.maxParallel cap applied here. Background dispatch is
13
+ * limited to the configured number of concurrent instances; overflow
14
+ * subtasks are marked queued and dispatched as slots free up.
15
+ *
16
+ * The delegator is intentionally tolerant: a failure to enqueue a
17
+ * background instance must NOT break the user-facing task creation.
18
+ * Subtasks are always persisted to the regular tasks store first; the
19
+ * background dispatch is best-effort.
20
+ *
21
+ * v3.5.4 (bug: dispatch stuck) — Dispatch path replaced. The previous
22
+ * implementation shell-executed `node plugins/bizar/dist/cli.js bg enqueue`,
23
+ * but the plugin is a Bun-native TS project with no compiled `dist/`,
24
+ * so the exec always failed. The silent try/catch around execSync meant
25
+ * the failure was invisible to both the user and the dashboard log.
26
+ *
27
+ * New path:
28
+ * 1. Read the plugin's serve-info file (port + password + worktree).
29
+ * 2. POST /api/session on the plugin's opencode serve child.
30
+ * 3. POST /api/session/{id}/prompt to fire the task prompt.
31
+ * 4. Write a state file under ~/.cache/bizar/bg/ so the dashboard's
32
+ * background-store can list and kill it.
33
+ *
34
+ * If serve-info is missing (plugin not running) we log a clear warning
35
+ * at startup AND surface the dispatch failure in the `/submit` response,
36
+ * so the user can re-dispatch manually with `/api/tasks/:id/start`
37
+ * after the plugin is up.
38
+ */
39
+
40
+ import { tasksStore } from './tasks-store.mjs';
41
+ import { agentsStore } from './agents-store.mjs';
42
+ import { notificationsStore } from './notifications-store.mjs';
43
+ import { backgroundStore } from './background-store.mjs';
44
+ import { getActualBgLogPath } from './lib/path-safe.mjs';
45
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, renameSync } from 'node:fs';
46
+ import { join } from 'node:path';
47
+ import { homedir } from 'node:os';
48
+ import { randomBytes } from 'node:crypto';
49
+
50
+ const HOME = homedir();
51
+
52
+ // Background state directory — defaults to what the opencode plugin
53
+ // actually uses (see plugins/bizar/src/background-state.ts: stateDir
54
+ // defaults to `~/.cache/bizar` so instances live at
55
+ // `~/.cache/bizar/bg/<instanceId>.json`).
56
+ const BG_DIRS = [
57
+ join(HOME, '.cache', 'bizar', 'bg'),
58
+ join(HOME, '.config', 'opencode', 'bg'),
59
+ join(HOME, '.bizar', 'bg'),
60
+ ];
61
+
62
+ function genId(prefix) {
63
+ return `${prefix}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
64
+ }
65
+
66
+ function genShortHex(bytes = 6) {
67
+ return randomBytes(bytes).toString('hex');
68
+ }
69
+
70
+ /**
71
+ * v3.5.4 — Synthesize the prompt text for a subtask. Combines the
72
+ * subtask title and description in the same shape the plugin's own
73
+ * `bizar_spawn_background` tool uses internally, so an opencode agent
74
+ * receives the same brief whether it was dispatched via MCP or via
75
+ * the dashboard's HTTP bridge.
76
+ */
77
+ function buildPromptText(sub) {
78
+ const parts = [];
79
+ parts.push(`# ${sub.title || 'Subtask'}`);
80
+ if (sub.description) parts.push(String(sub.description).trim());
81
+ parts.push('');
82
+ parts.push('---');
83
+ parts.push(`Subtask ID: ${sub.id}`);
84
+ parts.push(`Assigned agent: ${sub.assignee || 'tyr'}`);
85
+ parts.push(`Parent task ID: ${sub.parent || '(none)'}`);
86
+ return parts.join('\n');
87
+ }
88
+
89
+ /**
90
+ * v3.5.4 — Write a per-instance state file under BG_DIRS so the
91
+ * dashboard's `backgroundStore.list()` and `kill()` paths find this
92
+ * bg instance. The shape matches what `background-state.ts` writes
93
+ * so the existing merge + dedupe logic works without changes.
94
+ *
95
+ * Idempotent: writes are atomic via tmp + rename. Failures are logged
96
+ * but do NOT throw — the opencode session is already running.
97
+ */
98
+ function writeBgStateFile(instanceId, payload) {
99
+ const dir = pickBgDir();
100
+ try {
101
+ mkdirSync(dir, { recursive: true });
102
+ } catch (err) {
103
+ console.warn(`[task-delegator] cannot create bg dir ${dir}: ${err.message}`);
104
+ return null;
105
+ }
106
+ const file = join(dir, `${instanceId}.json`);
107
+ const tmp = `${file}.tmp`;
108
+ try {
109
+ writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
110
+ try {
111
+ renameSync(tmp, file);
112
+ } catch {
113
+ writeFileSync(file, JSON.stringify(payload, null, 2), 'utf8');
114
+ }
115
+ return file;
116
+ } catch (err) {
117
+ console.warn(`[task-delegator] failed to write bg state file ${file}: ${err.message}`);
118
+ return null;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Read-only enumeration of bg state files for diagnostics / debug.
124
+ * Used by `dispatchToBackground` to pick a target state dir.
125
+ */
126
+ function pickBgDir() {
127
+ for (const dir of BG_DIRS) {
128
+ if (existsSync(dir)) return dir;
129
+ }
130
+ return BG_DIRS[0];
131
+ }
132
+
133
+ /**
134
+ * v3.7.1 — Auto-generate a task title from a prompt body when the
135
+ * caller didn't supply one. Mirrors `autoTitleFromContent` in
136
+ * `src/web/lib/utils.ts`. Strips markdown headers, takes the first
137
+ * non-empty line, truncates to ~60 chars at a word boundary. Falls
138
+ * back to a timestamped placeholder if there's nothing to summarize.
139
+ */
140
+ function autoTitleFromContent(body, fallback = true) {
141
+ const raw = (body || '').replace(/\r\n?/g, '\n').trim();
142
+ if (!raw) {
143
+ if (!fallback) return '';
144
+ const now = new Date();
145
+ const pad = (n) => String(n).padStart(2, '0');
146
+ return `Untitled task — ${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
147
+ }
148
+ const firstLine = raw
149
+ .split('\n')
150
+ .map((l) => l.replace(/^[\s>#*\-]+/, '').trim())
151
+ .find((l) => l.length > 0) || '';
152
+ if (!firstLine) {
153
+ if (!fallback) return '';
154
+ const now = new Date();
155
+ const pad = (n) => String(n).padStart(2, '0');
156
+ return `Untitled task — ${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
157
+ }
158
+ if (firstLine.length <= 60) return firstLine;
159
+ const cut = firstLine.slice(0, 60);
160
+ const lastSpace = cut.lastIndexOf(' ');
161
+ return (lastSpace > 20 ? cut.slice(0, lastSpace) : cut).trimEnd() + '…';
162
+ }
163
+
164
+ export const taskDelegator = {
165
+ /**
166
+ * Submit a task to Odin. Odin analyzes, splits, and dispatches.
167
+ *
168
+ * @param {object} taskInput - { title, description, priority?, tags? }
169
+ * @param {object} ctx - { projectRoot, projectId, broadcast }
170
+ * @returns {Promise<{ main, subtasks }>}
171
+ */
172
+ async submit(taskInput, ctx = {}) {
173
+ const broadcast = ctx.broadcast || (() => {});
174
+ const projectId = ctx.projectId || null;
175
+
176
+ // v3.7.1 — Title is now optional. Auto-generate from the description
177
+ // when the caller didn't supply one. Reject only when BOTH fields
178
+ // are empty (so curl/api callers without a description get a clear
179
+ // error).
180
+ const description = (taskInput.description || '').trim();
181
+ let title = (taskInput.title || '').trim();
182
+ if (!title && description) {
183
+ title = autoTitleFromContent(description);
184
+ }
185
+ if (!title) {
186
+ throw new Error('title or description is required');
187
+ }
188
+
189
+ // 1. Create the main task. Persisted via the regular store so it
190
+ // shows up in /api/tasks and the UI immediately.
191
+ const main = await tasksStore.create(projectId, {
192
+ title,
193
+ description: taskInput.description || '',
194
+ status: 'queued',
195
+ priority: ['low', 'normal', 'high'].includes(taskInput.priority)
196
+ ? taskInput.priority
197
+ : 'normal',
198
+ tags: Array.isArray(taskInput.tags) ? taskInput.tags : [],
199
+ assignee: 'odin',
200
+ parent: null,
201
+ metadata: { progress: 0, currentStep: 'Queued for delegation', progressHistory: [{ ts: new Date().toISOString(), progress: 0, step: 'Queued for delegation' }] },
202
+ });
203
+ broadcast({ type: 'tasks:change', task: main });
204
+
205
+ // v3.3.0 — Drop a notification that the user submitted a task.
206
+ try {
207
+ notificationsStore.add({
208
+ severity: 'info',
209
+ source: 'odin',
210
+ title: 'Task submitted',
211
+ message: `Odin is splitting "${title}" into subtasks.`,
212
+ link: null,
213
+ meta: { taskId: main.id },
214
+ }, { broadcast });
215
+ } catch { /* best-effort */ }
216
+
217
+ // 2. Odin analyzes and splits.
218
+ const subtasks = await this.splitTask(main, ctx);
219
+
220
+ // 3. For each subtask, match an agent and persist the assignment.
221
+ const agents = agentsStore.list();
222
+ for (const sub of subtasks) {
223
+ const agent = this.matchAgent(sub, agents);
224
+ sub.assignee = agent.name;
225
+ const persisted = await tasksStore.update(projectId, sub.id, {
226
+ assignee: agent.name,
227
+ });
228
+ if (persisted) {
229
+ broadcast({ type: 'tasks:change', task: persisted });
230
+ }
231
+ }
232
+
233
+ // 4. Flip the main task to `doing` and record its subtasks.
234
+ main.subtasks = subtasks.map((s) => s.id);
235
+ const moved = await tasksStore.update(projectId, main.id, {
236
+ status: 'doing',
237
+ metadata: { progress: 10, currentStep: `Split into ${subtasks.length} subtask(s)`, startedAt: new Date().toISOString() },
238
+ });
239
+ if (moved) {
240
+ moved.subtasks = subtasks.map((s) => s.id);
241
+ broadcast({ type: 'tasks:change', task: moved });
242
+ }
243
+
244
+ // 5. Best-effort dispatch to background infrastructure. We collect
245
+ // per-subtask errors instead of swallowing them so the response
246
+ // includes `dispatchErrors[]`. The HTTP caller (and the UI toast)
247
+ // can then surface "X subtasks could not be dispatched" instead
248
+ // of a silent success.
249
+ const dispatchResult = { dispatched: [], errors: [], warnings: [] };
250
+ try {
251
+ const r = await this.dispatchToBackground(moved || main, subtasks, ctx, broadcast);
252
+ if (r && typeof r === 'object') {
253
+ if (Array.isArray(r.dispatched)) dispatchResult.dispatched = r.dispatched;
254
+ if (Array.isArray(r.errors)) dispatchResult.errors = r.errors;
255
+ if (Array.isArray(r.warnings)) dispatchResult.warnings = r.warnings;
256
+ }
257
+ } catch (err) {
258
+ console.error('[task-delegator] dispatch crashed:', err.message);
259
+ dispatchResult.errors.push({ kind: 'fatal', message: err.message || String(err) });
260
+ }
261
+
262
+ if (dispatchResult.errors.length > 0) {
263
+ try {
264
+ notificationsStore.add({
265
+ severity: 'warning',
266
+ source: 'odin',
267
+ title: 'Dispatch incomplete',
268
+ message: `${dispatchResult.errors.length} subtask(s) could not be dispatched. Use POST /api/tasks/:id/start to retry.`,
269
+ meta: { mainId: main.id, errors: dispatchResult.errors.slice(0, 5) },
270
+ }, { broadcast });
271
+ } catch { /* best-effort */ }
272
+ }
273
+
274
+ // 6. Audit activity.
275
+ try {
276
+ ctx.state?.appendActivity?.({
277
+ kind: 'task.delegated',
278
+ mainId: main.id,
279
+ subtasks: subtasks.map((s) => s.id),
280
+ title: main.title,
281
+ });
282
+ // Also write to the dashboard activity log so the Activity tab
283
+ // sees the delegation event.
284
+ try {
285
+ const { activityLog } = await import('./activity-log.mjs');
286
+ activityLog.append({
287
+ kind: 'task.delegated',
288
+ nodeId: `task:${main.id}`,
289
+ mainId: main.id,
290
+ subtaskIds: subtasks.map((s) => s.id),
291
+ title: main.title,
292
+ });
293
+ } catch { /* best-effort */ }
294
+ } catch {
295
+ /* best-effort */
296
+ }
297
+
298
+ return { main: moved || main, subtasks, dispatch: dispatchResult };
299
+ },
300
+
301
+ /**
302
+ * Split a task into 2-5 subtasks using heuristic rules.
303
+ * The split is intentionally conservative — when no signal matches
304
+ * the rule set we fall back to a single "do the work" subtask plus
305
+ * an optional "verify" subtask if the title hints at testing.
306
+ *
307
+ * @param {Task} main
308
+ * @param {object} ctx - { projectId, broadcast }
309
+ * @returns {Promise<Task[]>}
310
+ */
311
+ async splitTask(main, ctx = {}) {
312
+ const projectId = ctx.projectId || null;
313
+ const broadcast = ctx.broadcast || (() => {});
314
+ const title = (main.title || '').toLowerCase();
315
+ const desc = (main.description || '').toLowerCase();
316
+ const text = `${title} ${desc}`;
317
+
318
+ const wantImpl = /\b(implement|build|create|add|write|develop|craft|code|ship)\b/.test(text);
319
+ const wantTest = /\b(test|verify|validate|check|qa|cover|spec)\b/.test(text);
320
+ const wantDocs = /\b(document|readme|docs|comment|explain|write[- ]up)\b/.test(text);
321
+ const wantDesign = /\b(design|ui|ux|style|styling|theme|visual|layout|mockup)\b/.test(text);
322
+ const wantResearch = /\b(research|find|explore|investigate|compare|survey|analyze)\b/.test(text);
323
+ const wantRefactor = /\b(refactor|rework|restructure|clean[- ]up|reorganize)\b/.test(text);
324
+
325
+ const fragments = [];
326
+
327
+ if (wantImpl || wantRefactor) {
328
+ fragments.push({
329
+ key: 'impl',
330
+ title: `Implement: ${main.title}`,
331
+ description: main.description
332
+ ? `Implement the core change. Source description:\n\n${main.description}`
333
+ : `Implement: ${main.title}`,
334
+ priority: main.priority || 'normal',
335
+ assigneeHint: wantRefactor ? 'tyr' : 'tyr',
336
+ tags: [...(main.tags || []), 'implementation'],
337
+ });
338
+ }
339
+ if (wantTest) {
340
+ fragments.push({
341
+ key: 'test',
342
+ title: `Test: ${main.title}`,
343
+ description: `Write tests and verify: ${main.title}`,
344
+ priority: 'normal',
345
+ assigneeHint: 'thor',
346
+ tags: [...(main.tags || []), 'testing'],
347
+ });
348
+ }
349
+ if (wantDocs) {
350
+ fragments.push({
351
+ key: 'docs',
352
+ title: `Document: ${main.title}`,
353
+ description: `Write documentation for: ${main.title}`,
354
+ priority: 'low',
355
+ assigneeHint: 'heimdall',
356
+ tags: [...(main.tags || []), 'docs'],
357
+ });
358
+ }
359
+ if (wantDesign) {
360
+ fragments.push({
361
+ key: 'design',
362
+ title: `Design: ${main.title}`,
363
+ description: `Design work for: ${main.title}`,
364
+ priority: main.priority || 'normal',
365
+ assigneeHint: 'baldr',
366
+ tags: [...(main.tags || []), 'design'],
367
+ });
368
+ }
369
+ if (wantResearch) {
370
+ fragments.push({
371
+ key: 'research',
372
+ title: `Research: ${main.title}`,
373
+ description: `Research and summarize: ${main.title}`,
374
+ priority: 'normal',
375
+ assigneeHint: 'mimir',
376
+ tags: [...(main.tags || []), 'research'],
377
+ });
378
+ }
379
+
380
+ // Fallback when nothing matched: a single "do it" subtask.
381
+ if (fragments.length === 0) {
382
+ fragments.push({
383
+ key: 'work',
384
+ title: `Work on: ${main.title}`,
385
+ description: main.description || main.title,
386
+ priority: main.priority || 'normal',
387
+ assigneeHint: 'tyr',
388
+ tags: [...(main.tags || [])],
389
+ });
390
+ }
391
+
392
+ // Hard cap at 5 subtasks — keep the board tidy.
393
+ const limited = fragments.slice(0, 5);
394
+
395
+ const subtasks = [];
396
+ for (const f of limited) {
397
+ const t = await tasksStore.create(projectId, {
398
+ title: f.title,
399
+ description: f.description,
400
+ status: 'queued',
401
+ priority: f.priority,
402
+ tags: f.tags,
403
+ assignee: f.assigneeHint,
404
+ parent: main.id,
405
+ });
406
+ broadcast({ type: 'tasks:change', task: t });
407
+ subtasks.push(t);
408
+ }
409
+ return subtasks;
410
+ },
411
+
412
+ /**
413
+ * Match a subtask to the best agent. Returns a fallback agent if
414
+ * nothing matches (never null).
415
+ */
416
+ matchAgent(task, agents) {
417
+ const text = `${task.title || ''} ${task.description || ''}`.toLowerCase();
418
+ const tags = (task.tags || []).map((t) => t.toLowerCase());
419
+
420
+ // Priority rules. First match wins. Order matters.
421
+ const rules = [
422
+ { match: ['git', 'commit', 'pr', 'merge', 'branch', 'push'], agent: 'hermod' },
423
+ { match: ['design', 'ui', 'ux', 'style', 'theme', 'visual', 'layout'], agent: 'baldr' },
424
+ { match: ['research', 'find', 'explore', 'search', 'investigate'], agent: 'mimir' },
425
+ { match: ['clarify', 'question', 'ask'], agent: 'vor' },
426
+ { match: ['explain', 'how', 'why'], agent: 'frigg' },
427
+ { match: ['test', 'verify', 'validate', 'check', 'qa'], agent: 'thor' },
428
+ { match: ['document', 'doc', 'readme'], agent: 'heimdall' },
429
+ { match: ['refactor', 'rework', 'restructure', 'cleanup'], agent: 'tyr' },
430
+ { match: ['implement', 'build', 'create', 'add', 'code', 'fix'], agent: 'tyr' },
431
+ { match: ['security', 'audit', 'review'], agent: 'forseti' },
432
+ { match: ['simple', 'quick', 'small', 'easy'], agent: 'heimdall' },
433
+ { match: ['complex', 'architect'], agent: 'tyr' },
434
+ ];
435
+
436
+ for (const rule of rules) {
437
+ const hit =
438
+ rule.match.some((m) => text.includes(m)) ||
439
+ rule.match.some((m) => tags.includes(m));
440
+ if (hit) {
441
+ const agent = agents.find((a) => a.name === rule.agent);
442
+ if (agent) return agent;
443
+ }
444
+ }
445
+
446
+ // Honor any explicit assignee stored on the task.
447
+ if (task.assignee) {
448
+ const found = agents.find((a) => a.name === task.assignee);
449
+ if (found) return found;
450
+ }
451
+
452
+ return agents.find((a) => a.name === 'tyr') || agents[0] || { name: 'tyr' };
453
+ },
454
+
455
+ /**
456
+ * v3.5.4 (bug: dispatch stuck) — Best-effort dispatch to the background
457
+ * agent infrastructure. Talks to the plugin's opencode serve child via
458
+ * HTTP (createSession + sendPrompt) and writes a per-instance state
459
+ * file under BG_DIRS so the dashboard's `backgroundStore.list()` and
460
+ * `kill()` paths find the instance.
461
+ *
462
+ * Replaces the previous `execSync('node plugins/bizar/dist/cli.js …')`
463
+ * path which silently failed because the plugin is a Bun-native TS
464
+ * project with no compiled `dist/`.
465
+ *
466
+ * Returns a structured `{ dispatched: string[], errors: Array<…>, warnings: Array<…> }`
467
+ * so the caller (and `/api/tasks/submit`) can surface failures to the
468
+ * user instead of swallowing them.
469
+ *
470
+ * v3.5.1 — Respects agents.maxParallel: caps concurrent dispatches to
471
+ * the configured limit; overflow subtasks are queued until a slot
472
+ * frees up.
473
+ *
474
+ * @returns {Promise<{ dispatched: string[], errors: Array<{ subtaskId: string, kind: string, message: string }>, warnings: string[] }>}
475
+ */
476
+ async dispatchToBackground(main, subtasks, ctx, broadcast) {
477
+ const projectRoot = ctx.projectRoot || process.cwd();
478
+ const projectId = ctx.projectId || null;
479
+ const bgDir = pickBgDir();
480
+ const result = { dispatched: [], errors: [], warnings: [] };
481
+
482
+ // v3.5.1 — Read agents.maxParallel from settings (default 6).
483
+ const SETTINGS_FILE = join(HOME, '.config', 'bizar', 'settings.json');
484
+ let maxParallel = 6;
485
+ try {
486
+ if (existsSync(SETTINGS_FILE)) {
487
+ const raw = readFileSync(SETTINGS_FILE, 'utf8');
488
+ const settings = JSON.parse(raw);
489
+ maxParallel = settings?.agents?.maxParallel ?? 6;
490
+ }
491
+ } catch {
492
+ maxParallel = 6;
493
+ }
494
+
495
+ // Count currently running bg instances across all candidate dirs.
496
+ // v3.5.4 (bug #5) — `list()` is now async because it merges
497
+ // opencode-direct sessions via fetch.
498
+ const runningInstances = (await backgroundStore.list()).filter(
499
+ (b) => b.status === 'running' || b.status === 'pending',
500
+ );
501
+ const runningCount = runningInstances.length;
502
+ const slotsAvailable = Math.max(0, maxParallel - runningCount);
503
+
504
+ // v3.5.4 (bug: dispatch stuck) — Resolve the opencode serve child
505
+ // the plugin owns. If the plugin is not running we record a warning
506
+ // and skip the actual spawn (subtasks still flip to `doing` so the
507
+ // user can see them, but `metadata.dispatchPending = true` tells
508
+ // them to retry via `/api/tasks/:id/start` once the plugin is up).
509
+ let serveInfo = null;
510
+ let serveReachable = false;
511
+ try {
512
+ const { readServeInfo, pingOpencodeServe } = await import('./serve-info.mjs');
513
+ serveInfo = readServeInfo();
514
+ if (!serveInfo) {
515
+ result.warnings.push('serve-info not found — the bizar plugin is not running. Tasks will be marked queued; retry via POST /api/tasks/:id/start once the plugin is up.');
516
+ console.warn('[task-delegator] serve-info missing — plugin may not be running. Dispatch will be deferred.');
517
+ } else {
518
+ serveReachable = await pingOpencodeServe(serveInfo);
519
+ if (!serveReachable) {
520
+ result.warnings.push(`opencode serve at ${serveInfo.baseUrl} is not reachable. Tasks will be marked queued; retry via POST /api/tasks/:id/start.`);
521
+ console.warn(`[task-delegator] opencode serve not reachable at ${serveInfo.baseUrl}`);
522
+ }
523
+ }
524
+ } catch (err) {
525
+ result.warnings.push(`could not load serve-info helper: ${err.message}`);
526
+ console.warn('[task-delegator] serve-info helper failed:', err.message);
527
+ }
528
+
529
+ // Split: first N get dispatched now, rest are queued.
530
+ const toDispatch = subtasks.slice(0, slotsAvailable);
531
+ const toQueue = subtasks.slice(slotsAvailable);
532
+
533
+ for (const sub of toQueue) {
534
+ const metadata = {
535
+ ...(sub.metadata || {}),
536
+ waitingForSlot: true,
537
+ queuedAt: new Date().toISOString(),
538
+ maxParallel,
539
+ };
540
+ await tasksStore.update(projectId, sub.id, {
541
+ status: 'queued',
542
+ metadata,
543
+ });
544
+ broadcast({
545
+ type: 'tasks:change',
546
+ task: { id: sub.id, status: 'queued', metadata },
547
+ });
548
+ }
549
+
550
+ // v3.5.4 (bug: dispatch stuck) — Per-subtask dispatch path. We
551
+ // collect errors instead of swallowing them. Two failure modes:
552
+ // - serveInfo/serveReachable missing: mark dispatchPending so
553
+ // the UI shows "Awaiting dispatch" and `/api/tasks/:id/start`
554
+ // can retry.
555
+ // - HTTP error: record error message so the API response carries
556
+ // it.
557
+ for (const sub of toDispatch) {
558
+ const startedAt = new Date().toISOString();
559
+ let bgInstanceId = null;
560
+ let sessionId = null;
561
+ let dispatchError = null;
562
+ let dispatchPending = false;
563
+
564
+ // v3.11.1 — Active dispatch path. We no longer depend on the
565
+ // opencode serve child's HTTP API to drive the agent loop
566
+ // (per the opencode docs, that API is passive — it needs a
567
+ // TUI/web client connected to actually process prompts).
568
+ // Instead we spawn one `opencode run` subprocess per subtask
569
+ // and capture its output to the LogWriter's log file. The
570
+ // plugin's opencode-runner.ts and this dashboard's
571
+ // opencode-runner.mjs share the same wire format.
572
+ if (serveInfo && serveReachable) {
573
+ try {
574
+ const { spawnAgent } = await import('./opencode-runner.mjs');
575
+ // v3.11.0 — Fall back to projectRoot when serve-info is
576
+ // missing `worktree` (older plugin builds only wrote
577
+ // `{password, pid, port}`).
578
+ const dispatchWorktree = serveInfo.worktree || projectRoot || '';
579
+ const promptText = buildPromptText(sub);
580
+
581
+ // Predict the bg instance id (deterministic, derived from
582
+ // a short hex so the dashboard state file and the tmux
583
+ // session name match what the runner reports). The
584
+ // sessionId is generated by the subprocess; we'll learn
585
+ // it from the runner's structured-log stream.
586
+ const predictedInstanceId = `bg_${genShortHex(8)}`;
587
+ const logFile = getActualBgLogPath({ sessionId: predictedInstanceId });
588
+
589
+ const spawnRes = await spawnAgent({
590
+ prompt: promptText,
591
+ agent: sub.assignee || 'tyr',
592
+ worktree: dispatchWorktree,
593
+ logPath: logFile,
594
+ title: `bgr:${sub.assignee || 'tyr'}:${predictedInstanceId}:${sub.id}`,
595
+ });
596
+
597
+ if (!spawnRes.ok || !spawnRes.sessionId) {
598
+ dispatchError = spawnRes.error || 'opencode run failed before reporting session id';
599
+ } else {
600
+ sessionId = spawnRes.sessionId;
601
+ bgInstanceId = predictedInstanceId;
602
+
603
+ // v3.5.5 — Wrap the agent run in a tmux session so
604
+ // operators can `tmux attach -t bizar-bg-<id>` and watch
605
+ // the opencode process in real time.
606
+ try {
607
+ const tmuxRes = backgroundStore.spawnTmuxFor(
608
+ `bg_${sessionId.slice(0, 16)}`,
609
+ { command: 'tail', args: ['-n', '200', '-F', logFile] },
610
+ dispatchWorktree,
611
+ );
612
+ // We do not fail the dispatch when tmux is missing.
613
+ if (tmuxRes.note) {
614
+ // pre-existing session, no log
615
+ }
616
+ } catch (err) {
617
+ console.warn(`[task-delegator] tmux wrap failed: ${err.message}`);
618
+ }
619
+ }
620
+ } catch (err) {
621
+ dispatchError = err instanceof Error ? err.message : String(err);
622
+ }
623
+ } else {
624
+ // Plugin not running — leave the subtask marked queued with a
625
+ // dispatchPending flag. The UI surfaces this and the user can
626
+ // retry via POST /api/tasks/:id/start.
627
+ dispatchPending = true;
628
+ }
629
+
630
+ // v3.5.4 (bug: dispatch stuck) — Generate a bg instance ID even on
631
+ // failure so the dashboard can track attempts. Successful dispatches
632
+ // get a deterministic id derived from the opencode session ID.
633
+ bgInstanceId = sessionId
634
+ ? `bg_${sessionId.slice(0, 16)}`
635
+ : `bg_${genShortHex(8)}`;
636
+
637
+ // 3. Always write the bg state file so the dashboard's background
638
+ // list reflects every dispatch attempt (success or failure).
639
+ writeBgStateFile(bgInstanceId, {
640
+ instanceId: bgInstanceId,
641
+ sessionId: sessionId || null,
642
+ projectId,
643
+ worktree: serveInfo?.worktree || projectRoot || null,
644
+ agent: sub.assignee || 'tyr',
645
+ parentAgent: 'odin',
646
+ status: dispatchError ? 'failed' : 'pending',
647
+ startedAt: Date.now(),
648
+ lastActivityAt: Date.now(),
649
+ toolCallCount: 0,
650
+ promptPreview: (sub.title || '').slice(0, 200),
651
+ taskId: sub.id,
652
+ mainTaskId: main?.id || null,
653
+ dispatchPending,
654
+ error: dispatchError || null,
655
+ });
656
+
657
+ const metadata = { ...(sub.metadata || {}) };
658
+ if (bgInstanceId) metadata.bgInstanceId = bgInstanceId;
659
+ metadata.dispatchedAt = startedAt;
660
+ metadata.progress = dispatchError ? 0 : 5;
661
+ metadata.currentStep = dispatchError
662
+ ? `Dispatch failed: ${dispatchError}`
663
+ : (dispatchPending ? 'Awaiting dispatch (plugin offline)' : `Dispatched (${bgInstanceId})`);
664
+ metadata.progressHistory = [
665
+ ...(metadata.progressHistory || []),
666
+ {
667
+ ts: startedAt,
668
+ progress: metadata.progress,
669
+ step: metadata.currentStep,
670
+ agent: sub.assignee || null,
671
+ ...(dispatchError ? { error: dispatchError } : {}),
672
+ },
673
+ ];
674
+ metadata.startedAt = metadata.startedAt || startedAt;
675
+ if (dispatchError) metadata.dispatchError = dispatchError;
676
+ if (dispatchPending) metadata.dispatchPending = true;
677
+
678
+ // v3.5.4 (bug: dispatch stuck) — Mark as queued (not doing) when
679
+ // dispatch failed or is pending, so the task doesn't sit forever
680
+ // at 5% doing. The UI can still show "Dispatch pending" via the
681
+ // metadata flag.
682
+ const newStatus = dispatchError || dispatchPending ? 'queued' : 'doing';
683
+ const updated = await tasksStore.update(projectId, sub.id, {
684
+ status: newStatus,
685
+ metadata,
686
+ });
687
+ if (updated) {
688
+ broadcast({ type: 'tasks:change', task: updated });
689
+ if (!dispatchError && !dispatchPending) {
690
+ broadcast({
691
+ type: 'task:progress',
692
+ taskId: sub.id,
693
+ progress: 5,
694
+ step: metadata.currentStep,
695
+ agent: sub.assignee || null,
696
+ });
697
+ }
698
+ }
699
+
700
+ if (dispatchError) {
701
+ result.errors.push({ subtaskId: sub.id, kind: 'spawn_failed', message: dispatchError });
702
+ console.error(`[task-delegator] dispatch ${sub.id} failed:`, dispatchError);
703
+ } else if (dispatchPending) {
704
+ // Pending is not an error — just record which subtasks are
705
+ // waiting for the plugin to come up.
706
+ result.warnings.push(`subtask ${sub.id} awaiting plugin (${sub.assignee || 'tyr'})`);
707
+ } else {
708
+ result.dispatched.push(bgInstanceId);
709
+ }
710
+ }
711
+
712
+ // Note the bg dir in activity so the dashboard can link to it.
713
+ broadcast({
714
+ type: 'tasks:change',
715
+ task: { id: main.id, _bgDir: bgDir },
716
+ });
717
+
718
+ return result;
719
+ },
720
+
721
+ /**
722
+ * v3.5.4 — Dispatch a single queued task. Used by POST /api/tasks/:id/start
723
+ * to retry dispatch after the plugin comes up. Resolves to the same
724
+ * structured result as `dispatchToBackground`. Loads the task first so
725
+ * the caller doesn't need to know the storage layer.
726
+ *
727
+ * @param {string} taskId
728
+ * @param {object} ctx - { projectId, projectRoot, broadcast }
729
+ * @returns {Promise<{ ok: boolean, task?: object, errors: Array<…>, warnings: Array<…> }>}
730
+ */
731
+ async dispatchSingleTask(taskId, ctx = {}) {
732
+ const projectId = ctx.projectId || null;
733
+ const broadcast = ctx.broadcast || (() => {});
734
+ const all = await tasksStore.loadTasks(projectId, { includeArchived: false });
735
+ const task = all.find((t) => t.id === taskId);
736
+ if (!task) {
737
+ return { ok: false, errors: [{ subtaskId: taskId, kind: 'not_found', message: 'task not found' }], warnings: [] };
738
+ }
739
+ if (task.status !== 'queued') {
740
+ return {
741
+ ok: false,
742
+ task,
743
+ errors: [{ subtaskId: taskId, kind: 'invalid_status', message: `task is '${task.status}', must be 'queued' to start` }],
744
+ warnings: [],
745
+ };
746
+ }
747
+
748
+ // The task may be a subtask (has parent) or a top-level delegated
749
+ // task. We dispatch as a single-item subtask list so the same code
750
+ // path handles both.
751
+ const asSubtask = {
752
+ id: task.id,
753
+ title: task.title,
754
+ description: task.description,
755
+ assignee: task.assignee || 'tyr',
756
+ parent: task.parent || null,
757
+ };
758
+ const synthMain = { id: task.id };
759
+ const result = await this.dispatchToBackground(synthMain, [asSubtask], ctx, broadcast);
760
+ const reloaded = await tasksStore.loadTasks(projectId, { includeArchived: false });
761
+ const refreshed = reloaded.find((t) => t.id === taskId) || task;
762
+ return {
763
+ ok: result.errors.length === 0,
764
+ task: refreshed,
765
+ dispatched: result.dispatched,
766
+ errors: result.errors,
767
+ warnings: result.warnings,
768
+ };
769
+ },
770
+
771
+ /** For tests / introspection. */
772
+ _BG_DIRS: BG_DIRS,
773
+ };
774
+
775
+ // ── Public helpers for routes that need to walk bg state ─────────────
776
+
777
+ /**
778
+ * Read a bg instance file from any of the candidate bg dirs. Returns
779
+ * null if not found.
780
+ */
781
+ export function readBgInstance(instanceId) {
782
+ for (const dir of BG_DIRS) {
783
+ const file = join(dir, `${instanceId}.json`);
784
+ if (existsSync(file)) {
785
+ try {
786
+ const raw = readFileSync(file, 'utf8');
787
+ return { ...JSON.parse(raw), _bgDir: dir };
788
+ } catch {
789
+ return null;
790
+ }
791
+ }
792
+ }
793
+ return null;
794
+ }
795
+
796
+ /**
797
+ * List all bg instances across the candidate dirs.
798
+ */
799
+ export function listBgInstances() {
800
+ const out = [];
801
+ for (const dir of BG_DIRS) {
802
+ if (!existsSync(dir)) continue;
803
+ let files;
804
+ try {
805
+ files = readdirSync(dir).filter((f) => f.endsWith('.json'));
806
+ } catch {
807
+ continue;
808
+ }
809
+ for (const f of files) {
810
+ try {
811
+ const full = join(dir, f);
812
+ const st = statSync(full);
813
+ const data = JSON.parse(readFileSync(full, 'utf8'));
814
+ out.push({ ...data, _bgDir: dir, _mtime: st.mtimeMs });
815
+ } catch {
816
+ /* skip corrupt */
817
+ }
818
+ }
819
+ }
820
+ out.sort((a, b) => (b.startedAt || b._mtime || 0) - (a.startedAt || a._mtime || 0));
821
+ return out;
822
+ }
823
+
824
+ export { pickBgDir, BG_DIRS };