@aion0/forge 0.5.26 → 0.5.28
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/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
- package/RELEASE_NOTES.md +10 -29
- package/app/api/terminal-bell/route.ts +6 -2
- package/app/api/terminal-cwd/route.ts +7 -4
- package/components/CodeViewer.tsx +3 -31
- package/components/Dashboard.tsx +34 -20
- package/components/WebTerminal.tsx +36 -2
- package/lib/terminal-standalone.ts +19 -2
- package/package.json +1 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage Scanner — scans Claude Code JSONL session files for token usage data.
|
|
3
|
+
* Stores per-day aggregated results in SQLite for accurate daily breakdown.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
7
|
+
import { join, basename } from 'node:path';
|
|
8
|
+
import { getDb } from '@/src/core/db/database';
|
|
9
|
+
import { getDbPath } from '@/src/config';
|
|
10
|
+
import { getClaudeDir } from './dirs';
|
|
11
|
+
|
|
12
|
+
function db() { return getDb(getDbPath()); }
|
|
13
|
+
|
|
14
|
+
const PRICING: Record<string, { input: number; output: number }> = {
|
|
15
|
+
'claude-opus-4': { input: 15, output: 75 },
|
|
16
|
+
'claude-sonnet-4': { input: 3, output: 15 },
|
|
17
|
+
'claude-haiku-4': { input: 0.80, output: 4 },
|
|
18
|
+
'default': { input: 3, output: 15 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getModelFamily(model: string): string {
|
|
22
|
+
if (!model) return 'unknown';
|
|
23
|
+
if (model.includes('opus')) return 'claude-opus-4';
|
|
24
|
+
if (model.includes('haiku')) return 'claude-haiku-4';
|
|
25
|
+
if (model.includes('sonnet')) return 'claude-sonnet-4';
|
|
26
|
+
return 'unknown';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function calcCost(family: string, input: number, output: number): number {
|
|
30
|
+
const p = PRICING[family] || PRICING['default'];
|
|
31
|
+
// Only count input + output tokens. Cache tokens excluded from cost estimate
|
|
32
|
+
// because subscriptions (Max/Pro) don't charge per-token for cache.
|
|
33
|
+
return (input * p.input / 1_000_000) + (output * p.output / 1_000_000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function dirToProjectPath(dirName: string): string {
|
|
37
|
+
return dirName.replace(/^-/, '/').replace(/-/g, '/');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dirToProjectName(dirName: string): string {
|
|
41
|
+
return dirToProjectPath(dirName).split('/').pop() || dirName;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Get local date string from UTC timestamp */
|
|
45
|
+
function toLocalDate(ts: string): string {
|
|
46
|
+
if (!ts) return 'unknown';
|
|
47
|
+
try {
|
|
48
|
+
const d = new Date(ts);
|
|
49
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
50
|
+
} catch {
|
|
51
|
+
return ts.slice(0, 10) || 'unknown';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface DayModelBucket {
|
|
56
|
+
input: number; output: number; cacheRead: number; cacheCreate: number; count: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Parse JSONL and aggregate by day + model */
|
|
60
|
+
function parseByDayModel(content: string): Map<string, DayModelBucket> {
|
|
61
|
+
// key: "day|model"
|
|
62
|
+
const buckets = new Map<string, DayModelBucket>();
|
|
63
|
+
|
|
64
|
+
for (const line of content.split('\n')) {
|
|
65
|
+
if (!line.trim()) continue;
|
|
66
|
+
try {
|
|
67
|
+
const obj = JSON.parse(line);
|
|
68
|
+
if (obj.type === 'assistant' && obj.message?.usage) {
|
|
69
|
+
const u = obj.message.usage;
|
|
70
|
+
const model = getModelFamily(obj.message.model || '');
|
|
71
|
+
const day = toLocalDate(obj.timestamp || '');
|
|
72
|
+
const key = `${day}|${model}`;
|
|
73
|
+
|
|
74
|
+
let b = buckets.get(key);
|
|
75
|
+
if (!b) { b = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0, count: 0 }; buckets.set(key, b); }
|
|
76
|
+
b.input += u.input_tokens || 0;
|
|
77
|
+
b.output += u.output_tokens || 0;
|
|
78
|
+
b.cacheRead += u.cache_read_input_tokens || 0;
|
|
79
|
+
b.cacheCreate += u.cache_creation_input_tokens || 0;
|
|
80
|
+
b.count++;
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
return buckets;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Scan all JSONL files */
|
|
88
|
+
export function scanUsage(): { scanned: number; updated: number; errors: number } {
|
|
89
|
+
const projectsDir = join(getClaudeDir(), 'projects');
|
|
90
|
+
let scanned = 0, updated = 0, errors = 0;
|
|
91
|
+
|
|
92
|
+
let projectDirs: string[];
|
|
93
|
+
try { projectDirs = readdirSync(projectsDir); } catch { return { scanned: 0, updated: 0, errors: 0 }; }
|
|
94
|
+
|
|
95
|
+
const upsert = db().prepare(`
|
|
96
|
+
INSERT INTO token_usage (session_id, source, project_path, project_name, model, day, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cost_usd, message_count)
|
|
97
|
+
VALUES (?, 'terminal', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
98
|
+
ON CONFLICT(session_id, source, model, day) DO UPDATE SET
|
|
99
|
+
input_tokens = excluded.input_tokens, output_tokens = excluded.output_tokens,
|
|
100
|
+
cache_read_tokens = excluded.cache_read_tokens, cache_create_tokens = excluded.cache_create_tokens,
|
|
101
|
+
cost_usd = excluded.cost_usd, message_count = excluded.message_count
|
|
102
|
+
`);
|
|
103
|
+
|
|
104
|
+
const getScanState = db().prepare('SELECT last_size FROM usage_scan_state WHERE file_path = ?');
|
|
105
|
+
const setScanState = db().prepare(`
|
|
106
|
+
INSERT INTO usage_scan_state (file_path, last_size) VALUES (?, ?)
|
|
107
|
+
ON CONFLICT(file_path) DO UPDATE SET last_size = excluded.last_size, last_scan = datetime('now')
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
for (const projDir of projectDirs) {
|
|
111
|
+
const projPath = join(projectsDir, projDir);
|
|
112
|
+
try { if (!statSync(projPath).isDirectory()) continue; } catch { continue; }
|
|
113
|
+
|
|
114
|
+
const projectPath = dirToProjectPath(projDir);
|
|
115
|
+
const projectName = dirToProjectName(projDir);
|
|
116
|
+
let files: string[];
|
|
117
|
+
try { files = readdirSync(projPath).filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-')); } catch { continue; }
|
|
118
|
+
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
const filePath = join(projPath, file);
|
|
121
|
+
const sessionId = basename(file, '.jsonl');
|
|
122
|
+
scanned++;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const currentSize = statSync(filePath).size;
|
|
126
|
+
const scanState = getScanState.get(filePath) as { last_size: number } | undefined;
|
|
127
|
+
if (currentSize === (scanState?.last_size || 0)) continue;
|
|
128
|
+
|
|
129
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
130
|
+
const buckets = parseByDayModel(content);
|
|
131
|
+
|
|
132
|
+
if (buckets.size === 0) { setScanState.run(filePath, currentSize); continue; }
|
|
133
|
+
|
|
134
|
+
for (const [key, b] of buckets) {
|
|
135
|
+
const [day, model] = key.split('|');
|
|
136
|
+
const cost = calcCost(model, b.input, b.output);
|
|
137
|
+
upsert.run(sessionId, projectPath, projectName, model, day, b.input, b.output, b.cacheRead, b.cacheCreate, cost, b.count);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setScanState.run(filePath, currentSize);
|
|
141
|
+
updated++;
|
|
142
|
+
} catch { errors++; }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { scanned, updated, errors };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Record usage from task/mobile/pipeline */
|
|
149
|
+
export function recordUsage(opts: {
|
|
150
|
+
sessionId: string;
|
|
151
|
+
source: 'task' | 'mobile' | 'pipeline';
|
|
152
|
+
projectPath: string;
|
|
153
|
+
projectName: string;
|
|
154
|
+
model: string;
|
|
155
|
+
inputTokens: number;
|
|
156
|
+
outputTokens: number;
|
|
157
|
+
cacheReadTokens?: number;
|
|
158
|
+
cacheCreateTokens?: number;
|
|
159
|
+
taskId?: string;
|
|
160
|
+
}): void {
|
|
161
|
+
const family = getModelFamily(opts.model);
|
|
162
|
+
const cost = calcCost(family, opts.inputTokens, opts.outputTokens);
|
|
163
|
+
const day = toLocalDate(new Date().toISOString());
|
|
164
|
+
|
|
165
|
+
db().prepare(`
|
|
166
|
+
INSERT INTO token_usage (session_id, source, project_path, project_name, model, day, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cost_usd, message_count, task_id)
|
|
167
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
|
|
168
|
+
ON CONFLICT(session_id, source, model, day) DO UPDATE SET
|
|
169
|
+
input_tokens = token_usage.input_tokens + excluded.input_tokens,
|
|
170
|
+
output_tokens = token_usage.output_tokens + excluded.output_tokens,
|
|
171
|
+
cost_usd = token_usage.cost_usd + excluded.cost_usd,
|
|
172
|
+
message_count = token_usage.message_count + 1
|
|
173
|
+
`).run(opts.sessionId, opts.source, opts.projectPath, opts.projectName, family, day, opts.inputTokens, opts.outputTokens, opts.cacheReadTokens || 0, opts.cacheCreateTokens || 0, cost, opts.taskId || null);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Query usage data */
|
|
177
|
+
export function queryUsage(opts: {
|
|
178
|
+
days?: number;
|
|
179
|
+
projectName?: string;
|
|
180
|
+
source?: string;
|
|
181
|
+
model?: string;
|
|
182
|
+
}): {
|
|
183
|
+
total: { input: number; output: number; cost: number; sessions: number; messages: number };
|
|
184
|
+
byProject: { name: string; input: number; output: number; cost: number; sessions: number }[];
|
|
185
|
+
byModel: { model: string; input: number; output: number; cost: number; messages: number }[];
|
|
186
|
+
byDay: { date: string; input: number; output: number; cost: number }[];
|
|
187
|
+
bySource: { source: string; input: number; output: number; cost: number; messages: number }[];
|
|
188
|
+
} {
|
|
189
|
+
let where = '1=1';
|
|
190
|
+
const params: any[] = [];
|
|
191
|
+
|
|
192
|
+
if (opts.days) {
|
|
193
|
+
const cutoff = new Date();
|
|
194
|
+
cutoff.setDate(cutoff.getDate() - opts.days);
|
|
195
|
+
const cutoffDay = `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, '0')}-${String(cutoff.getDate()).padStart(2, '0')}`;
|
|
196
|
+
where += ' AND day >= ?';
|
|
197
|
+
params.push(cutoffDay);
|
|
198
|
+
}
|
|
199
|
+
if (opts.projectName) { where += ' AND project_name = ?'; params.push(opts.projectName); }
|
|
200
|
+
if (opts.source) { where += ' AND source = ?'; params.push(opts.source); }
|
|
201
|
+
if (opts.model) { where += ' AND model = ?'; params.push(opts.model); }
|
|
202
|
+
|
|
203
|
+
const totalRow = db().prepare(`
|
|
204
|
+
SELECT COALESCE(SUM(input_tokens), 0) as input, COALESCE(SUM(output_tokens), 0) as output,
|
|
205
|
+
COALESCE(SUM(cost_usd), 0) as cost, COUNT(DISTINCT session_id) as sessions,
|
|
206
|
+
COALESCE(SUM(message_count), 0) as messages
|
|
207
|
+
FROM token_usage WHERE ${where}
|
|
208
|
+
`).get(...params) as any;
|
|
209
|
+
|
|
210
|
+
const byProject = (db().prepare(`
|
|
211
|
+
SELECT project_name as name, SUM(input_tokens) as input, SUM(output_tokens) as output,
|
|
212
|
+
SUM(cost_usd) as cost, COUNT(DISTINCT session_id) as sessions
|
|
213
|
+
FROM token_usage WHERE ${where}
|
|
214
|
+
GROUP BY project_name ORDER BY cost DESC LIMIT 20
|
|
215
|
+
`).all(...params) as any[]).map(r => ({
|
|
216
|
+
name: r.name, input: r.input, output: r.output, cost: +r.cost.toFixed(4), sessions: r.sessions,
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
const byModel = (db().prepare(`
|
|
220
|
+
SELECT model, SUM(input_tokens) as input, SUM(output_tokens) as output,
|
|
221
|
+
SUM(cost_usd) as cost, SUM(message_count) as messages
|
|
222
|
+
FROM token_usage WHERE ${where}
|
|
223
|
+
GROUP BY model ORDER BY cost DESC
|
|
224
|
+
`).all(...params) as any[]).map(r => ({
|
|
225
|
+
model: r.model, input: r.input, output: r.output, cost: +r.cost.toFixed(4), messages: r.messages,
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
const byDay = (db().prepare(`
|
|
229
|
+
SELECT day as date, SUM(input_tokens) as input, SUM(output_tokens) as output, SUM(cost_usd) as cost
|
|
230
|
+
FROM token_usage WHERE ${where} AND day != 'unknown'
|
|
231
|
+
GROUP BY day ORDER BY day DESC LIMIT 30
|
|
232
|
+
`).all(...params) as any[]).map(r => ({
|
|
233
|
+
date: r.date, input: r.input, output: r.output, cost: +r.cost.toFixed(4),
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
const bySource = (db().prepare(`
|
|
237
|
+
SELECT source, SUM(input_tokens) as input, SUM(output_tokens) as output,
|
|
238
|
+
SUM(cost_usd) as cost, SUM(message_count) as messages
|
|
239
|
+
FROM token_usage WHERE ${where}
|
|
240
|
+
GROUP BY source ORDER BY cost DESC
|
|
241
|
+
`).all(...params) as any[]).map(r => ({
|
|
242
|
+
source: r.source, input: r.input, output: r.output, cost: +r.cost.toFixed(4), messages: r.messages,
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
total: { input: totalRow.input, output: totalRow.output, cost: +totalRow.cost.toFixed(4), sessions: totalRow.sessions, messages: totalRow.messages },
|
|
247
|
+
byProject, byModel, byDay, bySource,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Machine Tests — verify smith/task status transitions and message flow.
|
|
3
|
+
*
|
|
4
|
+
* Usage: npx tsx lib/workspace/__tests__/state-machine.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import { WorkspaceOrchestrator } from '../orchestrator';
|
|
9
|
+
import { AgentBus } from '../agent-bus';
|
|
10
|
+
import type { WorkspaceAgentConfig, AgentState, BusMessage, WorkerEvent } from '../types';
|
|
11
|
+
|
|
12
|
+
const TEST_WS = 'test-sm-' + Date.now();
|
|
13
|
+
const TEST_PATH = '/tmp/test-sm';
|
|
14
|
+
|
|
15
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function createOrch(): WorkspaceOrchestrator {
|
|
18
|
+
return new WorkspaceOrchestrator(TEST_WS, TEST_PATH, 'test');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addInput(orch: WorkspaceOrchestrator, id = 'input-1'): void {
|
|
22
|
+
orch.addAgent({
|
|
23
|
+
id, label: 'Requirements', icon: '📝', type: 'input',
|
|
24
|
+
content: '', entries: [], role: '', backend: 'cli',
|
|
25
|
+
dependsOn: [], outputs: [], steps: [],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function addAgent(orch: WorkspaceOrchestrator, id: string, label: string, dependsOn: string[]): void {
|
|
30
|
+
orch.addAgent({
|
|
31
|
+
id, label, icon: '🤖', role: 'test', backend: 'cli',
|
|
32
|
+
dependsOn, outputs: [], workDir: id, // unique workDir to avoid conflicts
|
|
33
|
+
steps: [{ id: 's1', label: 'Step 1', prompt: 'do something' }],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getState(orch: WorkspaceOrchestrator, id: string): AgentState {
|
|
38
|
+
return orch.getAllAgentStates()[id];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectEvents(orch: WorkspaceOrchestrator): any[] {
|
|
42
|
+
const events: any[] = [];
|
|
43
|
+
orch.on('event', (e: any) => events.push(e));
|
|
44
|
+
return events;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let passed = 0;
|
|
48
|
+
let failed = 0;
|
|
49
|
+
let testNum = 0;
|
|
50
|
+
|
|
51
|
+
function ok(condition: boolean, msg: string) {
|
|
52
|
+
if (condition) {
|
|
53
|
+
console.log(` ✅ ${msg}`);
|
|
54
|
+
passed++;
|
|
55
|
+
} else {
|
|
56
|
+
console.log(` ❌ ${msg}`);
|
|
57
|
+
failed++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function test(name: string, fn: () => void | Promise<void>) {
|
|
62
|
+
testNum++;
|
|
63
|
+
console.log(`\n📋 Test ${testNum}: ${name}`);
|
|
64
|
+
try {
|
|
65
|
+
const result = fn();
|
|
66
|
+
if (result instanceof Promise) {
|
|
67
|
+
return result.catch(e => {
|
|
68
|
+
console.log(` 💥 Crashed: ${e.message}`);
|
|
69
|
+
failed++;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
} catch (e: any) {
|
|
73
|
+
console.log(` 💥 Crashed: ${e.message}`);
|
|
74
|
+
failed++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Tests ───────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
async function runAll() {
|
|
81
|
+
console.log('🧪 State Machine Tests\n');
|
|
82
|
+
|
|
83
|
+
// Test 1: Initial state
|
|
84
|
+
await test('Initial agent state', () => {
|
|
85
|
+
const orch = createOrch();
|
|
86
|
+
addAgent(orch, 'a1', 'Agent1', []);
|
|
87
|
+
const s = getState(orch, 'a1');
|
|
88
|
+
ok(s.smithStatus === 'down', `smithStatus = down (got ${s.smithStatus})`);
|
|
89
|
+
ok(s.taskStatus === 'idle', `taskStatus = idle (got ${s.taskStatus})`);
|
|
90
|
+
// mode field removed — execution method determined by tmuxSession presence
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Test 2: startDaemon sets all smiths to active
|
|
94
|
+
await test('startDaemon sets smithStatus=active', async () => {
|
|
95
|
+
const orch = createOrch();
|
|
96
|
+
addInput(orch);
|
|
97
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
98
|
+
addAgent(orch, 'eng', 'Engineer', ['pm']);
|
|
99
|
+
|
|
100
|
+
ok(getState(orch, 'pm').smithStatus === 'down', 'PM starts down');
|
|
101
|
+
ok(getState(orch, 'eng').smithStatus === 'down', 'Engineer starts down');
|
|
102
|
+
|
|
103
|
+
await orch.startDaemon();
|
|
104
|
+
|
|
105
|
+
ok(getState(orch, 'pm').smithStatus === 'active', 'PM active after startDaemon');
|
|
106
|
+
ok(getState(orch, 'eng').smithStatus === 'active', 'Engineer active after startDaemon');
|
|
107
|
+
ok(orch.isDaemonActive(), 'daemonActive = true');
|
|
108
|
+
|
|
109
|
+
orch.stopDaemon();
|
|
110
|
+
ok(getState(orch, 'pm').smithStatus === 'down', 'PM down after stopDaemon');
|
|
111
|
+
ok(getState(orch, 'eng').smithStatus === 'down', 'Engineer down after stopDaemon');
|
|
112
|
+
ok(!orch.isDaemonActive(), 'daemonActive = false');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Test 3: stopDaemon preserves taskStatus
|
|
116
|
+
await test('stopDaemon preserves taskStatus', async () => {
|
|
117
|
+
const orch = createOrch();
|
|
118
|
+
addAgent(orch, 'a1', 'Agent1', []);
|
|
119
|
+
|
|
120
|
+
// Manually set to done to simulate completed agent
|
|
121
|
+
const states = orch.getAllAgentStates();
|
|
122
|
+
(states['a1'] as any).taskStatus = 'done';
|
|
123
|
+
// Hack: directly modify internal state for testing
|
|
124
|
+
(orch as any).agents.get('a1').state.taskStatus = 'done';
|
|
125
|
+
|
|
126
|
+
await orch.startDaemon();
|
|
127
|
+
ok(getState(orch, 'a1').taskStatus === 'done', 'taskStatus stays done after startDaemon');
|
|
128
|
+
|
|
129
|
+
orch.stopDaemon();
|
|
130
|
+
ok(getState(orch, 'a1').taskStatus === 'done', 'taskStatus stays done after stopDaemon');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Test 4: loadSnapshot resets smith to down and pending messages to failed
|
|
134
|
+
await test('loadSnapshot resets state correctly', () => {
|
|
135
|
+
const orch = createOrch();
|
|
136
|
+
|
|
137
|
+
const busLog: BusMessage[] = [
|
|
138
|
+
{ id: 'm1', from: 'a', to: 'b', type: 'notify', payload: { action: 'test' }, timestamp: Date.now(), status: 'pending' },
|
|
139
|
+
{ id: 'm2', from: 'a', to: 'b', type: 'notify', payload: { action: 'test2' }, timestamp: Date.now(), status: 'done' },
|
|
140
|
+
{ id: 'm3', from: 'a', to: 'b', type: 'notify', payload: { action: 'test3' }, timestamp: Date.now(), status: 'pending' },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
orch.loadSnapshot({
|
|
144
|
+
agents: [{
|
|
145
|
+
id: 'a1', label: 'Agent1', icon: '🤖', role: '', backend: 'cli',
|
|
146
|
+
dependsOn: [], outputs: [], workDir: 'a1',
|
|
147
|
+
steps: [{ id: 's1', label: 'Step1', prompt: 'test' }],
|
|
148
|
+
}],
|
|
149
|
+
agentStates: {
|
|
150
|
+
'a1': {
|
|
151
|
+
smithStatus: 'active', taskStatus: 'running',
|
|
152
|
+
history: [], artifacts: [],
|
|
153
|
+
} as AgentState,
|
|
154
|
+
},
|
|
155
|
+
busLog,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const s = getState(orch, 'a1');
|
|
159
|
+
ok(s.smithStatus === 'down', 'smithStatus reset to down after load');
|
|
160
|
+
ok(s.taskStatus === 'failed', 'running taskStatus becomes failed after load');
|
|
161
|
+
|
|
162
|
+
// Check bus messages
|
|
163
|
+
const log = orch.getBusLog();
|
|
164
|
+
const m1 = log.find(m => m.id === 'm1');
|
|
165
|
+
const m2 = log.find(m => m.id === 'm2');
|
|
166
|
+
const m3 = log.find(m => m.id === 'm3');
|
|
167
|
+
ok(m1?.status === 'failed', 'pending message m1 marked failed');
|
|
168
|
+
ok(m2?.status === 'done', 'acked message m2 unchanged');
|
|
169
|
+
ok(m3?.status === 'failed', 'pending message m3 marked failed');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Test 5: completeInput sends input_updated messages (no daemon to avoid CLI spawn)
|
|
173
|
+
await test('completeInput sends bus messages to downstream', () => {
|
|
174
|
+
const orch = createOrch();
|
|
175
|
+
addInput(orch);
|
|
176
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
177
|
+
|
|
178
|
+
// Don't start daemon — just test that completeInput sends bus messages
|
|
179
|
+
const busLog = orch.getBusLog();
|
|
180
|
+
const before = busLog.length;
|
|
181
|
+
|
|
182
|
+
orch.completeInput('input-1', 'Build a todo app');
|
|
183
|
+
|
|
184
|
+
const after = busLog.length;
|
|
185
|
+
ok(after > before, `New bus messages sent (${before} → ${after})`);
|
|
186
|
+
|
|
187
|
+
const inputMsgs = busLog.filter(m => m.payload.action === 'input_updated');
|
|
188
|
+
ok(inputMsgs.length > 0, 'input_updated message found');
|
|
189
|
+
ok(inputMsgs[0].from === 'input-1', 'from = input-1');
|
|
190
|
+
ok(inputMsgs[0].to === 'pm', 'to = pm');
|
|
191
|
+
// Smith is down so message stays pending (not acked)
|
|
192
|
+
ok(inputMsgs[0].status === 'pending', `message pending (smith down) — got ${inputMsgs[0].status}`);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Test 6: broadcastCompletion sends upstream_complete (no daemon)
|
|
196
|
+
await test('broadcastCompletion sends upstream_complete', () => {
|
|
197
|
+
const orch = createOrch();
|
|
198
|
+
addInput(orch);
|
|
199
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
200
|
+
addAgent(orch, 'eng', 'Engineer', ['pm']);
|
|
201
|
+
|
|
202
|
+
// Set PM to done with artifacts
|
|
203
|
+
(orch as any).agents.get('pm').state.taskStatus = 'done';
|
|
204
|
+
(orch as any).agents.get('pm').state.artifacts = [{ type: 'file', path: 'docs/prd.md' }];
|
|
205
|
+
(orch as any).agents.get('pm').state.history = [
|
|
206
|
+
{ type: 'result', subtype: 'final_summary', content: 'PRD completed', timestamp: new Date().toISOString() }
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
(orch as any).broadcastCompletion('pm');
|
|
210
|
+
|
|
211
|
+
const busLog = orch.getBusLog();
|
|
212
|
+
const upstreamMsgs = busLog.filter(m => m.payload.action === 'upstream_complete' && m.from === 'pm');
|
|
213
|
+
ok(upstreamMsgs.length > 0, 'upstream_complete message sent');
|
|
214
|
+
ok(upstreamMsgs[0].to === 'eng', 'sent to Engineer');
|
|
215
|
+
ok(upstreamMsgs[0].payload.files?.includes('docs/prd.md') === true, 'includes file path');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Test 7: Bus message ACK flow
|
|
219
|
+
await test('Bus message ACK: pending → acked on success, pending → failed on error', () => {
|
|
220
|
+
const bus = new AgentBus();
|
|
221
|
+
bus.setAgentStatus('a', 'alive');
|
|
222
|
+
bus.setAgentStatus('b', 'alive');
|
|
223
|
+
|
|
224
|
+
const msg = bus.send('a', 'b', 'notify', { action: 'test', content: 'hello' });
|
|
225
|
+
ok(msg.status === 'pending', 'starts as pending');
|
|
226
|
+
|
|
227
|
+
// Simulate ack
|
|
228
|
+
msg.status = 'done';
|
|
229
|
+
ok(msg.status === 'done', 'acked after processing');
|
|
230
|
+
|
|
231
|
+
// Test failed path
|
|
232
|
+
const msg2 = bus.send('a', 'b', 'notify', { action: 'test2', content: 'world' });
|
|
233
|
+
msg2.status = 'failed';
|
|
234
|
+
ok(msg2.status === 'failed', 'failed on error');
|
|
235
|
+
|
|
236
|
+
// Test retry
|
|
237
|
+
const retried = bus.retryMessage(msg2.id);
|
|
238
|
+
ok(retried !== null, 'retryMessage returns message');
|
|
239
|
+
ok(retried!.status === 'pending', 'retried message back to pending');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Test 8: markAllPendingAsFailed
|
|
243
|
+
await test('markAllPendingAsFailed on restart', () => {
|
|
244
|
+
const bus = new AgentBus();
|
|
245
|
+
bus.setAgentStatus('a', 'alive');
|
|
246
|
+
bus.setAgentStatus('b', 'alive');
|
|
247
|
+
|
|
248
|
+
const m1 = bus.send('a', 'b', 'notify', { action: 'test1' });
|
|
249
|
+
const m2 = bus.send('a', 'b', 'notify', { action: 'test2' });
|
|
250
|
+
m1.status = 'done'; // already processed
|
|
251
|
+
// m2 stays pending
|
|
252
|
+
|
|
253
|
+
bus.markAllPendingAsFailed();
|
|
254
|
+
|
|
255
|
+
ok(m1.status === 'done', 'acked message unchanged');
|
|
256
|
+
ok(m2.status === 'failed', 'pending message marked failed');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Test 9: Manual run (force) dep check logic — test validateCanRun vs force dep check
|
|
260
|
+
await test('Force run checks smith active, normal run checks taskStatus done', () => {
|
|
261
|
+
const orch = createOrch();
|
|
262
|
+
addInput(orch);
|
|
263
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
264
|
+
addAgent(orch, 'eng', 'Engineer', ['pm']);
|
|
265
|
+
|
|
266
|
+
// Normal validateCanRun: PM idle → should fail
|
|
267
|
+
let error1 = '';
|
|
268
|
+
try { orch.validateCanRun('eng'); } catch (e: any) { error1 = e.message; }
|
|
269
|
+
ok(error1.includes('not completed'), `Normal: rejects when PM idle`);
|
|
270
|
+
|
|
271
|
+
// PM done → should pass
|
|
272
|
+
(orch as any).agents.get('pm').state.taskStatus = 'done';
|
|
273
|
+
let error2 = '';
|
|
274
|
+
try { orch.validateCanRun('eng'); } catch (e: any) { error2 = e.message; }
|
|
275
|
+
ok(!error2, `Normal: passes when PM done`);
|
|
276
|
+
|
|
277
|
+
// Force dep check: PM smith down → should fail
|
|
278
|
+
(orch as any).agents.get('pm').state.taskStatus = 'idle';
|
|
279
|
+
(orch as any).agents.get('pm').state.smithStatus = 'down';
|
|
280
|
+
// Simulate the force dep check from runAgentDaemon
|
|
281
|
+
const config = (orch as any).agents.get('eng').config;
|
|
282
|
+
let forceError = '';
|
|
283
|
+
for (const depId of config.dependsOn) {
|
|
284
|
+
const dep = (orch as any).agents.get(depId);
|
|
285
|
+
if (dep && dep.config.type !== 'input' && dep.state.smithStatus !== 'active') {
|
|
286
|
+
forceError = `${dep.config.label} smith not active`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
ok(forceError.includes('not active'), `Force: rejects when PM smith down`);
|
|
290
|
+
|
|
291
|
+
// PM smith active → should pass
|
|
292
|
+
(orch as any).agents.get('pm').state.smithStatus = 'active';
|
|
293
|
+
forceError = '';
|
|
294
|
+
for (const depId of config.dependsOn) {
|
|
295
|
+
const dep = (orch as any).agents.get(depId);
|
|
296
|
+
if (dep && dep.config.type !== 'input' && dep.state.smithStatus !== 'active') {
|
|
297
|
+
forceError = `${dep.config.label} smith not active`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
ok(!forceError, `Force: passes when PM smith active`);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Test 10: Auto trigger requires taskStatus=done (check via validateCanRun)
|
|
304
|
+
await test('Auto trigger requires dep taskStatus=done', () => {
|
|
305
|
+
const orch = createOrch();
|
|
306
|
+
addInput(orch);
|
|
307
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
308
|
+
addAgent(orch, 'eng', 'Engineer', ['pm']);
|
|
309
|
+
|
|
310
|
+
// PM is idle — validateCanRun for Engineer should fail
|
|
311
|
+
let error = '';
|
|
312
|
+
try {
|
|
313
|
+
orch.validateCanRun('eng');
|
|
314
|
+
} catch (e: any) {
|
|
315
|
+
error = e.message;
|
|
316
|
+
}
|
|
317
|
+
ok(error.includes('not completed'), `validateCanRun rejects when PM idle: ${error}`);
|
|
318
|
+
|
|
319
|
+
// Set PM to done — should pass
|
|
320
|
+
(orch as any).agents.get('pm').state.taskStatus = 'done';
|
|
321
|
+
let error2 = '';
|
|
322
|
+
try {
|
|
323
|
+
orch.validateCanRun('eng');
|
|
324
|
+
} catch (e: any) {
|
|
325
|
+
error2 = e.message;
|
|
326
|
+
}
|
|
327
|
+
ok(!error2, `validateCanRun passes when PM done (got: ${error2 || 'no error'})`);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Test 11: taskStatus transitions - never goes from done to idle
|
|
331
|
+
await test('taskStatus never goes from done→idle', async () => {
|
|
332
|
+
const orch = createOrch();
|
|
333
|
+
addAgent(orch, 'a1', 'Agent1', []);
|
|
334
|
+
|
|
335
|
+
// Set to done
|
|
336
|
+
(orch as any).agents.get('a1').state.taskStatus = 'done';
|
|
337
|
+
(orch as any).agents.get('a1').state.smithStatus = 'active';
|
|
338
|
+
|
|
339
|
+
// Stop daemon should NOT change taskStatus
|
|
340
|
+
await orch.startDaemon();
|
|
341
|
+
orch.stopDaemon();
|
|
342
|
+
|
|
343
|
+
ok(getState(orch, 'a1').taskStatus === 'done',
|
|
344
|
+
`taskStatus stays done after stop (got ${getState(orch, 'a1').taskStatus})`);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Test 12: startDaemon doesn't re-send input messages
|
|
348
|
+
await test('startDaemon does NOT broadcast existing input', async () => {
|
|
349
|
+
const orch = createOrch();
|
|
350
|
+
addInput(orch);
|
|
351
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
352
|
+
|
|
353
|
+
// Complete input first
|
|
354
|
+
orch.completeInput('input-1', 'test content');
|
|
355
|
+
|
|
356
|
+
const busLogBefore = orch.getBusLog().length;
|
|
357
|
+
|
|
358
|
+
await orch.startDaemon();
|
|
359
|
+
|
|
360
|
+
const busLogAfter = orch.getBusLog().length;
|
|
361
|
+
const newMsgs = orch.getBusLog().slice(busLogBefore);
|
|
362
|
+
const inputMsgs = newMsgs.filter(m => m.payload.action === 'input_updated');
|
|
363
|
+
|
|
364
|
+
ok(inputMsgs.length === 0, `No input_updated sent by startDaemon (got ${inputMsgs.length})`);
|
|
365
|
+
|
|
366
|
+
orch.stopDaemon();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ─── Summary ──────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
console.log('\n' + '═'.repeat(40));
|
|
372
|
+
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
373
|
+
console.log('═'.repeat(40));
|
|
374
|
+
|
|
375
|
+
console.log('');
|
|
376
|
+
if (failed > 0) {
|
|
377
|
+
process.exit(1);
|
|
378
|
+
} else {
|
|
379
|
+
process.exit(0);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Force exit after 8s in case of dangling timers/workers
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
console.log('\n(Force exit due to dangling timers)');
|
|
386
|
+
process.exit(passed > 0 && failed === 0 ? 0 : 1);
|
|
387
|
+
}, 8000);
|
|
388
|
+
runAll();
|