@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,733 @@
1
+ /**
2
+ * src/server/artifacts-store.mjs
3
+ *
4
+ * v3.1.0 — Plan canvas editing.
5
+ *
6
+ * Each plan is a directory with a v2 canvas (`plan.json`) plus a
7
+ * `meta.json` (title, status, tags, …) and an empty `comments.json`
8
+ * for the v1 comment stream. The CLI's `cli/plan.mjs` uses a similar
9
+ * on-disk layout but with `comments.json` and an mdx file; we treat
10
+ * `plan.json` as the source of truth and ignore the mdx for v3.1.0
11
+ * edits so the editor never races the markdown regenerator.
12
+ *
13
+ * Storage locations, in order of preference:
14
+ * 1. <projectRoot>/.bizar/artifacts/<slug> (worktree, project-scoped)
15
+ * 2. ~/.config/opencode/artifacts/<slug> (global fallback)
16
+ */
17
+ import {
18
+ existsSync,
19
+ readFileSync,
20
+ writeFileSync,
21
+ renameSync,
22
+ readdirSync,
23
+ statSync,
24
+ mkdirSync,
25
+ rmSync,
26
+ } from 'node:fs';
27
+ import { join, dirname } from 'node:path';
28
+ import { homedir } from 'node:os';
29
+ import { randomBytes } from 'node:crypto';
30
+
31
+ const HOME = homedir();
32
+ const GLOBAL_PLANS_DIR = join(HOME, '.config', 'opencode', 'artifacts');
33
+
34
+ // Atomic JSON write: serialize to a sibling temp file, then rename into
35
+ // place. `rename` is atomic on POSIX (same filesystem), so a crash
36
+ // between write and rename never leaves a half-written / corrupt file.
37
+ function atomicWriteJson(filePath, data) {
38
+ const tmp = `${filePath}.tmp.${process.pid}`;
39
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
40
+ renameSync(tmp, filePath);
41
+ }
42
+
43
+ function safeReadJSON(file, fallback = null) {
44
+ try {
45
+ if (!existsSync(file)) return fallback;
46
+ const text = readFileSync(file, 'utf8');
47
+ if (!text.trim()) return fallback;
48
+ return JSON.parse(text);
49
+ } catch {
50
+ return fallback;
51
+ }
52
+ }
53
+
54
+ function safeReadText(file, fallback = '') {
55
+ try {
56
+ if (!existsSync(file)) return fallback;
57
+ return readFileSync(file, 'utf8');
58
+ } catch {
59
+ return fallback;
60
+ }
61
+ }
62
+
63
+ function genId(prefix) {
64
+ return `${prefix}_${randomBytes(5).toString('hex').slice(0, 9)}`;
65
+ }
66
+
67
+ const VALID_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
68
+
69
+ function emptyCanvas(title = 'Untitled plan') {
70
+ return {
71
+ schemaVersion: 2,
72
+ title,
73
+ elements: [],
74
+ connections: [],
75
+ comments: [],
76
+ viewport: { x: 0, y: 0, zoom: 1 },
77
+ };
78
+ }
79
+
80
+ function defaultMeta(slug, title) {
81
+ const now = new Date().toISOString();
82
+ return {
83
+ slug,
84
+ title: title || slug,
85
+ status: 'draft',
86
+ tags: [],
87
+ description: '',
88
+ author: process.env.USER || 'drb0rk',
89
+ created: now,
90
+ lastEdited: now,
91
+ };
92
+ }
93
+
94
+ export const artifactsStore = {
95
+ GLOBAL_PLANS_DIR,
96
+
97
+ /**
98
+ * Resolve the on-disk directory for a slug. Searches the worktree
99
+ * `artifacts/` first, then the global location.
100
+ *
101
+ * The caller passes `projectRoot` (typically the server's projectRoot).
102
+ */
103
+ resolveDir(slug, projectRoot) {
104
+ const candidates = [];
105
+ if (projectRoot) candidates.push(join(projectRoot, '.bizar', 'artifacts', slug));
106
+ candidates.push(join(GLOBAL_PLANS_DIR, slug));
107
+ for (const c of candidates) {
108
+ if (existsSync(c)) return c;
109
+ }
110
+ return null;
111
+ },
112
+
113
+ /**
114
+ * Same as resolveDir but allows creating a new plan in the worktree
115
+ * (preferred) or falling back to the global directory.
116
+ */
117
+ ensureDir(slug, projectRoot) {
118
+ const target = projectRoot
119
+ ? join(projectRoot, '.bizar', 'artifacts', slug)
120
+ : join(GLOBAL_PLANS_DIR, slug);
121
+ mkdirSync(target, { recursive: true });
122
+ return target;
123
+ },
124
+
125
+ list(projectRoot) {
126
+ const seen = new Set();
127
+ const out = [];
128
+ const dirs = [];
129
+ if (projectRoot) dirs.push(join(projectRoot, '.bizar', 'artifacts'));
130
+ dirs.push(GLOBAL_PLANS_DIR);
131
+ for (const dir of dirs) {
132
+ if (!existsSync(dir)) continue;
133
+ for (const entry of readdirSync(dir)) {
134
+ if (seen.has(entry)) continue;
135
+ const full = join(dir, entry);
136
+ const st = statSync(full);
137
+ if (!st || !st.isDirectory()) continue;
138
+ const meta = safeReadJSON(join(full, 'meta.json'), null);
139
+ const canvas = safeReadJSON(join(full, 'plan.json'), null);
140
+ const comments = safeReadJSON(join(full, 'comments.json'), null);
141
+ const elementCount = canvas && Array.isArray(canvas.elements)
142
+ ? canvas.elements.length
143
+ : 0;
144
+ const commentCount =
145
+ (canvas && Array.isArray(canvas.comments) ? canvas.comments.length : 0) +
146
+ (Array.isArray(comments) ? comments.length : 0);
147
+ out.push({
148
+ slug: entry,
149
+ title: meta?.title || entry,
150
+ status: meta?.status || 'draft',
151
+ tags: meta?.tags || [],
152
+ description: meta?.description || '',
153
+ author: meta?.author || null,
154
+ source: dir === GLOBAL_PLANS_DIR ? 'global' : 'worktree',
155
+ elementCount,
156
+ commentCount,
157
+ created: meta?.created || null,
158
+ lastEdited: meta?.lastEdited || null,
159
+ mtime: st.mtimeMs,
160
+ path: full,
161
+ });
162
+ seen.add(entry);
163
+ }
164
+ }
165
+ out.sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
166
+ return out;
167
+ },
168
+
169
+ get(slug, projectRoot) {
170
+ if (!VALID_SLUG.test(slug || '')) return null;
171
+ const dir = this.resolveDir(slug, projectRoot);
172
+ if (!dir) return null;
173
+ const meta = safeReadJSON(join(dir, 'meta.json'), null);
174
+ const canvas = safeReadJSON(join(dir, 'plan.json'), null);
175
+ // v3.20.15 — read `artifact.mdx` (the CLI's source-of-truth name from
176
+ // cli/artifact.mjs:writePlanFile), with `plan.mdx` as a fallback for
177
+ // legacy v0 plans that still exist on disk. Without this fallback,
178
+ // every artifact opened in the dashboard showed empty content even
179
+ // though `artifact.mdx` was present and correct.
180
+ const planMdx = safeReadText(join(dir, 'artifact.mdx'))
181
+ || safeReadText(join(dir, 'plan.mdx'));
182
+ return {
183
+ slug,
184
+ dir,
185
+ meta: meta || defaultMeta(slug, slug),
186
+ canvas: canvas || emptyCanvas(meta?.title || slug),
187
+ planMdx,
188
+ };
189
+ },
190
+
191
+ /** Create a new plan. Returns the full plan record. */
192
+ create(slug, body, projectRoot) {
193
+ if (!VALID_SLUG.test(slug || '')) {
194
+ const err = new Error('invalid slug — must match /^[a-z0-9][a-z0-9-]{0,63}$/');
195
+ err.status = 400;
196
+ throw err;
197
+ }
198
+ const dir = this.ensureDir(slug, projectRoot);
199
+ if (existsSync(join(dir, 'meta.json')) || existsSync(join(dir, 'plan.json'))) {
200
+ const err = new Error(`plan "${slug}" already exists`);
201
+ err.status = 409;
202
+ throw err;
203
+ }
204
+ const title = (body && body.title) || slug;
205
+ const meta = {
206
+ ...defaultMeta(slug, title),
207
+ ...(body || {}),
208
+ slug,
209
+ created: new Date().toISOString(),
210
+ lastEdited: new Date().toISOString(),
211
+ };
212
+ const canvas = emptyCanvas(title);
213
+ this._writePlan(dir, meta, canvas);
214
+ return { slug, dir, meta, canvas, planMdx: '' };
215
+ },
216
+
217
+ /** Update plan metadata (title, status, tags, description). */
218
+ updateMeta(slug, body, projectRoot) {
219
+ if (!VALID_SLUG.test(slug || '')) return null;
220
+ const dir = this.resolveDir(slug, projectRoot);
221
+ if (!dir) return null;
222
+ const meta = safeReadJSON(join(dir, 'meta.json'), defaultMeta(slug, slug));
223
+ if (typeof body?.title === 'string') meta.title = body.title.slice(0, 200);
224
+ if (typeof body?.description === 'string') meta.description = body.description;
225
+ if (Array.isArray(body?.tags)) meta.tags = body.tags.filter((t) => typeof t === 'string');
226
+ if (typeof body?.status === 'string') {
227
+ const valid = ['draft', 'approved', 'in-progress', 'done', 'rejected', 'archived'];
228
+ if (valid.includes(body.status)) meta.status = body.status;
229
+ }
230
+ if (typeof body?.author === 'string') meta.author = body.author;
231
+ meta.lastEdited = new Date().toISOString();
232
+ const canvas = safeReadJSON(join(dir, 'plan.json'), emptyCanvas(meta.title));
233
+ this._writePlan(dir, meta, canvas);
234
+ return { slug, dir, meta, canvas, planMdx: safeReadText(join(dir, 'plan.mdx')) };
235
+ },
236
+
237
+ /** Delete a plan directory. */
238
+ delete(slug, projectRoot) {
239
+ const dir = this.resolveDir(slug, projectRoot);
240
+ if (!dir) return false;
241
+ try {
242
+ rmSync(dir, { recursive: true, force: true });
243
+ } catch {
244
+ return false;
245
+ }
246
+ return true;
247
+ },
248
+
249
+ /** Read the canvas (with auto-migration from mdx if no plan.json). */
250
+ getCanvas(slug, projectRoot) {
251
+ const plan = this.get(slug, projectRoot);
252
+ if (!plan) return null;
253
+ if (!plan.canvas || !Array.isArray(plan.canvas.elements)) {
254
+ plan.canvas = emptyCanvas(plan.meta?.title || slug);
255
+ }
256
+ return plan.canvas;
257
+ },
258
+
259
+ /** Save the full canvas (PUT). */
260
+ saveCanvas(slug, canvas, projectRoot) {
261
+ const plan = this.get(slug, projectRoot);
262
+ if (!plan) return null;
263
+ const next = sanitizeCanvas(canvas, plan.meta?.title || slug);
264
+ const meta = { ...plan.meta, lastEdited: new Date().toISOString() };
265
+ if (typeof next.title === 'string' && next.title.trim()) {
266
+ meta.title = next.title.slice(0, 200);
267
+ }
268
+ this._writePlan(plan.dir, meta, next);
269
+ return { ...plan, meta, canvas: next };
270
+ },
271
+
272
+ /** Add an element. Returns the new element. */
273
+ addElement(slug, body, projectRoot) {
274
+ const plan = this.get(slug, projectRoot);
275
+ if (!plan) return null;
276
+ const canvas = sanitizeCanvas(plan.canvas, plan.meta?.title || slug);
277
+ const el = {
278
+ id: genId('el'),
279
+ type: typeof body?.type === 'string' ? body.type : 'note',
280
+ title: typeof body?.title === 'string' ? body.title.slice(0, 200) : '',
281
+ content: typeof body?.content === 'string' ? body.content : '',
282
+ x: Number.isFinite(body?.x) ? body.x : 80,
283
+ y: Number.isFinite(body?.y) ? body.y : 80,
284
+ width: Number.isFinite(body?.width) ? body.width : 240,
285
+ height: Number.isFinite(body?.height) ? body.height : 120,
286
+ status: typeof body?.status === 'string' ? body.status : 'open',
287
+ };
288
+ canvas.elements.push(el);
289
+ this._writePlan(plan.dir, { ...plan.meta, lastEdited: new Date().toISOString() }, canvas);
290
+ return { plan, canvas, element: el };
291
+ },
292
+
293
+ /** Update an element. Returns the updated element. */
294
+ updateElement(slug, elId, body, projectRoot) {
295
+ const plan = this.get(slug, projectRoot);
296
+ if (!plan) return null;
297
+ const canvas = sanitizeCanvas(plan.canvas, plan.meta?.title || slug);
298
+ const idx = canvas.elements.findIndex((e) => e.id === elId);
299
+ if (idx === -1) return null;
300
+ const cur = canvas.elements[idx];
301
+ const next = { ...cur };
302
+ if (typeof body?.type === 'string') next.type = body.type;
303
+ if (typeof body?.title === 'string') next.title = body.title.slice(0, 200);
304
+ if (typeof body?.content === 'string') next.content = body.content;
305
+ if (Number.isFinite(body?.x)) next.x = body.x;
306
+ if (Number.isFinite(body?.y)) next.y = body.y;
307
+ if (Number.isFinite(body?.width)) next.width = body.width;
308
+ if (Number.isFinite(body?.height)) next.height = body.height;
309
+ if (typeof body?.status === 'string') next.status = body.status;
310
+ canvas.elements[idx] = next;
311
+ this._writePlan(plan.dir, { ...plan.meta, lastEdited: new Date().toISOString() }, canvas);
312
+ return { plan, canvas, element: next };
313
+ },
314
+
315
+ /** Delete an element + its connections + its comments. */
316
+ deleteElement(slug, elId, projectRoot) {
317
+ const plan = this.get(slug, projectRoot);
318
+ if (!plan) return null;
319
+ const canvas = sanitizeCanvas(plan.canvas, plan.meta?.title || slug);
320
+ const before = canvas.elements.length;
321
+ canvas.elements = canvas.elements.filter((e) => e.id !== elId);
322
+ if (canvas.elements.length === before) return null;
323
+ canvas.connections = canvas.connections.filter(
324
+ (c) => c.from !== elId && c.to !== elId && c.fromElementId !== elId && c.toElementId !== elId,
325
+ );
326
+ canvas.comments = canvas.comments.filter(
327
+ (c) => c.elementId !== elId && c.id !== elId,
328
+ );
329
+ this._writePlan(plan.dir, { ...plan.meta, lastEdited: new Date().toISOString() }, canvas);
330
+ return { plan, canvas };
331
+ },
332
+
333
+ /** Bulk-update element positions (drag end). */
334
+ updatePositions(slug, positions, projectRoot) {
335
+ const plan = this.get(slug, projectRoot);
336
+ if (!plan) return null;
337
+ const canvas = sanitizeCanvas(plan.canvas, plan.meta?.title || slug);
338
+ const map = new Map();
339
+ if (Array.isArray(positions)) {
340
+ for (const p of positions) {
341
+ if (p && typeof p.id === 'string') map.set(p.id, p);
342
+ }
343
+ }
344
+ canvas.elements = canvas.elements.map((el) => {
345
+ const p = map.get(el.id);
346
+ if (!p) return el;
347
+ const next = { ...el };
348
+ if (Number.isFinite(p.x)) next.x = p.x;
349
+ if (Number.isFinite(p.y)) next.y = p.y;
350
+ return next;
351
+ });
352
+ this._writePlan(plan.dir, { ...plan.meta, lastEdited: new Date().toISOString() }, canvas);
353
+ return { plan, canvas };
354
+ },
355
+
356
+ /** Add a connection between two elements. */
357
+ addConnection(slug, body, projectRoot) {
358
+ const plan = this.get(slug, projectRoot);
359
+ if (!plan) return null;
360
+ const canvas = sanitizeCanvas(plan.canvas, plan.meta?.title || slug);
361
+ const from = body?.from || body?.fromElementId;
362
+ const to = body?.to || body?.toElementId;
363
+ if (!from || !to) return null;
364
+ const conn = {
365
+ id: genId('conn'),
366
+ from,
367
+ to,
368
+ label: typeof body?.label === 'string' ? body.label : '',
369
+ };
370
+ canvas.connections.push(conn);
371
+ this._writePlan(plan.dir, { ...plan.meta, lastEdited: new Date().toISOString() }, canvas);
372
+ return { plan, canvas, connection: conn };
373
+ },
374
+
375
+ /** Delete a connection. */
376
+ deleteConnection(slug, connId, projectRoot) {
377
+ const plan = this.get(slug, projectRoot);
378
+ if (!plan) return null;
379
+ const canvas = sanitizeCanvas(plan.canvas, plan.meta?.title || slug);
380
+ const before = canvas.connections.length;
381
+ canvas.connections = canvas.connections.filter((c) => c.id !== connId);
382
+ if (canvas.connections.length === before) return null;
383
+ this._writePlan(plan.dir, { ...plan.meta, lastEdited: new Date().toISOString() }, canvas);
384
+ return { plan, canvas };
385
+ },
386
+
387
+ /**
388
+ * Add a comment. Pass elementId to pin to an element; omit for canvas-level.
389
+ *
390
+ * v3.5.4 (bug: pin) — Accepts optional `x` and `y` (finite numbers) on the
391
+ * body. Stored verbatim on the comment so the UI can render a canvas pin
392
+ * even for canvas-level comments. `elementId` and (x, y) are independent:
393
+ * a comment can be pinned to an element AND have a position override, or
394
+ * be canvas-only with no coords. Both fields default to null.
395
+ */
396
+ addComment(slug, elId, body, projectRoot) {
397
+ const plan = this.get(slug, projectRoot);
398
+ if (!plan) return null;
399
+ const canvas = sanitizeCanvas(plan.canvas, plan.meta?.title || slug);
400
+ const text = (body?.text || '').toString().trim();
401
+ if (!text) return null;
402
+ const comment = {
403
+ id: genId('cmt'),
404
+ elementId: elId || body?.elementId || null,
405
+ author: (body?.author || process.env.USER || 'drb0rk').toString().slice(0, 60),
406
+ text: text.slice(0, 4000),
407
+ created: new Date().toISOString(),
408
+ thread: [],
409
+ };
410
+ // v3.5.4 (bug: pin) — Persist canvas-pin coordinates if provided. We
411
+ // accept either `x`/`y` as direct numbers or a `pin: {x, y}` object so
412
+ // the frontend can send whichever shape is convenient.
413
+ const rawX = body?.pin?.x ?? body?.x;
414
+ const rawY = body?.pin?.y ?? body?.y;
415
+ if (Number.isFinite(rawX)) comment.x = rawX;
416
+ if (Number.isFinite(rawY)) comment.y = rawY;
417
+ canvas.comments.push(comment);
418
+ this._writePlan(plan.dir, { ...plan.meta, lastEdited: new Date().toISOString() }, canvas);
419
+ return { plan, canvas, comment };
420
+ },
421
+
422
+ /** Delete a comment. */
423
+ deleteComment(slug, cid, projectRoot) {
424
+ const plan = this.get(slug, projectRoot);
425
+ if (!plan) return null;
426
+ const canvas = sanitizeCanvas(plan.canvas, plan.meta?.title || slug);
427
+ const before = canvas.comments.length;
428
+ canvas.comments = canvas.comments.filter((c) => c.id !== cid);
429
+ if (canvas.comments.length === before) return null;
430
+ this._writePlan(plan.dir, { ...plan.meta, lastEdited: new Date().toISOString() }, canvas);
431
+ return { plan, canvas };
432
+ },
433
+
434
+ /**
435
+ * v3.3.0 — Record the user's response to a question element on the
436
+ * canvas. Marks the question resolved, attaches the choice + any
437
+ * freeform text to the element's `question.response` field, and
438
+ * also writes a canvas-level comment so the conversation shows up
439
+ * in the existing comment thread.
440
+ *
441
+ * Returns { plan, canvas, element, response } or null if not found.
442
+ */
443
+ respondToQuestion(slug, qid, { choiceId, text } = {}, projectRoot) {
444
+ const plan = this.get(slug, projectRoot);
445
+ if (!plan) return null;
446
+ const canvas = sanitizeCanvas(plan.canvas, plan.meta?.title || slug);
447
+ const idx = canvas.elements.findIndex((e) => e.id === qid);
448
+ if (idx === -1) return null;
449
+ const el = canvas.elements[idx];
450
+ if (el.type !== 'question') return null;
451
+ const choices = Array.isArray(el.choices) ? el.choices : [];
452
+ const choice = choices.find((c) => c.id === choiceId) || null;
453
+ const response = {
454
+ choiceId: choiceId || null,
455
+ label: choice?.label || null,
456
+ text: text || null,
457
+ respondedAt: new Date().toISOString(),
458
+ };
459
+ const next = {
460
+ ...el,
461
+ status: 'resolved',
462
+ response,
463
+ };
464
+ canvas.elements[idx] = next;
465
+ // Drop a canvas comment so the conversation thread is visible.
466
+ const comment = {
467
+ id: typeof el.id === 'string' && el.id.length >= 9 ? `cmt_${el.id.slice(-9)}` : `cmt_${Date.now().toString(36)}`,
468
+ elementId: el.id,
469
+ author: 'user',
470
+ text: response.text
471
+ ? `Answer: ${response.label || response.choiceId || 'custom'} — ${response.text}`
472
+ : `Answer: ${response.label || response.choiceId || 'custom'}`,
473
+ created: response.respondedAt,
474
+ thread: [],
475
+ };
476
+ canvas.comments.push(comment);
477
+ this._writePlan(plan.dir, { ...plan.meta, lastEdited: new Date().toISOString() }, canvas);
478
+ return { plan, canvas, element: next, response, comment };
479
+ },
480
+
481
+ /**
482
+ * v3.22.0 — Submit user feedback for an artifact (glyph).
483
+ *
484
+ * Reads the on-disk comments + canvas question answers, writes a
485
+ * structured `feedback.md` file the agent can read, marks the
486
+ * artifact's `meta.json` status as `review`, and returns the
487
+ * feedback summary.
488
+ *
489
+ * `body.answers` is `{ questionId, value }[]` — the agent-side
490
+ * OpenQuestions answers. We persist them by writing them into the
491
+ * feedback file so a follow-up read finds them, and (best-effort)
492
+ * also by walking the canvas elements to mark question elements
493
+ * as resolved with the chosen value.
494
+ *
495
+ * Returns:
496
+ * { ok: true, slug, feedbackFile, commentCount, questionCount }
497
+ *
498
+ * Returns:
499
+ * { ok: false, error } — if the slug doesn't exist or is invalid.
500
+ */
501
+ submitFeedback(slug, body, projectRoot) {
502
+ if (!VALID_SLUG.test(slug || '')) {
503
+ return { ok: false, error: 'invalid_slug' };
504
+ }
505
+ const dir = this.resolveDir(slug, projectRoot);
506
+ if (!dir) return { ok: false, error: 'not_found' };
507
+
508
+ const meta = safeReadJSON(join(dir, 'meta.json'), defaultMeta(slug, slug));
509
+ const now = new Date().toISOString();
510
+
511
+ // Gather free-placed comments. Prefer the canvas.comments array
512
+ // (the v2 source of truth), fall back to comments.json if the
513
+ // canvas is empty.
514
+ const canvas = safeReadJSON(join(dir, 'plan.json'), emptyCanvas(meta.title || slug));
515
+ const commentsFromCanvas = Array.isArray(canvas.comments) ? canvas.comments : [];
516
+ const commentsFromFile = safeReadJSON(join(dir, 'comments.json'), []);
517
+ const comments = commentsFromCanvas.length > 0
518
+ ? commentsFromCanvas
519
+ : (Array.isArray(commentsFromFile) ? commentsFromFile : []);
520
+
521
+ // Gather OpenQuestions answers. The body may carry
522
+ // `[{ questionId, value }]` (preferred) or, for compatibility,
523
+ // already-resolved question elements on the canvas.
524
+ const rawAnswers = Array.isArray(body?.answers) ? body.answers : [];
525
+ const answers = [];
526
+ for (const a of rawAnswers) {
527
+ if (!a || typeof a !== 'object') continue;
528
+ const qid = typeof a.questionId === 'string' ? a.questionId : '';
529
+ const value = typeof a.value === 'string' || typeof a.value === 'number'
530
+ ? String(a.value)
531
+ : (a.value == null ? '' : String(a.value));
532
+ if (!qid) continue;
533
+ // Try to find the question's label from the canvas.
534
+ let label = qid;
535
+ let kind = null;
536
+ let options = null;
537
+ const el = Array.isArray(canvas.elements)
538
+ ? canvas.elements.find((e) => e && e.id === qid)
539
+ : null;
540
+ if (el && el.type === 'question') {
541
+ if (typeof el.title === 'string' && el.title) label = el.title;
542
+ if (typeof el.kind === 'string') kind = el.kind;
543
+ if (Array.isArray(el.options)) options = el.options;
544
+ // Persist the answer onto the question element so subsequent
545
+ // reads see the resolved state.
546
+ el.status = 'resolved';
547
+ el.response = {
548
+ choiceId: typeof a.choiceId === 'string' ? a.choiceId : null,
549
+ label: typeof a.label === 'string' ? a.label : null,
550
+ value,
551
+ respondedAt: now,
552
+ };
553
+ }
554
+ answers.push({ questionId: qid, label, kind, options, value });
555
+ }
556
+
557
+ // Build the markdown feedback file.
558
+ const submittedBy = (body && typeof body.submitter === 'string' && body.submitter)
559
+ ? body.submitter
560
+ : (process.env.USER || 'drb0rk');
561
+
562
+ const lines = [];
563
+ lines.push('---');
564
+ lines.push(`glyph: ${slug}`);
565
+ lines.push(`submittedAt: ${now}`);
566
+ lines.push(`submittedBy: ${submittedBy}`);
567
+ lines.push(`commentCount: ${comments.length}`);
568
+ lines.push(`questionCount: ${answers.length}`);
569
+ lines.push('---');
570
+ lines.push('');
571
+ lines.push(`# Feedback for ${meta.title || slug}`);
572
+ lines.push('');
573
+ lines.push('## Free-placed comments');
574
+ if (comments.length === 0) {
575
+ lines.push('_no free-placed comments_');
576
+ } else {
577
+ for (const c of comments) {
578
+ const x = Number.isFinite(c.x) ? c.x : null;
579
+ const y = Number.isFinite(c.y) ? c.y : null;
580
+ const author = typeof c.author === 'string' ? c.author : 'drb0rk';
581
+ const text = typeof c.text === 'string' ? c.text : '';
582
+ const coord = x !== null && y !== null ? `(${x}, ${y})` : '(canvas)';
583
+ lines.push(`- ${coord} — ${author}: ${text}`);
584
+ }
585
+ }
586
+ lines.push('');
587
+ lines.push('## Open-question answers');
588
+ if (answers.length === 0) {
589
+ lines.push('_no open-question answers_');
590
+ } else {
591
+ for (const qa of answers) {
592
+ lines.push(`### Q: ${qa.label}`);
593
+ lines.push(`A: ${qa.value}`);
594
+ if (qa.kind) lines.push(`(kind: ${qa.kind})`);
595
+ lines.push('');
596
+ }
597
+ }
598
+ lines.push('## Full MDX source');
599
+ lines.push('');
600
+ lines.push('```mdx');
601
+ const planMdx = safeReadText(join(dir, 'artifact.mdx'))
602
+ || safeReadText(join(dir, 'plan.mdx'))
603
+ || '';
604
+ lines.push(planMdx);
605
+ lines.push('```');
606
+ lines.push('');
607
+
608
+ const feedbackFile = join(dir, 'feedback.md');
609
+ writeFileSync(feedbackFile, lines.join('\n'), 'utf8');
610
+
611
+ // Update meta.json — mark status as `review` and bump lastEdited.
612
+ meta.status = 'review';
613
+ meta.lastEdited = now;
614
+ atomicWriteJson(join(dir, 'meta.json'), meta);
615
+
616
+ // Persist the updated canvas (so question responses survive).
617
+ if (Array.isArray(canvas.elements)) {
618
+ atomicWriteJson(join(dir, 'plan.json'), canvas);
619
+ }
620
+
621
+ return {
622
+ ok: true,
623
+ slug,
624
+ feedbackFile,
625
+ commentCount: comments.length,
626
+ questionCount: answers.length,
627
+ };
628
+ },
629
+
630
+ // ── internal ────────────────────────────────────────────────────────
631
+ _writePlan(dir, meta, canvas) {
632
+ mkdirSync(dir, { recursive: true });
633
+ atomicWriteJson(join(dir, 'meta.json'), meta);
634
+ atomicWriteJson(join(dir, 'plan.json'), canvas);
635
+ if (!existsSync(join(dir, 'comments.json'))) {
636
+ writeFileSync(join(dir, 'comments.json'), '[]\n', 'utf8');
637
+ }
638
+ },
639
+ };
640
+
641
+ function sanitizeCanvas(raw, fallbackTitle) {
642
+ const base = emptyCanvas(fallbackTitle);
643
+ if (!raw || typeof raw !== 'object') return base;
644
+ const out = { ...base, ...raw };
645
+ out.title = typeof raw.title === 'string' ? raw.title : base.title;
646
+ out.elements = Array.isArray(raw.elements)
647
+ ? raw.elements.map(sanitizeElement).filter(Boolean)
648
+ : [];
649
+ out.connections = Array.isArray(raw.connections)
650
+ ? raw.connections
651
+ .map((c) => {
652
+ if (!c || typeof c !== 'object') return null;
653
+ return {
654
+ id: typeof c.id === 'string' ? c.id : genId('conn'),
655
+ from: c.from || c.fromElementId || null,
656
+ to: c.to || c.toElementId || null,
657
+ label: typeof c.label === 'string' ? c.label : '',
658
+ };
659
+ })
660
+ .filter((c) => c.from && c.to)
661
+ : [];
662
+ out.comments = Array.isArray(raw.comments)
663
+ ? raw.comments
664
+ .map((c) => {
665
+ if (!c || typeof c !== 'object') return null;
666
+ // v3.5.4 (bug: pin) — Preserve canvas-pin coordinates when
667
+ // re-reading a plan. `pin: {x, y}` and top-level `x`/`y` both
668
+ // round-trip; we normalize to top-level on the stored shape.
669
+ const pinX = c.pin?.x ?? c.x;
670
+ const pinY = c.pin?.y ?? c.y;
671
+ const next = {
672
+ id: typeof c.id === 'string' ? c.id : genId('cmt'),
673
+ elementId: c.elementId || null,
674
+ author: typeof c.author === 'string' ? c.author : 'drb0rk',
675
+ text: typeof c.text === 'string' ? c.text : '',
676
+ created: typeof c.created === 'string' ? c.created : new Date().toISOString(),
677
+ thread: Array.isArray(c.thread) ? c.thread : [],
678
+ };
679
+ if (Number.isFinite(pinX)) next.x = pinX;
680
+ if (Number.isFinite(pinY)) next.y = pinY;
681
+ return next;
682
+ })
683
+ .filter((c) => c.text)
684
+ : [];
685
+ if (!raw.viewport || typeof raw.viewport !== 'object') {
686
+ out.viewport = { x: 0, y: 0, zoom: 1 };
687
+ } else {
688
+ out.viewport = {
689
+ x: Number.isFinite(raw.viewport.x) ? raw.viewport.x : 0,
690
+ y: Number.isFinite(raw.viewport.y) ? raw.viewport.y : 0,
691
+ zoom: Number.isFinite(raw.viewport.zoom) ? raw.viewport.zoom : 1,
692
+ };
693
+ }
694
+ return out;
695
+ }
696
+
697
+ function sanitizeElement(el) {
698
+ if (!el || typeof el !== 'object') return null;
699
+ return {
700
+ id: typeof el.id === 'string' ? el.id : genId('el'),
701
+ type: typeof el.type === 'string' ? el.type : 'note',
702
+ title: typeof el.title === 'string' ? el.title : '',
703
+ content: typeof el.content === 'string' ? el.content : '',
704
+ x: Number.isFinite(el.x) ? el.x : 0,
705
+ y: Number.isFinite(el.y) ? el.y : 0,
706
+ width: Number.isFinite(el.width) ? el.width : 240,
707
+ height: Number.isFinite(el.height) ? el.height : 120,
708
+ status: typeof el.status === 'string' ? el.status : 'open',
709
+ };
710
+ }
711
+
712
+
713
+ /**
714
+ * Best-effort artifact-slug extraction from a bg agent's assistant
715
+ * message. Looks for backtick-quoted paths under `artifacts/<slug>/`
716
+ * or for `<artifact-slug>` mentions. Returns the slug string or null.
717
+ *
718
+ * v3.19.0: New export so bg-poller.mjs compiles. The previous Plans
719
+ * store had a similar helper; this is a clean re-implementation.
720
+ */
721
+ export function extractArtifactFromMessage(text) {
722
+ if (!text || typeof text !== 'string') return null;
723
+ // Match backtick-wrapped slugs first: `artifacts/foo` or `my-plan`
724
+ const backtick = text.match(/`([a-z0-9][a-z0-9_-]{1,40})`/i);
725
+ if (backtick) return backtick[1];
726
+ // Match <artifact-slug> style tags
727
+ const angle = text.match(/<\s*([a-z0-9][a-z0-9_-]{1,40})\s*>/i);
728
+ if (angle) return angle[1];
729
+ // Match `bizar artifact new <slug>` command-like lines
730
+ const cmd = text.match(/(?:artifact|plan)\s+(?:new|create)\s+([a-z0-9][a-z0-9_-]{1,40})/i);
731
+ if (cmd) return cmd[1];
732
+ return null;
733
+ }