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