@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,21 @@
1
+ /**
2
+ * src/server/routes-v2/health.mjs
3
+ *
4
+ * v0.7.0-alpha.1 — Public health endpoint (no auth required).
5
+ */
6
+
7
+ import express from 'express';
8
+
9
+ export function createV2HealthRouter({ version, startedAt }) {
10
+ const router = express.Router();
11
+
12
+ router.get('/health', (_req, res) => {
13
+ res.status(200).json({
14
+ status: 'ok',
15
+ uptime: Math.max(0, Math.floor((Date.now() - startedAt) / 1000)),
16
+ version,
17
+ });
18
+ });
19
+
20
+ return router;
21
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * src/server/routes-v2/index.mjs
3
+ *
4
+ * v0.7.0-alpha.1 — Mounts /api/v2/* routes with HTTP basic auth.
5
+ *
6
+ * Public: GET /health
7
+ * Protected (Basic auth): /sessions, /sessions/:id, /event
8
+ *
9
+ * Also exposes GET /doc — the OpenAPI spec served from
10
+ * `.bizar/research/OPENAPI_SPEC.yaml` (or wherever build copies it).
11
+ */
12
+
13
+ import express from 'express';
14
+ import { readFileSync, existsSync } from 'node:fs';
15
+ import { join, dirname } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { v2BasicAuth } from './auth.mjs';
18
+ import { createV2HealthRouter } from './health.mjs';
19
+ import { createV2EventsRouter } from './events.mjs';
20
+ import { createV2SessionsRouter } from './sessions.mjs';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+
25
+ /**
26
+ * Locate the OpenAPI spec file.
27
+ * Search order:
28
+ * 1. <package>/.bizar/research/OPENAPI_SPEC.yaml (development)
29
+ * 2. <package>/OPENAPI_SPEC.yaml (when copied at build)
30
+ * 3. <repo-root>/.bizar/research/OPENAPI_SPEC.yaml (BizarHarness monorepo)
31
+ * 4. <package>/../.bizar/research/OPENAPI_SPEC.yaml (workspace dev fallback)
32
+ */
33
+ function findOpenApiSpec() {
34
+ const packageRoot = join(__dirname, '..', '..', '..'); // src/server/routes-v2 → package root (bizar-dash)
35
+ const candidates = [
36
+ join(packageRoot, '.bizar', 'research', 'OPENAPI_SPEC.yaml'),
37
+ join(packageRoot, 'OPENAPI_SPEC.yaml'),
38
+ join(packageRoot, '..', '.bizar', 'research', 'OPENAPI_SPEC.yaml'),
39
+ join(packageRoot, '..', '..', '.bizar', 'research', 'OPENAPI_SPEC.yaml'),
40
+ ];
41
+ for (const candidate of candidates) {
42
+ if (existsSync(candidate)) return candidate;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ export function createV2Router({ eventBus, getPassword, version, startedAt }) {
48
+ const router = express.Router();
49
+ const auth = v2BasicAuth(getPassword);
50
+
51
+ // v0.7.0-alpha.1 — debug request logger (logs every v2 hit). Comment
52
+ // out for production. Useful for verifying SDK publish calls land.
53
+ router.use((req, _res, next) => {
54
+ console.log(
55
+ `[v2-req] ${req.method} ${req.originalUrl} auth=${req.headers.authorization ? 'yes' : 'no'}`,
56
+ );
57
+ next();
58
+ });
59
+
60
+ // Public health endpoint.
61
+ router.use(createV2HealthRouter({ version, startedAt }));
62
+
63
+ // OpenAPI spec.
64
+ router.get('/doc', (_req, res) => {
65
+ const specPath = findOpenApiSpec();
66
+ if (!specPath) {
67
+ return res.status(404).json({
68
+ name: 'DashboardError',
69
+ data: { statusCode: 404, message: 'OpenAPI spec not found' },
70
+ });
71
+ }
72
+ try {
73
+ const content = readFileSync(specPath, 'utf8');
74
+ res.setHeader('Content-Type', 'application/yaml; charset=utf-8');
75
+ res.status(200).send(content);
76
+ } catch (err) {
77
+ res.status(500).json({
78
+ name: 'DashboardError',
79
+ data: { statusCode: 500, message: err instanceof Error ? err.message : String(err) },
80
+ });
81
+ }
82
+ });
83
+
84
+ // Protected routes.
85
+ router.use(auth);
86
+ router.use(createV2EventsRouter({ eventBus }));
87
+ router.use(createV2SessionsRouter({ eventBus }));
88
+
89
+ return router;
90
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * src/server/routes-v2/sessions.mjs
3
+ *
4
+ * v0.7.0-alpha.1 — Sessions CRUD for /api/v2/sessions/*.
5
+ *
6
+ * Stores sessions in-memory (Map). The plugin is the source of truth for
7
+ * session state (it owns opencode serve); the dashboard reflects what the
8
+ * plugin publishes via the event bus + REST.
9
+ *
10
+ * For v0.7.0-alpha.1 this is a minimal implementation sufficient for the
11
+ * smoke test. v0.7.1 will integrate with the dashboard's existing
12
+ * background-store.mjs.
13
+ */
14
+
15
+ import express from 'express';
16
+
17
+ export function createV2SessionsRouter({ eventBus }) {
18
+ const router = express.Router();
19
+ const sessions = new Map(); // id → Session
20
+
21
+ router.get('/sessions', (req, res) => {
22
+ const statusFilter = typeof req.query.status === 'string' ? req.query.status : null;
23
+ const agentFilter = typeof req.query.agent === 'string' ? req.query.agent : null;
24
+ let limit = 50;
25
+ if (typeof req.query.limit === 'string') {
26
+ const n = Number.parseInt(req.query.limit, 10);
27
+ if (Number.isFinite(n) && n > 0 && n <= 200) limit = n;
28
+ }
29
+
30
+ let list = Array.from(sessions.values());
31
+ if (statusFilter) list = list.filter((s) => s.status === statusFilter);
32
+ if (agentFilter) list = list.filter((s) => s.agent === agentFilter);
33
+ list.sort((a, b) => b.createdAt - a.createdAt);
34
+ res.status(200).json(list.slice(0, limit));
35
+ });
36
+
37
+ router.post('/sessions', (req, res) => {
38
+ const body = req.body ?? {};
39
+ if (typeof body.agent !== 'string' || body.agent.length === 0) {
40
+ return res.status(400).json({
41
+ name: 'DashboardError',
42
+ data: { statusCode: 400, message: '`agent` is required' },
43
+ });
44
+ }
45
+ if (typeof body.prompt !== 'string' || body.prompt.length === 0) {
46
+ return res.status(400).json({
47
+ name: 'DashboardError',
48
+ data: { statusCode: 400, message: '`prompt` is required' },
49
+ });
50
+ }
51
+ const now = Date.now();
52
+ const id = `bgr_${randomUlid()}`;
53
+ const session = {
54
+ id,
55
+ agent: body.agent,
56
+ status: 'pending',
57
+ parentId: typeof body.parentId === 'string' ? body.parentId : undefined,
58
+ model: typeof body.model === 'string' ? body.model : undefined,
59
+ toolCallCount: 0,
60
+ worktree: typeof body.worktree === 'string' ? body.worktree : process.cwd(),
61
+ createdAt: now,
62
+ updatedAt: now,
63
+ };
64
+ sessions.set(id, session);
65
+ eventBus.publish({
66
+ type: 'session.created',
67
+ properties: { sessionId: id, agent: session.agent },
68
+ });
69
+ res.status(201).json(session);
70
+ });
71
+
72
+ router.get('/sessions/:sessionId', (req, res) => {
73
+ const session = sessions.get(req.params.sessionId);
74
+ if (!session) {
75
+ return res.status(404).json({
76
+ name: 'DashboardError',
77
+ data: { statusCode: 404, message: 'Session not found' },
78
+ });
79
+ }
80
+ res.status(200).json(session);
81
+ });
82
+
83
+ router.delete('/sessions/:sessionId', (req, res) => {
84
+ const session = sessions.get(req.params.sessionId);
85
+ if (!session) {
86
+ return res.status(404).json({
87
+ name: 'DashboardError',
88
+ data: { statusCode: 404, message: 'Session not found' },
89
+ });
90
+ }
91
+ session.status = 'killed';
92
+ session.updatedAt = Date.now();
93
+ eventBus.publish({
94
+ type: 'session.updated',
95
+ properties: { sessionId: session.id, status: 'killed' },
96
+ });
97
+ res.status(204).end();
98
+ });
99
+
100
+ return router;
101
+ }
102
+
103
+ function randomUlid() {
104
+ // Lightweight ULID-ish suffix (10 chars base36 of timestamp + random).
105
+ const ts = Date.now().toString(36).toUpperCase();
106
+ let rnd = '';
107
+ for (let i = 0; i < 10; i += 1) {
108
+ rnd += Math.floor(Math.random() * 36).toString(36).toUpperCase();
109
+ }
110
+ return `${ts}${rnd}`;
111
+ }
@@ -0,0 +1,369 @@
1
+ /**
2
+ * src/server/schedules-runner.mjs
3
+ *
4
+ * v3.9.0 — Executes due schedules.
5
+ *
6
+ * Called by the service daemon (cli/service.mjs) on a tick. For each due
7
+ * schedule, this module runs the action and records the result via
8
+ * schedulesStore.recordRun().
9
+ *
10
+ * Action types:
11
+ * - command → spawn a child process
12
+ * - agent → submit via taskDelegator (Odin split-and-dispatch).
13
+ * Honors `budgetCheck.skipIfBudgetLow` to skip when too
14
+ * many bg tasks are already running.
15
+ * - webhook → POST JSON to the URL
16
+ *
17
+ * Errors are caught and recorded; the loop never throws.
18
+ */
19
+ import { spawn } from 'node:child_process';
20
+ import { writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ import { homedir } from 'node:os';
23
+ import { isIP } from 'node:net';
24
+ import { projectsStore } from './projects-store.mjs';
25
+ import { schedulesStore } from './schedules-store.mjs';
26
+
27
+ const HOME = homedir();
28
+ const LOG_DIR = join(HOME, '.config', 'bizar');
29
+ const LOG_FILE = join(LOG_DIR, 'service.log');
30
+ const ALLOW_PRIVATE_WEBHOOKS = process.env.BIZAR_DASHBOARD_ALLOW_PRIVATE_WEBHOOKS === '1';
31
+ const SHELL_META = /[;&|`$<>\n\r]/;
32
+
33
+ function logLine(line) {
34
+ try {
35
+ mkdirSync(LOG_DIR, { recursive: true });
36
+ appendFileSync(LOG_FILE, line + '\n', 'utf8');
37
+ } catch {
38
+ /* ignore */
39
+ }
40
+ }
41
+
42
+ function parseCommandTarget(action) {
43
+ if (action?.command && Array.isArray(action?.args)) {
44
+ return {
45
+ command: String(action.command).trim(),
46
+ args: action.args.map((arg) => String(arg)),
47
+ };
48
+ }
49
+ const target = String(action?.target || '').trim();
50
+ if (!target) return { error: 'empty command target' };
51
+ if (SHELL_META.test(target)) {
52
+ return { error: 'shell metacharacters are not allowed in schedule command targets' };
53
+ }
54
+ const parts = target.split(/\s+/).filter(Boolean);
55
+ if (parts.length === 0) return { error: 'empty command target' };
56
+ return { command: parts[0], args: parts.slice(1) };
57
+ }
58
+
59
+ function isPrivateHostname(hostname) {
60
+ const host = String(hostname || '').replace(/^\[|\]$/g, '').toLowerCase();
61
+ if (!host) return true;
62
+ if (host === 'localhost' || host === '::1') return true;
63
+ const ipVersion = isIP(host);
64
+ if (ipVersion === 4) {
65
+ if (host.startsWith('127.')) return true;
66
+ if (host.startsWith('10.')) return true;
67
+ if (host.startsWith('192.168.')) return true;
68
+ if (host.startsWith('169.254.')) return true;
69
+ const octets = host.split('.').map((part) => Number(part));
70
+ if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) return true;
71
+ return false;
72
+ }
73
+ if (ipVersion === 6) {
74
+ return host === '::1' || host.startsWith('fc') || host.startsWith('fd') || host.startsWith('fe80:');
75
+ }
76
+ return host.endsWith('.local');
77
+ }
78
+
79
+ function validateWebhookTarget(rawUrl) {
80
+ try {
81
+ const url = new URL(String(rawUrl || ''));
82
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
83
+ return { error: 'webhook URL must use http or https' };
84
+ }
85
+ if (url.username || url.password) {
86
+ return { error: 'webhook URL must not embed credentials' };
87
+ }
88
+ if (!ALLOW_PRIVATE_WEBHOOKS && isPrivateHostname(url.hostname)) {
89
+ return { error: 'private, loopback, and .local webhook targets are blocked by default' };
90
+ }
91
+ return { url };
92
+ } catch {
93
+ return { error: 'invalid webhook URL' };
94
+ }
95
+ }
96
+
97
+ async function runAction(action, ctx = {}) {
98
+ if (!action || typeof action !== 'object') {
99
+ throw new Error('invalid action');
100
+ }
101
+ if (action.type === 'command') {
102
+ return runCommand(action);
103
+ }
104
+ if (action.type === 'webhook') {
105
+ return runWebhook(action);
106
+ }
107
+ if (action.type === 'agent') {
108
+ return runAgentAction(action, ctx);
109
+ }
110
+ throw new Error(`unknown action type: ${action.type}`);
111
+ }
112
+
113
+ /**
114
+ * Dispatch an `agent` schedule action via the dashboard's task delegator.
115
+ * Mirrors the contract used by /api/tasks/submit: Odin analyzes the prompt,
116
+ * splits into subtasks, and dispatches each to a matched agent.
117
+ *
118
+ * The delegator's `submit()` returns `{ main, subtasks, dispatch }`. We
119
+ * treat the call as successful when `main` was persisted AND Odin
120
+ * produced at least one subtask. Subtask dispatch errors are surfaced as
121
+ * the `error` field of the returned shape so the schedule's `lastError`
122
+ * reflects the dispatch outcome, not just "the task was created".
123
+ *
124
+ * Lazy import keeps this module loadable when the task delegator is not
125
+ * yet initialised (e.g. during unit tests).
126
+ *
127
+ * @param {object} action
128
+ * @param {object} [ctx] Optional ctx (projectId, broadcast). Passed through.
129
+ * @returns {Promise<{ ok: boolean, taskId?: string, subtaskCount?: number, error?: string }>}
130
+ */
131
+ async function runAgentAction(action, ctx = {}) {
132
+ const title =
133
+ action.title ||
134
+ action.name ||
135
+ (typeof action.target === 'string' && action.target.trim()) ||
136
+ 'Scheduled task';
137
+ const prompt =
138
+ action.prompt ||
139
+ (action.body && typeof action.body === 'object' && action.body.prompt) ||
140
+ (typeof action.target === 'string' ? action.target.trim() : '') ||
141
+ 'Run scheduled task.';
142
+ const projectId = ctx.projectId || null;
143
+ try {
144
+ const { taskDelegator } = await import('./task-delegator.mjs');
145
+ const result = await taskDelegator.submit(
146
+ {
147
+ title,
148
+ description: prompt,
149
+ tags: ['scheduled'],
150
+ priority: 'normal',
151
+ },
152
+ {
153
+ projectId,
154
+ broadcast: ctx.broadcast || (() => {}),
155
+ },
156
+ );
157
+ const mainOk = !!(result && result.main && result.main.id);
158
+ const subtaskCount = (result && Array.isArray(result.subtasks)) ? result.subtasks.length : 0;
159
+ const dispatchErrors = result && result.dispatch && Array.isArray(result.dispatch.errors)
160
+ ? result.dispatch.errors
161
+ : [];
162
+ const dispatchErrMsg = dispatchErrors.length > 0
163
+ ? dispatchErrors.map((e) => e && e.message).filter(Boolean).join('; ') || 'dispatch error'
164
+ : null;
165
+ if (!mainOk) {
166
+ return { ok: false, error: 'task delegator did not persist a main task' };
167
+ }
168
+ if (subtaskCount === 0) {
169
+ return { ok: false, taskId: result.main.id, error: 'no subtasks produced' };
170
+ }
171
+ return {
172
+ ok: dispatchErrors.length === 0,
173
+ taskId: result.main.id,
174
+ subtaskCount,
175
+ error: dispatchErrMsg || undefined,
176
+ };
177
+ } catch (err) {
178
+ return { ok: false, error: err && err.message ? err.message : String(err) };
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Budget pre-flight: count in-flight bg tasks (running + pending) and
184
+ * skip the schedule if the count meets or exceeds `maxConcurrent`.
185
+ * Returns `{ skip: true, reason }` to skip, or `{ skip: false }` to
186
+ * continue with the run.
187
+ *
188
+ * Lazy-imports backgroundStore so this module can be loaded even when
189
+ * the bg subsystem hasn't booted yet.
190
+ *
191
+ * @param {{ skipIfBudgetLow?: boolean, maxConcurrent?: number }|undefined} budgetCheck
192
+ * @returns {Promise<{ skip: boolean, reason?: string, running?: number, cap?: number }>}
193
+ */
194
+ async function checkBudget(budgetCheck) {
195
+ if (!budgetCheck || !budgetCheck.skipIfBudgetLow) {
196
+ return { skip: false };
197
+ }
198
+ const cap = Number.isFinite(budgetCheck.maxConcurrent) ? budgetCheck.maxConcurrent : 6;
199
+ let running = 0;
200
+ try {
201
+ const { backgroundStore } = await import('./background-store.mjs');
202
+ const list = backgroundStore.list();
203
+ running = list.filter(
204
+ (b) => b && (b.status === 'running' || b.status === 'pending'),
205
+ ).length;
206
+ } catch (err) {
207
+ // If the bg subsystem is unreachable we treat that as "no tasks
208
+ // running" — better to over-fire a schedule than to silently skip.
209
+ logLine(`[schedule] budget check failed: ${err.message || err}`);
210
+ return { skip: false };
211
+ }
212
+ if (running >= cap) {
213
+ return {
214
+ skip: true,
215
+ reason: `budget: ${running} concurrent >= ${cap}`,
216
+ running,
217
+ cap,
218
+ };
219
+ }
220
+ return { skip: false, running, cap };
221
+ }
222
+
223
+ function runCommand(action) {
224
+ return new Promise((resolve) => {
225
+ const parsed = parseCommandTarget(action);
226
+ if (parsed.error) {
227
+ resolve({ ok: false, error: parsed.error });
228
+ return;
229
+ }
230
+ try {
231
+ const child = spawn(parsed.command, parsed.args, {
232
+ shell: false,
233
+ stdio: ['ignore', 'pipe', 'pipe'],
234
+ env: process.env,
235
+ });
236
+ let stdout = '';
237
+ let stderr = '';
238
+ child.stdout.on('data', (b) => (stdout += b.toString()));
239
+ child.stderr.on('data', (b) => (stderr += b.toString()));
240
+ child.on('error', (err) => {
241
+ resolve({ ok: false, error: err.message, stdout, stderr });
242
+ });
243
+ child.on('close', (code) => {
244
+ resolve({
245
+ ok: code === 0,
246
+ error: code === 0 ? null : `exit ${code}`,
247
+ stdout: stdout.slice(-2000),
248
+ stderr: stderr.slice(-2000),
249
+ });
250
+ });
251
+ } catch (err) {
252
+ resolve({ ok: false, error: err.message });
253
+ }
254
+ });
255
+ }
256
+
257
+ async function runWebhook(action) {
258
+ const validated = validateWebhookTarget(action.target);
259
+ if (validated.error) return { ok: false, error: validated.error };
260
+ const method = String(action.method || 'POST').toUpperCase();
261
+ const allowedMethods = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']);
262
+ if (!allowedMethods.has(method)) {
263
+ return { ok: false, error: `unsupported webhook method: ${method}` };
264
+ }
265
+ try {
266
+ const r = await fetch(validated.url, {
267
+ method,
268
+ headers: { 'Content-Type': 'application/json' },
269
+ body: action.body && method !== 'GET' && method !== 'HEAD' ? JSON.stringify(action.body) : undefined,
270
+ redirect: 'manual',
271
+ });
272
+ if (r.status >= 300 && r.status < 400) {
273
+ return { ok: false, status: r.status, error: 'redirect responses are blocked for webhooks' };
274
+ }
275
+ return { ok: r.ok, status: r.status };
276
+ } catch (err) {
277
+ return { ok: false, error: err.message };
278
+ }
279
+ }
280
+
281
+ export const schedulesRunner = {
282
+ LOG_FILE,
283
+
284
+ /** Run all currently-due schedules for every project. */
285
+ async tick() {
286
+ const reg = projectsStore.list();
287
+ const fired = [];
288
+ const projectIds = new Set(reg.projects.map((project) => project.id));
289
+ try {
290
+ if (schedulesStore.list('default').length > 0) projectIds.add('default');
291
+ } catch {
292
+ /* ignore */
293
+ }
294
+ let skipped = 0;
295
+ for (const projectId of projectIds) {
296
+ const due = schedulesStore.due(projectId, Date.now());
297
+ for (const sched of due) {
298
+ const result = await this.runOne(projectId, sched);
299
+ if (result && result.skipped) skipped += 1;
300
+ fired.push({ projectId, scheduleId: sched.id, ...result });
301
+ }
302
+ }
303
+ if (skipped > 0) {
304
+ logLine(`[schedule] tick: ${skipped} skipped by budget pre-flight`);
305
+ }
306
+ return fired;
307
+ },
308
+
309
+ /** Run a single schedule immediately and record the result. */
310
+ async runOne(projectId, sched) {
311
+ const startedAt = new Date().toISOString();
312
+ logLine(
313
+ `[${startedAt}] run ${projectId}/${sched.id} (${sched.type}: ${sched.schedule})`,
314
+ );
315
+ // Budget pre-flight — only applies to agent actions with a budget
316
+ // gate. A skip records a `skipped` result and does NOT advance
317
+ // `nextRun` (the schedule keeps firing on the same cadence).
318
+ if (sched && sched.action && sched.action.type === 'agent') {
319
+ try {
320
+ const budget = await checkBudget(sched.budgetCheck);
321
+ if (budget.skip) {
322
+ logLine(`[schedule] budget check: ${budget.reason}, skipping ${sched.id}`);
323
+ const updated = schedulesStore.recordRun(projectId, sched.id, {
324
+ result: 'skipped',
325
+ error: budget.reason || 'budget exhausted',
326
+ });
327
+ return { ok: true, skipped: true, schedule: updated, runResult: { skipped: true, ...budget } };
328
+ }
329
+ } catch (err) {
330
+ // A failing budget check must NOT block the schedule — log and
331
+ // proceed with the action.
332
+ logLine(`[schedule] budget check threw: ${err.message || err}`);
333
+ }
334
+ }
335
+ try {
336
+ const res = await runAction(sched.action, { projectId });
337
+ // The runner distinguishes "ran but skipped by budget" (ok:true,
338
+ // skipped:true) from "ran successfully" (ok:true, no skip) and
339
+ // "ran and errored" (ok:false).
340
+ const result = res && res.skipped
341
+ ? 'skipped'
342
+ : res.ok
343
+ ? 'success'
344
+ : 'error';
345
+ const updated = schedulesStore.recordRun(projectId, sched.id, {
346
+ result,
347
+ error: res.error || null,
348
+ });
349
+ const suffix = result === 'success'
350
+ ? 'success'
351
+ : result === 'skipped'
352
+ ? `skipped: ${res.error || 'budget'}`
353
+ : `error: ${res.error}`;
354
+ logLine(
355
+ `[${new Date().toISOString()}] done ${projectId}/${sched.id} → ${suffix}`,
356
+ );
357
+ return { ok: res.ok, skipped: result === 'skipped', schedule: updated, runResult: res };
358
+ } catch (err) {
359
+ const updated = schedulesStore.recordRun(projectId, sched.id, {
360
+ result: 'error',
361
+ error: err.message || String(err),
362
+ });
363
+ logLine(
364
+ `[${new Date().toISOString()}] failed ${projectId}/${sched.id}: ${err.message}`,
365
+ );
366
+ return { ok: false, schedule: updated, runResult: { error: err.message } };
367
+ }
368
+ },
369
+ };