@aion0/forge 0.5.26 → 0.5.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
  2. package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
  3. package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
  4. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
  5. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
  6. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
  7. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
  8. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
  9. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
  10. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
  11. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
  12. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
  13. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
  14. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
  15. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
  16. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
  17. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
  18. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
  19. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
  20. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
  21. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
  22. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
  23. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
  24. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
  25. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
  26. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
  27. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
  28. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
  29. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
  30. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
  31. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
  32. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
  33. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
  34. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
  35. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
  36. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
  37. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
  38. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
  39. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
  40. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
  41. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
  42. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
  43. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
  44. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
  45. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
  46. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
  47. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
  48. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
  49. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
  50. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
  51. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
  52. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
  53. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
  54. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
  55. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
  56. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
  57. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
  58. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
  59. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
  60. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
  61. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
  62. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
  63. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
  64. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
  65. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
  66. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
  67. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
  68. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
  69. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
  70. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
  71. package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
  72. package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
  73. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
  74. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
  75. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
  76. package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
  77. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
  78. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
  79. package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
  80. package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
  81. package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
  82. package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
  83. package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
  84. package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
  85. package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
  86. package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
  87. package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
  88. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
  89. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
  90. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
  91. package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
  92. package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
  93. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
  94. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
  95. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
  96. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
  97. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
  98. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
  99. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
  100. package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
  101. package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
  102. package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
  103. package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
  104. package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
  105. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
  106. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
  107. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
  108. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
  109. package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
  110. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
  111. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
  112. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
  113. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
  114. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
  115. package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
  116. package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
  117. package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
  118. package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
  119. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
  120. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
  121. package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
  122. package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
  123. package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
  124. package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
  125. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
  126. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
  127. package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
  128. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
  129. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
  130. package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
  131. package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
  132. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
  133. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
  134. package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
  135. package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
  136. package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
  137. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
  138. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
  139. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
  140. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
  141. package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
  142. package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
  143. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
  144. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
  145. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
  146. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
  147. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
  148. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
  149. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
  150. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
  151. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
  152. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
  153. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
  154. package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
  155. package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
  156. package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
  157. package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
  158. package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
  159. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
  160. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
  161. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
  162. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
  163. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
  164. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
  165. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
  166. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
  167. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
  168. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
  169. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
  170. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
  171. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
  172. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
  173. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
  174. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
  175. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
  176. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
  177. package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
  178. package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
  179. package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
  180. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
  181. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
  182. package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
  183. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
  184. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
  185. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
  186. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
  187. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
  188. package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
  189. package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
  190. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
  191. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
  192. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
  193. package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
  194. package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
  195. package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
  196. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
  197. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
  198. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
  199. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
  200. package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
  201. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
  202. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
  203. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
  204. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
  205. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
  206. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
  207. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
  208. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
  209. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
  210. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
  211. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
  212. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
  213. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
  214. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
  215. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
  216. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
  217. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
  218. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
  219. package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
  220. package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
  221. package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
  222. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
  223. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
  224. package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
  225. package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
  226. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
  227. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
  228. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
  229. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
  230. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
  231. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
  232. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
  233. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
  234. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
  235. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
  236. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
  237. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
  238. package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
  239. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
  240. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
  241. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
  242. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
  243. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
  244. package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
  245. package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
  246. package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
  247. package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
  248. package/RELEASE_NOTES.md +11 -28
  249. package/app/api/terminal-bell/route.ts +6 -2
  250. package/components/WebTerminal.tsx +36 -2
  251. package/lib/terminal-standalone.ts +19 -2
  252. package/next-env.d.ts +1 -1
  253. package/package.json +1 -1
@@ -0,0 +1,978 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Workspace Daemon — standalone process for managing workspace agent orchestrators.
4
+ *
5
+ * Runs as an independent HTTP server (like terminal-standalone.ts).
6
+ * Next.js API routes proxy requests here.
7
+ *
8
+ * Usage: npx tsx lib/workspace-standalone.ts [--forge-port=8403]
9
+ *
10
+ * Env:
11
+ * WORKSPACE_PORT — HTTP port (default: webPort + 2 = 8405)
12
+ * FORGE_DATA_DIR — data directory
13
+ */
14
+
15
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
16
+ import { readdirSync, statSync } from 'node:fs';
17
+ import { join, resolve } from 'node:path';
18
+ import { homedir } from 'node:os';
19
+ import { WorkspaceOrchestrator, type OrchestratorEvent } from './workspace/orchestrator';
20
+ import { loadWorkspace, saveWorkspace, findWorkspaceByProject } from './workspace/persistence';
21
+ import { installForgeSkills, applyProfileToProject } from './workspace/skill-installer';
22
+ import {
23
+ loadMemory, formatMemoryForDisplay, getMemoryStats,
24
+ addObservation, addSessionSummary,
25
+ } from './workspace/smith-memory';
26
+ import type { WorkspaceAgentConfig, WorkspaceState, BusMessage } from './workspace/types';
27
+ import { execSync } from 'node:child_process';
28
+
29
+ // ─── Config ──────────────────────────────────────────────
30
+
31
+ const PORT = Number(process.env.WORKSPACE_PORT) || 8405;
32
+ const FORGE_PORT = Number(process.env.PORT) || 8403;
33
+ const MAX_ACTIVE = 5;
34
+
35
+ // ─── State ───────────────────────────────────────────────
36
+
37
+ const orchestrators = new Map<string, WorkspaceOrchestrator>();
38
+ const sseClients = new Map<string, Set<ServerResponse>>();
39
+ const startTime = Date.now();
40
+
41
+ // ─── Orchestrator Lifecycle ──────────────────────────────
42
+
43
+ function getOrchestrator(id: string): WorkspaceOrchestrator | null {
44
+ return orchestrators.get(id) || null;
45
+ }
46
+
47
+ function loadOrchestrator(id: string): WorkspaceOrchestrator {
48
+ const existing = orchestrators.get(id);
49
+ if (existing) return existing;
50
+
51
+ const state = loadWorkspace(id);
52
+ if (!state) throw new Error('Workspace not found');
53
+
54
+ const orch = new WorkspaceOrchestrator(state.id, state.projectPath, state.projectName);
55
+ if (state.agents.length > 0) {
56
+ orch.loadSnapshot({
57
+ agents: state.agents,
58
+ agentStates: state.agentStates,
59
+ busLog: state.busLog,
60
+ busOutbox: state.busOutbox,
61
+ });
62
+ }
63
+
64
+ // Wire up SSE broadcasting
65
+ orch.on('event', (event: OrchestratorEvent) => {
66
+ broadcastSSE(id, event);
67
+ });
68
+
69
+ orchestrators.set(id, orch);
70
+ // Loaded silently — debug only if needed
71
+ return orch;
72
+ }
73
+
74
+ function unloadOrchestrator(id: string): void {
75
+ const orch = orchestrators.get(id);
76
+ if (!orch) return;
77
+ orch.shutdown();
78
+ orchestrators.delete(id);
79
+ // Close SSE connections for this workspace
80
+ const clients = sseClients.get(id);
81
+ if (clients) {
82
+ for (const res of clients) {
83
+ try { res.end(); } catch {}
84
+ }
85
+ sseClients.delete(id);
86
+ }
87
+ // Unloaded silently
88
+ }
89
+
90
+ function evictIdleWorkspace(): boolean {
91
+ for (const [id, orch] of orchestrators) {
92
+ const states = orch.getAllAgentStates();
93
+ const hasRunning = Object.values(states).some(s =>
94
+ s.taskStatus === 'running' || s.smithStatus === 'active'
95
+ );
96
+ if (!hasRunning) {
97
+ unloadOrchestrator(id);
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ }
103
+
104
+ // ─── SSE Management ──────────────────────────────────────
105
+
106
+ function addSSEClient(workspaceId: string, res: ServerResponse): void {
107
+ if (!sseClients.has(workspaceId)) sseClients.set(workspaceId, new Set());
108
+ sseClients.get(workspaceId)!.add(res);
109
+ }
110
+
111
+ function removeSSEClient(workspaceId: string, res: ServerResponse): void {
112
+ sseClients.get(workspaceId)?.delete(res);
113
+ if (sseClients.get(workspaceId)?.size === 0) sseClients.delete(workspaceId);
114
+ }
115
+
116
+ function broadcastSSE(workspaceId: string, event: OrchestratorEvent): void {
117
+ const clients = sseClients.get(workspaceId);
118
+ if (!clients) return;
119
+ const data = `data: ${JSON.stringify(event)}\n\n`;
120
+ for (const res of clients) {
121
+ try { res.write(data); } catch { removeSSEClient(workspaceId, res); }
122
+ }
123
+ }
124
+
125
+ // ─── HTTP Helpers ────────────────────────────────────────
126
+
127
+ function readBody(req: IncomingMessage): Promise<string> {
128
+ return new Promise((resolve, reject) => {
129
+ const chunks: Buffer[] = [];
130
+ req.on('data', (c: Buffer) => chunks.push(c));
131
+ req.on('end', () => resolve(Buffer.concat(chunks).toString()));
132
+ req.on('error', reject);
133
+ });
134
+ }
135
+
136
+ function json(res: ServerResponse, data: unknown, status = 200): void {
137
+ res.writeHead(status, { 'Content-Type': 'application/json' });
138
+ res.end(JSON.stringify(data));
139
+ }
140
+
141
+ function jsonError(res: ServerResponse, msg: string, status = 400): void {
142
+ json(res, { error: msg }, status);
143
+ }
144
+
145
+ function parseUrl(url: string): { path: string; query: URLSearchParams } {
146
+ const u = new URL(url, 'http://localhost');
147
+ return { path: u.pathname, query: u.searchParams };
148
+ }
149
+
150
+ // ─── Route: Agent Operations ─────────────────────────────
151
+
152
+ async function handleAgentsPost(id: string, body: any, res: ServerResponse): Promise<void> {
153
+ let orch: WorkspaceOrchestrator;
154
+ try {
155
+ orch = loadOrchestrator(id);
156
+ } catch (err: any) {
157
+ return jsonError(res, err.message, err.message.includes('not found') ? 404 : 429);
158
+ }
159
+
160
+ const { action, agentId, config, content, input } = body;
161
+
162
+ try {
163
+ switch (action) {
164
+ case 'add': {
165
+ if (!config) return jsonError(res, 'config required');
166
+ try {
167
+ orch.addAgent(config as WorkspaceAgentConfig);
168
+ return json(res, { ok: true });
169
+ } catch (err: any) {
170
+ return jsonError(res, err.message);
171
+ }
172
+ }
173
+ case 'create_pipeline': {
174
+ const { createDevPipeline } = require('./workspace/presets');
175
+ const pipeline = createDevPipeline();
176
+ for (const cfg of pipeline) orch.addAgent(cfg);
177
+ return json(res, { ok: true, agents: pipeline.length });
178
+ }
179
+ case 'remove': {
180
+ if (!agentId) return jsonError(res, 'agentId required');
181
+ orch.removeAgent(agentId);
182
+ return json(res, { ok: true });
183
+ }
184
+ case 'update': {
185
+ if (!agentId || !config) return jsonError(res, 'agentId and config required');
186
+ try {
187
+ orch.updateAgentConfig(agentId, config as WorkspaceAgentConfig);
188
+ return json(res, { ok: true });
189
+ } catch (err: any) {
190
+ return jsonError(res, err.message);
191
+ }
192
+ }
193
+ case 'agent_done': {
194
+ // Called by Claude Code Stop hook — agent finished a turn
195
+ if (!agentId) return jsonError(res, 'agentId required');
196
+ orch.handleHookDone(agentId);
197
+ return json(res, { ok: true });
198
+ }
199
+ case 'run': {
200
+ if (!agentId) return jsonError(res, 'agentId required');
201
+ if (!orch.isDaemonActive()) return jsonError(res, 'Start daemon first before running agents');
202
+ try {
203
+ await orch.runAgent(agentId, input, true); // force=true: manual trigger skips dep check
204
+ return json(res, { ok: true, status: 'started' });
205
+ } catch (err: any) {
206
+ return jsonError(res, err.message);
207
+ }
208
+ }
209
+ case 'run_all': {
210
+ orch.runAll().catch(err => {
211
+ console.error('[workspace] runAll error:', err.message);
212
+ });
213
+ return json(res, { ok: true, status: 'started' });
214
+ }
215
+ case 'complete_input': {
216
+ if (!agentId || !content) return jsonError(res, 'agentId and content required');
217
+ orch.completeInput(agentId, content);
218
+ return json(res, { ok: true });
219
+ }
220
+ case 'pause': {
221
+ if (!agentId) return jsonError(res, 'agentId required');
222
+ orch.pauseAgent(agentId);
223
+ return json(res, { ok: true });
224
+ }
225
+ case 'resume': {
226
+ if (!agentId) return jsonError(res, 'agentId required');
227
+ orch.resumeAgent(agentId);
228
+ return json(res, { ok: true });
229
+ }
230
+ case 'stop': {
231
+ if (!agentId) return jsonError(res, 'agentId required');
232
+ orch.stopAgent(agentId);
233
+ return json(res, { ok: true });
234
+ }
235
+ case 'mark_done': {
236
+ if (!agentId) return jsonError(res, 'agentId required');
237
+ orch.markAgentDone(agentId, body.notify !== false);
238
+ return json(res, { ok: true });
239
+ }
240
+ case 'mark_failed': {
241
+ if (!agentId) return jsonError(res, 'agentId required');
242
+ orch.markAgentFailed(agentId, body.notify !== false);
243
+ return json(res, { ok: true });
244
+ }
245
+ case 'retry': {
246
+ if (!agentId) return jsonError(res, 'agentId required');
247
+ if (!orch.isDaemonActive()) return jsonError(res, 'Start daemon first before retrying agents');
248
+ const retryState = orch.getAgentState(agentId);
249
+ if (!retryState) return jsonError(res, 'Agent not found');
250
+ if (retryState.taskStatus === 'running') return jsonError(res, 'Agent is already running');
251
+ if (retryState.taskStatus !== 'failed') return jsonError(res, `Agent is ${retryState.taskStatus}, not failed`);
252
+ try {
253
+ console.log(`[workspace] Retry ${agentId}: smith=${retryState.smithStatus}, task=${retryState.taskStatus}`);
254
+ await orch.runAgent(agentId, undefined, true);
255
+ return json(res, { ok: true, status: 'retrying' });
256
+ } catch (err: any) {
257
+ console.error(`[workspace] Retry failed for ${agentId}:`, err.message);
258
+ return jsonError(res, err.message);
259
+ }
260
+ }
261
+ case 'set_tmux_session': {
262
+ if (!agentId) return jsonError(res, 'agentId required');
263
+ const { sessionName } = body;
264
+ orch.setTmuxSession(agentId, sessionName);
265
+ return json(res, { ok: true });
266
+ }
267
+ case 'reset': {
268
+ if (!agentId) return jsonError(res, 'agentId required');
269
+ orch.resetAgent(agentId);
270
+ // If daemon is active, re-enter daemon mode for this agent
271
+ if (orch.isDaemonActive()) {
272
+ orch.restartAgentDaemon(agentId);
273
+ }
274
+ return json(res, { ok: true });
275
+ }
276
+ case 'open_terminal': {
277
+ if (!agentId) return jsonError(res, 'agentId required');
278
+ const agentState = orch.getAgentState(agentId);
279
+ const agentConfig = orch.getSnapshot().agents.find(a => a.id === agentId);
280
+ if (!agentState || !agentConfig) return jsonError(res, 'Agent not found', 404);
281
+
282
+ // Resolve launch info using shared logic (same as VibeCoding terminal)
283
+ let launchInfo: any = { cliCmd: 'claude', cliType: 'claude-code', supportsSession: true };
284
+ try {
285
+ const { resolveTerminalLaunch, clearAgentCache } = await import('./agents/index.js');
286
+ clearAgentCache(); // ensure fresh settings are read
287
+ launchInfo = resolveTerminalLaunch(agentConfig.agentId);
288
+ } catch {}
289
+
290
+ // resolveOnly: return launch info + current session ID (no side effects)
291
+ if (body.resolveOnly) {
292
+ let currentSessionId: string | null = null;
293
+ if (agentConfig.primary) {
294
+ try {
295
+ const { getFixedSession } = await import('./project-sessions.js');
296
+ currentSessionId = getFixedSession(orch.projectPath) || null;
297
+ } catch {}
298
+ } else {
299
+ currentSessionId = agentConfig.boundSessionId || null;
300
+ }
301
+ return json(res, { ok: true, ...launchInfo, currentSessionId });
302
+ }
303
+
304
+ // Primary agent: always return its fixed session, no selection
305
+ if (agentConfig.primary && agentState.tmuxSession) {
306
+ return json(res, { ok: true, primary: true, tmuxSession: agentState.tmuxSession, fixedSession: true, ...launchInfo });
307
+ }
308
+
309
+ // No tmux has-session liveness check — it adds latency with no benefit.
310
+ // If the session is dead, openTerminalSession will recreate it anyway.
311
+
312
+ // Ensure tmux session exists (creates if needed, no 3s startup delay)
313
+ // forceRestart: kill existing tmux so launch script is rewritten with current boundSessionId
314
+ const tmuxSession = await orch.openTerminalSession(agentId, body.forceRestart === true);
315
+
316
+ orch.setManualMode(agentId);
317
+ // Skills call Next.js API (/api/workspace/.../smith), so use FORGE_PORT not daemon PORT
318
+ const result = installForgeSkills(orch.projectPath, id, agentId, FORGE_PORT);
319
+
320
+ return json(res, {
321
+ ok: true,
322
+ primary: agentConfig.primary || undefined,
323
+ fixedSession: agentConfig.primary || undefined,
324
+ skillsInstalled: result.installed,
325
+ agentId,
326
+ label: agentConfig.label,
327
+ tmuxSession: tmuxSession || undefined,
328
+ ...launchInfo,
329
+ });
330
+ }
331
+ case 'close_terminal': {
332
+ if (!agentId) return jsonError(res, 'agentId required');
333
+ if (body.kill) {
334
+ // Kill: clear tmuxSession → message loop falls back to headless (claude -p)
335
+ orch.clearTmuxSession(agentId);
336
+ console.log(`[workspace] ${agentId}: terminal killed, falling back to headless`);
337
+ }
338
+ // Suspend: tmuxSession stays, agent can reattach later
339
+ return json(res, { ok: true });
340
+ }
341
+ case 'create_ticket': {
342
+ if (!agentId || !content) return jsonError(res, 'agentId (from) and content required');
343
+ const targetId = body.targetId;
344
+ if (!targetId) return jsonError(res, 'targetId required');
345
+ const causedByMsg = body.causedByMessageId ? orch.getBus().getLog().find(m => m.id === body.causedByMessageId) : undefined;
346
+ const causedBy = causedByMsg ? { messageId: causedByMsg.id, from: causedByMsg.from, to: causedByMsg.to } : undefined;
347
+ const ticket = orch.getBus().createTicket(agentId, targetId, body.ticketAction || 'bug_report', content, body.files, causedBy);
348
+ return json(res, { ok: true, ticketId: ticket.id });
349
+ }
350
+ case 'update_ticket': {
351
+ const { messageId, ticketStatus } = body;
352
+ if (!messageId || !ticketStatus) return jsonError(res, 'messageId and ticketStatus required');
353
+ orch.getBus().updateTicketStatus(messageId, ticketStatus);
354
+ return json(res, { ok: true });
355
+ }
356
+ case 'message': {
357
+ if (!agentId || !content) return jsonError(res, 'agentId and content required');
358
+ orch.sendMessageToAgent(agentId, content);
359
+ return json(res, { ok: true });
360
+ }
361
+ case 'approve': {
362
+ if (!agentId) return jsonError(res, 'agentId required');
363
+ orch.approveAgent(agentId);
364
+ return json(res, { ok: true });
365
+ }
366
+ case 'reject': {
367
+ if (!agentId) return jsonError(res, 'agentId required');
368
+ orch.rejectApproval(agentId);
369
+ return json(res, { ok: true });
370
+ }
371
+ case 'retry_message': {
372
+ const { messageId } = body;
373
+ if (!messageId) return jsonError(res, 'messageId required');
374
+ if (!orch.isDaemonActive()) return jsonError(res, 'Start daemon first before retrying messages');
375
+ const msg = orch.getBus().retryMessage(messageId);
376
+ if (!msg) return jsonError(res, 'Message not found or already pending');
377
+ orch.emit('event', { type: 'bus_message_status', messageId: msg.id, status: 'pending' });
378
+ return json(res, { ok: true, messageId: msg.id, action: msg.payload.action });
379
+ }
380
+ case 'abort_message': {
381
+ const { messageId } = body;
382
+ if (!messageId) return jsonError(res, 'messageId required');
383
+ const abortMsg = orch.getBus().abortMessage(messageId);
384
+ if (abortMsg) {
385
+ orch.emit('event', { type: 'bus_message_status', messageId, status: 'failed' });
386
+ }
387
+ return json(res, { ok: true, messageId, aborted: !!abortMsg });
388
+ }
389
+ case 'approve_message': {
390
+ const { messageId } = body;
391
+ if (!messageId) return jsonError(res, 'messageId required');
392
+ const approveMsg = orch.getBus().getLog().find(m => m.id === messageId);
393
+ if (!approveMsg) return jsonError(res, 'Message not found');
394
+ if (approveMsg.status !== 'pending_approval') return jsonError(res, 'Message is not pending approval');
395
+ if (body.content) approveMsg.payload.content = body.content;
396
+ approveMsg.status = 'pending';
397
+ orch.emit('event', { type: 'bus_message_status', messageId, status: 'pending' });
398
+ return json(res, { ok: true });
399
+ }
400
+ case 'reject_message': {
401
+ const { messageId } = body;
402
+ if (!messageId) return jsonError(res, 'messageId required');
403
+ const rejectMsg = orch.getBus().getLog().find(m => m.id === messageId);
404
+ if (!rejectMsg) return jsonError(res, 'Message not found');
405
+ rejectMsg.status = 'failed';
406
+ orch.emit('event', { type: 'bus_message_status', messageId, status: 'failed' });
407
+ return json(res, { ok: true });
408
+ }
409
+ case 'delete_message': {
410
+ const { messageId, messageIds } = body;
411
+ const ids: string[] = messageIds || (messageId ? [messageId] : []);
412
+ if (ids.length === 0) return jsonError(res, 'messageId or messageIds required');
413
+ for (const id of ids) orch.getBus().deleteMessage(id);
414
+ // Push updated bus log to frontend
415
+ orch.emit('event', { type: 'bus_log_updated', log: orch.getBus().getLog() } as any);
416
+ return json(res, { ok: true, deleted: ids.length });
417
+ }
418
+ case 'message_done': {
419
+ const { messageId } = body;
420
+ if (!messageId) return jsonError(res, 'messageId required');
421
+ const msg = orch.getBus().getLog().find(m => m.id === messageId);
422
+ if (!msg) return jsonError(res, 'Message not found');
423
+ msg.status = 'done';
424
+ orch.emit('event', { type: 'bus_message_status', messageId, status: 'done' } as any);
425
+ return json(res, { ok: true });
426
+ }
427
+ case 'start_daemon': {
428
+ // Check active daemon count before starting (include 'starting' to prevent exceeding MAX_ACTIVE)
429
+ const activeCount = Array.from(orchestrators.values()).filter(o =>
430
+ o.isDaemonActive() || Object.values(o.getAllAgentStates()).some(s => s.smithStatus === 'starting')
431
+ ).length;
432
+ if (activeCount >= MAX_ACTIVE && !orch.isDaemonActive()) {
433
+ return jsonError(res, `Maximum ${MAX_ACTIVE} active daemons. Stop agents in another workspace first.`);
434
+ }
435
+ orch.startDaemon().catch(err => {
436
+ console.error('[workspace] startDaemon error:', err.message);
437
+ });
438
+ return json(res, { ok: true, status: 'daemon_started' });
439
+ }
440
+ case 'stop_daemon': {
441
+ orch.stopDaemon();
442
+ return json(res, { ok: true, status: 'daemon_stopped' });
443
+ }
444
+ default:
445
+ return jsonError(res, `Unknown action: ${action}`);
446
+ }
447
+ } catch (err: any) {
448
+ return jsonError(res, err.message, 500);
449
+ }
450
+ }
451
+
452
+ function handleAgentsGet(id: string, res: ServerResponse): void {
453
+ let orch: WorkspaceOrchestrator;
454
+ try {
455
+ orch = loadOrchestrator(id);
456
+ } catch (err: any) {
457
+ return jsonError(res, err.message, err.message.includes('not found') ? 404 : 429);
458
+ }
459
+
460
+ json(res, {
461
+ agents: orch.getSnapshot().agents,
462
+ states: orch.getAllAgentStates(),
463
+ busLog: orch.getBusLog(),
464
+ daemonActive: orch.isDaemonActive(),
465
+ });
466
+ }
467
+
468
+ // ─── Route: SSE Stream ───────────────────────────────────
469
+
470
+ function handleStream(id: string, req: IncomingMessage, res: ServerResponse): void {
471
+ let orch: WorkspaceOrchestrator;
472
+ try {
473
+ orch = loadOrchestrator(id);
474
+ } catch (err: any) {
475
+ res.writeHead(err.message.includes('not found') ? 404 : 429);
476
+ res.end(err.message);
477
+ return;
478
+ }
479
+
480
+ res.writeHead(200, {
481
+ 'Content-Type': 'text/event-stream',
482
+ 'Cache-Control': 'no-cache, no-transform',
483
+ 'Connection': 'keep-alive',
484
+ });
485
+
486
+ // Send initial snapshot
487
+ const snapshot = orch.getSnapshot();
488
+ res.write(`data: ${JSON.stringify({ type: 'init', ...snapshot })}\n\n`);
489
+
490
+ addSSEClient(id, res);
491
+
492
+ // Keep-alive ping every 15s
493
+ const ping = setInterval(() => {
494
+ try { res.write(`: ping\n\n`); } catch {
495
+ clearInterval(ping);
496
+ removeSSEClient(id, res);
497
+ }
498
+ }, 15000);
499
+
500
+ // Cleanup on disconnect
501
+ req.on('close', () => {
502
+ clearInterval(ping);
503
+ removeSSEClient(id, res);
504
+ });
505
+ }
506
+
507
+ // ─── Route: Smith API ────────────────────────────────────
508
+
509
+ async function handleSmith(id: string, body: any, res: ServerResponse): Promise<void> {
510
+ const orch = getOrchestrator(id);
511
+ if (!orch) return jsonError(res, 'Workspace not found', 404);
512
+
513
+ const { action, agentId } = body;
514
+
515
+ switch (action) {
516
+ case 'done': {
517
+ if (!agentId) return jsonError(res, 'agentId required');
518
+
519
+ try {
520
+ let gitDiff = '';
521
+ try {
522
+ gitDiff = execSync('git diff --stat HEAD', {
523
+ cwd: orch.projectPath, encoding: 'utf-8', timeout: 5000,
524
+ }).trim();
525
+ } catch {}
526
+
527
+ // Parse file names from --stat output (lines with ' | ') instead of a second execSync
528
+ const changedFiles = gitDiff
529
+ .split('\n')
530
+ .filter(l => l.includes(' | '))
531
+ .map(l => l.split(' | ')[0].trim())
532
+ .filter(Boolean);
533
+ const entry = (orch as any).agents?.get(agentId);
534
+ const config = entry?.config;
535
+
536
+ if (config && changedFiles.length > 0) {
537
+ await addObservation(id, agentId, config.label, config.role, {
538
+ type: 'change',
539
+ title: `Manual work completed: ${changedFiles.length} files changed`,
540
+ filesModified: changedFiles.slice(0, 10),
541
+ detail: gitDiff.slice(0, 500),
542
+ stepLabel: 'manual',
543
+ });
544
+
545
+ await addSessionSummary(id, agentId, {
546
+ request: 'Manual development session',
547
+ investigated: `Worked on ${changedFiles.length} files`,
548
+ learned: '', completed: gitDiff.slice(0, 300), nextSteps: '',
549
+ filesRead: [], filesModified: changedFiles,
550
+ });
551
+ }
552
+
553
+ // Parse bus markers
554
+ const { output } = body;
555
+ let markersSent = 0;
556
+ if (output && typeof output === 'string') {
557
+ const markerRegex = /\[SEND:([^:]+):([^\]]+)\]\s*(.+)/g;
558
+ const snapshot = orch.getSnapshot();
559
+ const labelToId = new Map(snapshot.agents.map(a => [a.label.toLowerCase(), a.id]));
560
+ const seen = new Set<string>();
561
+ let match;
562
+ while ((match = markerRegex.exec(output)) !== null) {
563
+ const targetLabel = match[1].trim();
564
+ const msgAction = match[2].trim();
565
+ const content = match[3].trim();
566
+ const targetId = labelToId.get(targetLabel.toLowerCase());
567
+ if (targetId && targetId !== agentId) {
568
+ const key = `${targetId}:${msgAction}:${content}`;
569
+ if (!seen.has(key)) {
570
+ seen.add(key);
571
+ orch.getBus().send(agentId, targetId, 'notify', { action: msgAction, content });
572
+ markersSent++;
573
+ }
574
+ }
575
+ }
576
+ }
577
+
578
+ orch.completeManualAgent(agentId, changedFiles);
579
+
580
+ return json(res, {
581
+ ok: true, filesChanged: changedFiles.length,
582
+ files: changedFiles.slice(0, 20),
583
+ gitDiff: gitDiff.slice(0, 500), markersSent,
584
+ });
585
+ } catch (err: any) {
586
+ return jsonError(res, err.message, 500);
587
+ }
588
+ }
589
+
590
+ case 'send': {
591
+ const { to, msgAction, content } = body;
592
+ if (!to || !content) {
593
+ return jsonError(res, 'to and content required');
594
+ }
595
+
596
+ const snapshot = orch.getSnapshot();
597
+ const target = snapshot.agents.find(a => a.label.toLowerCase() === to.toLowerCase() || a.id === to);
598
+ if (!target) return jsonError(res, `Agent "${to}" not found. Available: ${snapshot.agents.map(a => a.label).join(', ')}`, 404);
599
+
600
+ // Resolve sender: validate agentId exists in workspace, otherwise 'user'
601
+ const senderExists = agentId && agentId !== 'unknown' && snapshot.agents.some(a => a.id === agentId);
602
+ if (agentId && agentId !== 'unknown' && !senderExists) {
603
+ return jsonError(res, `Sender agent "${agentId}" not found in workspace`);
604
+ }
605
+ const senderId = senderExists ? agentId : 'user';
606
+
607
+ // Block: if sender is currently processing a message FROM the target,
608
+ // don't send — the result is already delivered via markMessageDone
609
+ if (senderId !== 'user') {
610
+ const senderEntry = orch.getSnapshot().agentStates[senderId];
611
+ if (senderEntry?.currentMessageId) {
612
+ const currentMsg = orch.getBus().getLog().find(m => m.id === senderEntry.currentMessageId);
613
+ if (currentMsg && currentMsg.from === target.id && currentMsg.status === 'running') {
614
+ return json(res, {
615
+ ok: true, skipped: true,
616
+ reason: `You are processing a message from ${target.label}. Your result will be delivered automatically — no need to send a reply.`,
617
+ });
618
+ }
619
+ }
620
+ }
621
+
622
+ const sentMsg = orch.getBus().send(senderId, target.id, 'notify', {
623
+ action: msgAction || 'agent_message',
624
+ content,
625
+ });
626
+
627
+ return json(res, { ok: true, sentTo: target.label, messageId: sentMsg.id });
628
+ }
629
+
630
+ case 'logs': {
631
+ if (!agentId) return jsonError(res, 'agentId required');
632
+ const { readAgentLog } = await import('./workspace/persistence.js');
633
+ const logs = readAgentLog(id, agentId);
634
+ return json(res, { logs });
635
+ }
636
+
637
+ case 'clear_logs': {
638
+ if (!agentId) return jsonError(res, 'agentId required');
639
+ const { clearAgentLog } = await import('./workspace/persistence.js');
640
+ clearAgentLog(id, agentId);
641
+ // Also clear in-memory history
642
+ const agentState = orch.getAgentState(agentId);
643
+ if (agentState) (agentState as any).history = [];
644
+ return json(res, { ok: true });
645
+ }
646
+
647
+ case 'inbox': {
648
+ if (!agentId) return jsonError(res, 'agentId required');
649
+
650
+ const messages = orch.getBus().getMessagesFor(agentId)
651
+ .filter(m => m.type !== 'ack')
652
+ .slice(-20)
653
+ .map(m => ({
654
+ id: m.id,
655
+ from: (orch.getSnapshot().agents.find(a => a.id === m.from)?.label || m.from),
656
+ action: m.payload.action,
657
+ content: m.payload.content,
658
+ status: m.status || 'pending',
659
+ time: new Date(m.timestamp).toLocaleTimeString(),
660
+ }));
661
+
662
+ return json(res, { messages });
663
+ }
664
+
665
+ case 'message_done': {
666
+ // Manual mode: user marks a specific inbox message as done
667
+ const { messageId } = body;
668
+ if (!agentId || !messageId) return jsonError(res, 'agentId and messageId required');
669
+ const busMsg = orch.getBus().getLog().find(m => m.id === messageId && m.to === agentId);
670
+ if (!busMsg) return jsonError(res, 'Message not found');
671
+ busMsg.status = 'done';
672
+ return json(res, { ok: true });
673
+ }
674
+
675
+ case 'message_failed': {
676
+ const { messageId } = body;
677
+ if (!agentId || !messageId) return jsonError(res, 'agentId and messageId required');
678
+ const busMsg = orch.getBus().getLog().find(m => m.id === messageId && m.to === agentId);
679
+ if (!busMsg) return jsonError(res, 'Message not found');
680
+ busMsg.status = 'failed';
681
+ return json(res, { ok: true });
682
+ }
683
+
684
+ case 'sessions': {
685
+ // List recent claude sessions for resume picker
686
+ // Uses the agent's workDir (or project root) to find sessions
687
+ try {
688
+ const agentConfig = agentId ? orch.getSnapshot().agents.find(a => a.id === agentId) : null;
689
+ const agentWorkDir = agentConfig?.workDir && agentConfig.workDir !== './' && agentConfig.workDir !== '.'
690
+ ? join(orch.projectPath, agentConfig.workDir) : orch.projectPath;
691
+ const encoded = resolve(agentWorkDir).replace(/[^a-zA-Z0-9]/g, '-');
692
+ const sessDir = join(homedir(), '.claude', 'projects', encoded);
693
+ const entries = readdirSync(sessDir);
694
+ const files = entries
695
+ .filter((f: string) => f.endsWith('.jsonl'))
696
+ .map((f: string) => {
697
+ const fp = join(sessDir, f);
698
+ const st = statSync(fp);
699
+ return { id: f.replace('.jsonl', ''), modified: st.mtime.toISOString(), size: st.size };
700
+ })
701
+ .sort((a: any, b: any) => new Date(b.modified).getTime() - new Date(a.modified).getTime())
702
+ .slice(0, 5);
703
+ return json(res, { sessions: files });
704
+ } catch {
705
+ return json(res, { sessions: [] });
706
+ }
707
+ }
708
+
709
+ case 'status': {
710
+ const snapshot = orch.getSnapshot();
711
+ const states = orch.getAllAgentStates();
712
+ const agents = snapshot.agents.map(a => ({
713
+ id: a.id, label: a.label, icon: a.icon, type: a.type, primary: a.primary || undefined,
714
+ smithStatus: states[a.id]?.smithStatus || 'down',
715
+ taskStatus: states[a.id]?.taskStatus || 'idle',
716
+ hasTmux: !!states[a.id]?.tmuxSession,
717
+ currentStep: states[a.id]?.currentStep,
718
+ }));
719
+ return json(res, { agents });
720
+ }
721
+
722
+ case 'primary_session': {
723
+ // Get the primary agent's tmux session + project-level fixed session
724
+ const primary = orch.getPrimaryAgent();
725
+ if (!primary) return json(res, { ok: false, error: 'No primary agent configured' });
726
+ let fixedSessionId: string | null = null;
727
+ try {
728
+ const { getFixedSession } = await import('./project-sessions.js');
729
+ fixedSessionId = getFixedSession(orch.projectPath) || null;
730
+ } catch {}
731
+ return json(res, {
732
+ ok: true,
733
+ agentId: primary.config.id,
734
+ label: primary.config.label,
735
+ tmuxSession: primary.state.tmuxSession || null,
736
+ fixedSessionId,
737
+ });
738
+ }
739
+
740
+ default:
741
+ return jsonError(res, `Unknown action: ${action}`);
742
+ }
743
+ }
744
+
745
+ // ─── Route: Memory ───────────────────────────────────────
746
+
747
+ function handleMemory(workspaceId: string, query: URLSearchParams, res: ServerResponse): void {
748
+ const agentId = query.get('agentId');
749
+ if (!agentId) return jsonError(res, 'agentId required');
750
+
751
+ const memory = loadMemory(workspaceId, agentId);
752
+ const stats = getMemoryStats(memory);
753
+ const display = formatMemoryForDisplay(memory);
754
+
755
+ json(res, { memory, stats, display });
756
+ }
757
+
758
+ // ─── HTTP Router ─────────────────────────────────────────
759
+
760
+ const server = createServer(async (req, res) => {
761
+ const { path, query } = parseUrl(req.url || '/');
762
+ const method = req.method || 'GET';
763
+
764
+ // CORS for local dev
765
+ res.setHeader('Access-Control-Allow-Origin', '*');
766
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
767
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
768
+
769
+ if (method === 'OPTIONS') {
770
+ res.writeHead(204);
771
+ res.end();
772
+ return;
773
+ }
774
+
775
+ try {
776
+ // Health check
777
+ if (path === '/health' && method === 'GET') {
778
+ return json(res, {
779
+ ok: true,
780
+ active: orchestrators.size,
781
+ maxActive: MAX_ACTIVE,
782
+ uptime: Math.floor((Date.now() - startTime) / 1000),
783
+ });
784
+ }
785
+
786
+ // Resolve projectPath → workspaceId + agentId (walks up directories)
787
+ if (path === '/resolve' && method === 'GET') {
788
+ const projectPath = query.get('projectPath') || '';
789
+ if (!projectPath) return jsonError(res, 'projectPath required');
790
+ // Walk up directories to find workspace
791
+ let dir = projectPath;
792
+ while (dir && dir !== '/') {
793
+ const ws = findWorkspaceByProject(dir);
794
+ if (ws) {
795
+ const primary = ws.agents?.find((a: any) => a.primary);
796
+ return json(res, {
797
+ workspaceId: ws.id,
798
+ projectPath: ws.projectPath,
799
+ projectName: ws.projectName,
800
+ primaryAgentId: primary?.id || null,
801
+ });
802
+ }
803
+ dir = dir.replace(/\/[^/]+$/, '') || '/';
804
+ }
805
+ return json(res, { workspaceId: null });
806
+ }
807
+
808
+ // Active workspaces
809
+ if (path === '/workspaces/active' && method === 'GET') {
810
+ return json(res, {
811
+ workspaces: Array.from(orchestrators.keys()),
812
+ });
813
+ }
814
+
815
+ // Create workspace (daemon is the exclusive writer of state.json)
816
+ if (path === '/workspace/create' && method === 'POST') {
817
+ const bodyStr = await readBody(req);
818
+ const { id, projectPath, projectName, agents, agentStates, nodePositions, template, createdAt } = JSON.parse(bodyStr);
819
+
820
+ if (!projectPath || !projectName) return jsonError(res, 'projectPath and projectName required');
821
+
822
+ // Guard against concurrent creates for the same project
823
+ const existing = findWorkspaceByProject(projectPath);
824
+ if (existing && !template) return json(res, existing);
825
+
826
+ const state: import('./workspace/types').WorkspaceState = {
827
+ id: id || require('node:crypto').randomUUID(),
828
+ projectPath,
829
+ projectName,
830
+ agents: [],
831
+ agentStates: {},
832
+ nodePositions: {},
833
+ busLog: [],
834
+ createdAt: createdAt || Date.now(),
835
+ updatedAt: Date.now(),
836
+ };
837
+
838
+ // Import template: create agents from template with new IDs
839
+ if (template?.agents) {
840
+ const idMap = new Map<string, string>();
841
+ const ts = Date.now();
842
+ for (const agent of template.agents) {
843
+ const newId = `${agent.label.toLowerCase().replace(/\s+/g, '-')}-${ts}-${Math.random().toString(36).slice(2, 5)}`;
844
+ idMap.set(agent.id, newId);
845
+ }
846
+ for (const agent of template.agents) {
847
+ state.agents.push({
848
+ ...agent,
849
+ id: idMap.get(agent.id) || agent.id,
850
+ dependsOn: agent.dependsOn.map((d: string) => idMap.get(d) || d),
851
+ entries: agent.type === 'input' ? [] : undefined,
852
+ });
853
+ state.agentStates[idMap.get(agent.id) || agent.id] = { smithStatus: 'down', taskStatus: 'idle', history: [], artifacts: [] };
854
+ }
855
+ if (template.nodePositions) {
856
+ for (const [oldId, pos] of Object.entries(template.nodePositions)) {
857
+ const newId = idMap.get(oldId);
858
+ if (newId) state.nodePositions[newId] = pos as { x: number; y: number };
859
+ }
860
+ }
861
+ }
862
+
863
+ // Persist to disk — daemon is the single writer
864
+ await saveWorkspace(state);
865
+
866
+ // Load into memory immediately so SSE/API work without a second round-trip
867
+ const orch = new WorkspaceOrchestrator(state.id, state.projectPath, state.projectName);
868
+ if (state.agents.length > 0) {
869
+ orch.loadSnapshot({
870
+ agents: state.agents,
871
+ agentStates: state.agentStates,
872
+ busLog: state.busLog,
873
+ busOutbox: state.busOutbox,
874
+ });
875
+ }
876
+ orch.on('event', (event: OrchestratorEvent) => { broadcastSSE(state.id, event); });
877
+ orchestrators.set(state.id, orch);
878
+
879
+ return json(res, state, 201);
880
+ }
881
+
882
+ // Route: /workspace/:id/...
883
+ const wsMatch = path.match(/^\/workspace\/([^/]+)(\/.*)?$/);
884
+ if (!wsMatch) {
885
+ return jsonError(res, 'Not found', 404);
886
+ }
887
+
888
+ const id = wsMatch[1];
889
+ const subPath = wsMatch[2] || '';
890
+
891
+ // Load/Unload
892
+ if (subPath === '/load' && method === 'POST') {
893
+ try {
894
+ loadOrchestrator(id);
895
+ return json(res, { ok: true });
896
+ } catch (err: any) {
897
+ return jsonError(res, err.message, err.message.includes('not found') ? 404 : 429);
898
+ }
899
+ }
900
+
901
+ if (subPath === '/unload' && method === 'POST') {
902
+ unloadOrchestrator(id);
903
+ return json(res, { ok: true });
904
+ }
905
+
906
+ // Agent operations
907
+ if (subPath === '/agents' && method === 'POST') {
908
+ const bodyStr = await readBody(req);
909
+ let body: any;
910
+ try { body = JSON.parse(bodyStr); } catch { return jsonError(res, 'Invalid JSON', 400); }
911
+ return handleAgentsPost(id, body, res);
912
+ }
913
+
914
+ if (subPath === '/agents' && method === 'GET') {
915
+ return handleAgentsGet(id, res);
916
+ }
917
+
918
+ // SSE stream
919
+ if (subPath === '/stream' && method === 'GET') {
920
+ return handleStream(id, req, res);
921
+ }
922
+
923
+ // Smith API
924
+ if (subPath === '/smith' && method === 'POST') {
925
+ const bodyStr = await readBody(req);
926
+ let body: any;
927
+ try { body = JSON.parse(bodyStr); } catch { return jsonError(res, 'Invalid JSON', 400); }
928
+ return handleSmith(id, body, res);
929
+ }
930
+
931
+ // Memory
932
+ if (subPath === '/memory' && method === 'GET') {
933
+ return handleMemory(id, query, res);
934
+ }
935
+
936
+ return jsonError(res, 'Not found', 404);
937
+
938
+ } catch (err: any) {
939
+ console.error('[workspace] Request error:', err);
940
+ return jsonError(res, err.message || 'Internal error', 500);
941
+ }
942
+ });
943
+
944
+ // ─── Graceful Shutdown ───────────────────────────────────
945
+
946
+ function shutdown() {
947
+ console.log('[workspace] Shutting down...');
948
+ for (const [id] of orchestrators) {
949
+ unloadOrchestrator(id);
950
+ }
951
+ server.close(() => {
952
+ console.log('[workspace] Server closed.');
953
+ process.exit(0);
954
+ });
955
+ // Force exit after 5s
956
+ setTimeout(() => process.exit(0), 5000);
957
+ }
958
+
959
+ process.on('SIGTERM', shutdown);
960
+ process.on('SIGINT', shutdown);
961
+ process.on('uncaughtException', (err) => {
962
+ console.error('[workspace] Uncaught exception:', err);
963
+ });
964
+ process.on('unhandledRejection', (err) => {
965
+ console.error('[workspace] Unhandled rejection:', err);
966
+ });
967
+
968
+ // ─── Start ───────────────────────────────────────────────
969
+
970
+ // Start MCP Server alongside workspace daemon
971
+ import { startMcpServer, setOrchestratorResolver, getMcpPort } from './forge-mcp-server.js';
972
+ setOrchestratorResolver((id: string) => loadOrchestrator(id));
973
+ const MCP_PORT = getMcpPort();
974
+ startMcpServer(MCP_PORT).catch(err => console.error('[forge-mcp] Failed to start:', err));
975
+
976
+ server.listen(PORT, () => {
977
+ console.log(`[workspace] Daemon started on http://0.0.0.0:${PORT} (max ${MAX_ACTIVE} workspaces, MCP on ${MCP_PORT})`);
978
+ });