@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.
Files changed (255) 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 +10 -29
  249. package/app/api/terminal-bell/route.ts +6 -2
  250. package/app/api/terminal-cwd/route.ts +7 -4
  251. package/components/CodeViewer.tsx +3 -31
  252. package/components/Dashboard.tsx +34 -20
  253. package/components/WebTerminal.tsx +36 -2
  254. package/lib/terminal-standalone.ts +19 -2
  255. package/package.json +1 -1
@@ -0,0 +1,951 @@
1
+ /**
2
+ * Task Manager — persistent task queue backed by SQLite.
3
+ * Tasks survive server restarts. Background runner picks up queued tasks.
4
+ */
5
+
6
+ import { randomUUID } from 'node:crypto';
7
+ import { spawn, execSync } from 'node:child_process';
8
+ import { realpathSync } from 'node:fs';
9
+ import { getDb } from '@/src/core/db/database';
10
+ import { getDbPath } from '@/src/config';
11
+ import { loadSettings } from './settings';
12
+ import { notifyTaskComplete, notifyTaskFailed } from './notify';
13
+ import type { Task, TaskLogEntry, TaskStatus, TaskMode, WatchConfig } from '@/src/types';
14
+
15
+ /** Normalize SQLite datetime('now') → ISO 8601 UTC string. */
16
+ function toIsoUTC(s: string | null | undefined): string | null {
17
+ if (!s) return null;
18
+ if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(s)) return s.replace(' ', 'T') + 'Z';
19
+ return s;
20
+ }
21
+
22
+ const runnerKey = Symbol.for('mw-task-runner');
23
+ const gRunner = globalThis as any;
24
+ if (!gRunner[runnerKey]) gRunner[runnerKey] = { runner: null, currentTaskId: null };
25
+ const runnerState: { runner: ReturnType<typeof setInterval> | null; currentTaskId: string | null } = gRunner[runnerKey];
26
+
27
+ // Per-project concurrency: track which projects have a running prompt task
28
+ const runningProjects = new Set<string>();
29
+
30
+ // Event listeners for real-time updates
31
+ type TaskListener = (taskId: string, event: 'log' | 'status', data?: any) => void;
32
+ const listeners = new Set<TaskListener>();
33
+
34
+ export function onTaskEvent(fn: TaskListener): () => void {
35
+ listeners.add(fn);
36
+ return () => listeners.delete(fn);
37
+ }
38
+
39
+ function emit(taskId: string, event: 'log' | 'status', data?: any) {
40
+ for (const fn of listeners) {
41
+ try { fn(taskId, event, data); } catch {}
42
+ }
43
+ }
44
+
45
+ function db() {
46
+ return getDb(getDbPath());
47
+ }
48
+
49
+ // Per-task model overrides (used by pipeline to set pipelineModel)
50
+ export const taskModelOverrides = new Map<string, string>();
51
+
52
+ // ─── CRUD ────────────────────────────────────────────────────
53
+
54
+ export function createTask(opts: {
55
+ projectName: string;
56
+ projectPath: string;
57
+ prompt: string;
58
+ mode?: TaskMode;
59
+ priority?: number;
60
+ conversationId?: string; // Explicit override; otherwise auto-inherits from project
61
+ scheduledAt?: string; // ISO timestamp — task won't run until this time
62
+ watchConfig?: WatchConfig;
63
+ agent?: string; // Agent ID (default: from settings)
64
+ }): Task {
65
+ const id = randomUUID().slice(0, 8);
66
+ const mode = opts.mode || 'prompt';
67
+ const agent = opts.agent || '';
68
+
69
+ // For prompt mode: auto-inherit conversation_id
70
+ // For monitor mode: conversationId is required (the session to watch)
71
+ const convId = opts.conversationId === ''
72
+ ? null
73
+ : (opts.conversationId || (mode === 'prompt' ? getProjectConversationId(opts.projectName) : null));
74
+
75
+ db().prepare(`
76
+ INSERT INTO tasks (id, project_name, project_path, prompt, mode, status, priority, conversation_id, log, scheduled_at, watch_config, agent)
77
+ VALUES (?, ?, ?, ?, ?, 'queued', ?, ?, '[]', ?, ?, ?)
78
+ `).run(
79
+ id, opts.projectName, opts.projectPath, opts.prompt, mode,
80
+ opts.priority || 0, convId || null, opts.scheduledAt || null,
81
+ opts.watchConfig ? JSON.stringify(opts.watchConfig) : null,
82
+ agent || null,
83
+ );
84
+
85
+ // Kick the runner
86
+ ensureRunnerStarted();
87
+
88
+ return getTask(id)!;
89
+ }
90
+
91
+ /**
92
+ * Get the most recent conversation_id for a project.
93
+ * This allows all tasks for the same project to share one Claude session.
94
+ */
95
+ export function getProjectConversationId(projectName: string): string | null {
96
+ const row = db().prepare(`
97
+ SELECT conversation_id FROM tasks
98
+ WHERE project_name = ? AND conversation_id IS NOT NULL AND status = 'done'
99
+ ORDER BY completed_at DESC LIMIT 1
100
+ `).get(projectName) as any;
101
+ return row?.conversation_id || null;
102
+ }
103
+
104
+ export function getTask(id: string): Task | null {
105
+ const row = db().prepare('SELECT * FROM tasks WHERE id = ?').get(id) as any;
106
+ if (!row) return null;
107
+ return rowToTask(row);
108
+ }
109
+
110
+ export function listTasks(status?: TaskStatus): Task[] {
111
+ let query = 'SELECT * FROM tasks';
112
+ const params: string[] = [];
113
+ if (status) {
114
+ query += ' WHERE status = ?';
115
+ params.push(status);
116
+ }
117
+ query += ' ORDER BY created_at DESC';
118
+ const rows = db().prepare(query).all(...params) as any[];
119
+ return rows.map(rowToTask);
120
+ }
121
+
122
+ export function cancelTask(id: string): boolean {
123
+ const task = getTask(id);
124
+ if (!task) return false;
125
+ if (task.status === 'done' || task.status === 'failed') return false;
126
+
127
+ // Cancel monitor tasks
128
+ if (task.mode === 'monitor' && activeMonitors.has(id)) {
129
+ cancelMonitor(id);
130
+ return true;
131
+ }
132
+
133
+ updateTaskStatus(id, 'cancelled');
134
+
135
+ // Clean up project lock if this was a running prompt task
136
+ if (task.status === 'running') {
137
+ runningProjects.delete(task.projectName);
138
+ }
139
+
140
+ return true;
141
+ }
142
+
143
+ export function deleteTask(id: string): boolean {
144
+ const task = getTask(id);
145
+ if (!task) return false;
146
+ if (task.status === 'running') cancelTask(id);
147
+ db().prepare('DELETE FROM tasks WHERE id = ?').run(id);
148
+ return true;
149
+ }
150
+
151
+ export function updateTask(id: string, updates: { prompt?: string; projectName?: string; projectPath?: string; priority?: number; scheduledAt?: string; restart?: boolean }): Task | null {
152
+ const task = getTask(id);
153
+ if (!task) return null;
154
+
155
+ // If running, cancel first
156
+ if (task.status === 'running') cancelTask(id);
157
+
158
+ const fields: string[] = [];
159
+ const values: any[] = [];
160
+ if (updates.prompt !== undefined) { fields.push('prompt = ?'); values.push(updates.prompt); }
161
+ if (updates.projectName !== undefined) { fields.push('project_name = ?'); values.push(updates.projectName); }
162
+ if (updates.projectPath !== undefined) { fields.push('project_path = ?'); values.push(updates.projectPath); }
163
+ if (updates.priority !== undefined) { fields.push('priority = ?'); values.push(updates.priority); }
164
+ if (updates.scheduledAt !== undefined) { fields.push('scheduled_at = ?'); values.push(updates.scheduledAt || null); }
165
+
166
+ // Reset to queued so it runs again
167
+ if (updates.restart) {
168
+ fields.push("status = 'queued'", 'started_at = NULL', 'completed_at = NULL', 'error = NULL', "log = '[]'", 'result_summary = NULL', 'git_diff = NULL', 'cost_usd = NULL');
169
+ }
170
+
171
+ if (fields.length === 0) return task;
172
+
173
+ values.push(id);
174
+ db().prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values);
175
+
176
+ if (updates.restart) ensureRunnerStarted();
177
+
178
+ return getTask(id);
179
+ }
180
+
181
+ export function retryTask(id: string): Task | null {
182
+ const task = getTask(id);
183
+ if (!task) return null;
184
+ if (task.status !== 'failed' && task.status !== 'cancelled') return null;
185
+
186
+ // Create a new task with same params (including agent)
187
+ return createTask({
188
+ projectName: task.projectName,
189
+ projectPath: task.projectPath,
190
+ prompt: task.prompt,
191
+ priority: task.priority,
192
+ agent: (task as any).agent || undefined,
193
+ });
194
+ }
195
+
196
+ // ─── Background Runner ───────────────────────────────────────
197
+
198
+ export function ensureRunnerStarted() {
199
+ if (runnerState.runner) return;
200
+ runnerState.runner = setInterval(processNextTask, 3000);
201
+ // Also try immediately
202
+ processNextTask();
203
+ }
204
+
205
+ export function stopRunner() {
206
+ if (runnerState.runner) {
207
+ clearInterval(runnerState.runner);
208
+ runnerState.runner = null;
209
+ }
210
+ }
211
+
212
+ async function processNextTask() {
213
+ // Find all queued tasks ready to run
214
+ const queued = db().prepare(`
215
+ SELECT * FROM tasks WHERE status = 'queued'
216
+ AND (scheduled_at IS NULL OR replace(replace(scheduled_at, 'T', ' '), 'Z', '') <= datetime('now'))
217
+ ORDER BY priority DESC, created_at ASC
218
+ `).all() as any[];
219
+
220
+ for (const next of queued) {
221
+ const task = rowToTask(next);
222
+
223
+ if (task.mode === 'monitor') {
224
+ // Monitor tasks run in background, don't block the runner
225
+ startMonitorTask(task);
226
+ continue;
227
+ }
228
+
229
+ // Skip if this project already has a running prompt task
230
+ if (runningProjects.has(task.projectName)) continue;
231
+
232
+ // Run this task
233
+ runningProjects.add(task.projectName);
234
+ runnerState.currentTaskId = task.id;
235
+
236
+ // Execute async — don't await so we can process tasks for other projects in parallel
237
+ executeTask(task)
238
+ .catch((err: any) => {
239
+ appendLog(task.id, { type: 'system', subtype: 'error', content: err.message, timestamp: new Date().toISOString() });
240
+ updateTaskStatus(task.id, 'failed', err.message);
241
+ })
242
+ .finally(() => {
243
+ runningProjects.delete(task.projectName);
244
+ if (runnerState.currentTaskId === task.id) runnerState.currentTaskId = null;
245
+ });
246
+ }
247
+ }
248
+
249
+ function executeShellTask(task: Task): Promise<void> {
250
+ return new Promise((resolve) => {
251
+ updateTaskStatus(task.id, 'running');
252
+ db().prepare('UPDATE tasks SET started_at = datetime(\'now\') WHERE id = ?').run(task.id);
253
+ console.log(`[task:shell] ${task.projectName}: "${task.prompt.slice(0, 80)}"`);
254
+
255
+ const child = spawn('bash', ['-c', task.prompt], {
256
+ cwd: task.projectPath,
257
+ env: { ...process.env },
258
+ stdio: ['ignore', 'pipe', 'pipe'],
259
+ });
260
+
261
+ let stdout = '';
262
+ let stderr = '';
263
+ child.stdout.on('data', (chunk: Buffer) => {
264
+ const text = chunk.toString();
265
+ stdout += text;
266
+ appendLog(task.id, { type: 'system', subtype: 'text', content: text, timestamp: new Date().toISOString() });
267
+ });
268
+ child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });
269
+
270
+ child.on('exit', (code) => {
271
+ if (code === 0) {
272
+ db().prepare('UPDATE tasks SET status = ?, result_summary = ?, completed_at = datetime(\'now\') WHERE id = ?')
273
+ .run('done', stdout.trim(), task.id);
274
+ emit(task.id, 'status', 'done');
275
+ } else {
276
+ const errMsg = stderr.trim() || `Exit code ${code}`;
277
+ db().prepare('UPDATE tasks SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?')
278
+ .run('failed', errMsg, task.id);
279
+ emit(task.id, 'status', 'failed');
280
+ }
281
+ resolve();
282
+ });
283
+ });
284
+ }
285
+
286
+ function executeTask(task: Task): Promise<void> {
287
+ if (task.mode === 'shell') return executeShellTask(task);
288
+
289
+ return new Promise((resolve, reject) => {
290
+ const settings = loadSettings();
291
+ const { getAgent } = require('./agents');
292
+ const agentId = (task as any).agent || settings.defaultAgent || 'claude';
293
+ const adapter = getAgent(agentId);
294
+
295
+ // Model priority: per-task override > agent scene model > global taskModel
296
+ // "default" means "no override" — fall through to next level
297
+ const agentCfg = settings.agents?.[agentId];
298
+ const agentModel = agentCfg?.models?.task;
299
+ const effectiveAgentModel = agentModel && agentModel !== 'default' ? agentModel : null;
300
+ const model = taskModelOverrides.get(task.id) || effectiveAgentModel || settings.taskModel;
301
+ const supportsModel = adapter.config.capabilities?.supportsModel;
302
+ const spawnOpts = adapter.buildTaskSpawn({
303
+ projectPath: task.projectPath,
304
+ prompt: task.prompt,
305
+ model: supportsModel && model && model !== 'default' ? model : undefined,
306
+ conversationId: task.conversationId || undefined,
307
+ skipPermissions: true,
308
+ outputFormat: adapter.config.capabilities?.supportsStreamJson ? 'stream-json' : undefined,
309
+ });
310
+
311
+ const env = { ...process.env, ...(spawnOpts.env || {}) };
312
+ delete env.CLAUDECODE;
313
+
314
+ updateTaskStatus(task.id, 'running');
315
+ db().prepare('UPDATE tasks SET started_at = datetime(\'now\') WHERE id = ?').run(task.id);
316
+
317
+ const agentName = adapter.config.name || agentId;
318
+ console.log(`[task] ${task.projectName} [${agentName}${supportsModel && model ? '/' + model : ''}]: "${task.prompt.slice(0, 60)}..."`);
319
+
320
+ // Log agent info as first entry
321
+ appendLog(task.id, { type: 'system', subtype: 'init', content: `Agent: ${agentName}${supportsModel && model && model !== 'default' ? ` | Model: ${model}` : ''}`, timestamp: new Date().toISOString() });
322
+
323
+ const needsTTY = adapter.config.capabilities?.requiresTTY;
324
+ let child: any;
325
+ let ptyProcess: any = null;
326
+
327
+ if (needsTTY) {
328
+ // Use node-pty for agents that require a terminal environment
329
+ const pty = require('node-pty');
330
+ ptyProcess = pty.spawn(spawnOpts.cmd, spawnOpts.args, {
331
+ name: 'xterm-256color',
332
+ cols: 120,
333
+ rows: 40,
334
+ cwd: task.projectPath,
335
+ env,
336
+ });
337
+ // Strip terminal control codes from PTY output for clean logging
338
+ const stripAnsi = (s: string) => s
339
+ .replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '') // CSI sequences
340
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC sequences
341
+ .replace(/\x1b[()][0-9A-B]/g, '') // charset
342
+ .replace(/\x1b[=>]/g, '') // keypad
343
+ .replace(/\r/g, '') // carriage return
344
+ .replace(/\x07/g, ''); // bell
345
+
346
+ // Auto-kill PTY after idle (interactive agents don't exit on their own)
347
+ let ptyBytes = 0;
348
+ let ptyIdleTimer: any = null;
349
+ const PTY_IDLE_MS = 15000; // 15s idle = done
350
+
351
+ // Create a child-like interface for pty
352
+ let exitCb: Function | null = null;
353
+
354
+ ptyProcess.onData((data: string) => {
355
+ const clean = stripAnsi(data);
356
+ ptyBytes += clean.length;
357
+ if (dataCallback) dataCallback(Buffer.from(clean));
358
+ // Reset idle timer
359
+ if (ptyIdleTimer) clearTimeout(ptyIdleTimer);
360
+ if (ptyBytes > 500) {
361
+ ptyIdleTimer = setTimeout(() => {
362
+ console.log(`[task] PTY idle timeout — killing process (${ptyBytes} bytes received)`);
363
+ try { ptyProcess.kill(); } catch {}
364
+ }, PTY_IDLE_MS);
365
+ }
366
+ });
367
+
368
+ ptyProcess.onExit(({ exitCode }: any) => {
369
+ if (ptyIdleTimer) clearTimeout(ptyIdleTimer);
370
+ if (exitCb) exitCb(exitCode, null);
371
+ });
372
+
373
+ let dataCallback: Function | null = null;
374
+ child = {
375
+ stdout: { on: (evt: string, cb: Function) => { if (evt === 'data') dataCallback = cb; } },
376
+ stderr: { on: (_evt: string, _cb: Function) => {} },
377
+ on: (evt: string, cb: Function) => { if (evt === 'exit') exitCb = cb; if (evt === 'error') {} },
378
+ kill: (sig: string) => { if (ptyIdleTimer) clearTimeout(ptyIdleTimer); try { ptyProcess.kill(sig); } catch {} },
379
+ stdin: null,
380
+ pid: ptyProcess.pid,
381
+ };
382
+ } else {
383
+ child = spawn(spawnOpts.cmd, spawnOpts.args, {
384
+ cwd: task.projectPath,
385
+ env,
386
+ stdio: ['pipe', 'pipe', 'pipe'],
387
+ });
388
+ child.stdin?.end();
389
+ }
390
+
391
+ let buffer = '';
392
+ let resultText = '';
393
+ let totalCost = 0;
394
+ let sessionId = '';
395
+ let modelUsed = '';
396
+ let totalInputTokens = 0;
397
+ let totalOutputTokens = 0;
398
+
399
+ child.on('error', (err: any) => {
400
+ console.error(`[task-runner] Spawn error:`, err.message);
401
+ updateTaskStatus(task.id, 'failed', err.message);
402
+ reject(err);
403
+ });
404
+
405
+ child.stdout?.on('data', (data: Buffer) => {
406
+ // stdout chunk processing (silent)
407
+
408
+ // Check if cancelled
409
+ if (getTask(task.id)?.status === 'cancelled') {
410
+ child.kill('SIGTERM');
411
+ return;
412
+ }
413
+
414
+ buffer += data.toString();
415
+ const lines = buffer.split('\n');
416
+ buffer = lines.pop() || '';
417
+
418
+ for (const line of lines) {
419
+ if (!line.trim()) continue;
420
+ let jsonParsed = false;
421
+ try {
422
+ const parsed = JSON.parse(line);
423
+ jsonParsed = true;
424
+ const entries = parseStreamJson(parsed);
425
+ for (const entry of entries) {
426
+ // Skip Claude's Model init line for non-claude agents (we already logged our own)
427
+ if (entry.subtype === 'init' && agentId !== 'claude' && entry.content?.startsWith('Model:')) continue;
428
+ appendLog(task.id, entry);
429
+ }
430
+
431
+ if (parsed.session_id) sessionId = parsed.session_id;
432
+ if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.model) {
433
+ modelUsed = parsed.model;
434
+ }
435
+ // Accumulate token usage from assistant messages
436
+ if (parsed.type === 'assistant' && parsed.message?.usage) {
437
+ totalInputTokens += parsed.message.usage.input_tokens || 0;
438
+ totalOutputTokens += parsed.message.usage.output_tokens || 0;
439
+ }
440
+ if (parsed.type === 'result') {
441
+ resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
442
+ totalCost = parsed.total_cost_usd || 0;
443
+ if (parsed.total_input_tokens) totalInputTokens = parsed.total_input_tokens;
444
+ if (parsed.total_output_tokens) totalOutputTokens = parsed.total_output_tokens;
445
+ }
446
+ } catch {
447
+ // Non-JSON output (generic agents) — log as raw text
448
+ if (!jsonParsed) {
449
+ resultText += (resultText ? '\n' : '') + line;
450
+ appendLog(task.id, { type: 'system', subtype: 'text', content: line, timestamp: new Date().toISOString() });
451
+ }
452
+ }
453
+ }
454
+ });
455
+
456
+ child.stderr?.on('data', (data: Buffer) => {
457
+ const text = data.toString().trim();
458
+ // stderr logged to task log only
459
+ if (text) {
460
+ appendLog(task.id, { type: 'system', subtype: 'error', content: text, timestamp: new Date().toISOString() });
461
+ }
462
+ });
463
+
464
+ child.on('exit', (code: any, signal: any) => {
465
+ // Process exit handled below
466
+ // Process remaining buffer
467
+ if (buffer.trim()) {
468
+ try {
469
+ const parsed = JSON.parse(buffer);
470
+ const entries = parseStreamJson(parsed);
471
+ for (const entry of entries) appendLog(task.id, entry);
472
+ if (parsed.type === 'result') {
473
+ resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
474
+ totalCost = parsed.total_cost_usd || 0;
475
+ }
476
+ } catch {}
477
+ }
478
+
479
+ // Save conversation ID for follow-up
480
+ if (sessionId) {
481
+ db().prepare('UPDATE tasks SET conversation_id = ? WHERE id = ?').run(sessionId, task.id);
482
+ }
483
+
484
+ // Capture git diff
485
+ try {
486
+ const { execSync } = require('node:child_process');
487
+ const diff = execSync('git diff HEAD', { cwd: task.projectPath, timeout: 5000 }).toString();
488
+ if (diff.trim()) {
489
+ db().prepare('UPDATE tasks SET git_diff = ? WHERE id = ?').run(diff, task.id);
490
+ }
491
+ } catch {}
492
+
493
+ const currentStatus = getTask(task.id)?.status;
494
+ if (currentStatus === 'cancelled') {
495
+ resolve();
496
+ return;
497
+ }
498
+
499
+ if (code === 0) {
500
+ db().prepare(`
501
+ UPDATE tasks SET status = 'done', result_summary = ?, cost_usd = ?, completed_at = datetime('now')
502
+ WHERE id = ?
503
+ `).run(resultText, totalCost, task.id);
504
+ emit(task.id, 'status', 'done');
505
+ console.log(`[task] Done: ${task.id} ${task.projectName} (cost: $${totalCost?.toFixed(4) || '0'}, ${totalInputTokens}in/${totalOutputTokens}out)`);
506
+ // Record usage
507
+ try {
508
+ const { recordUsage } = require('./usage-scanner');
509
+ let isPipeline = false;
510
+ try { const { pipelineTaskIds: ptids } = require('./pipeline'); isPipeline = ptids.has(task.id); } catch {}
511
+ recordUsage({
512
+ sessionId: sessionId || task.id,
513
+ source: isPipeline ? 'pipeline' : 'task',
514
+ projectPath: task.projectPath,
515
+ projectName: task.projectName,
516
+ model: modelUsed || 'unknown',
517
+ inputTokens: totalInputTokens,
518
+ outputTokens: totalOutputTokens,
519
+ taskId: task.id,
520
+ });
521
+ } catch {}
522
+ const doneTask = getTask(task.id);
523
+ if (doneTask) notifyTaskComplete(doneTask).catch(() => {});
524
+ notifyTerminalSession(task, 'done', sessionId);
525
+ resolve();
526
+ } else {
527
+ const errMsg = `Process exited with code ${code}`;
528
+ console.error(`[task] Failed: ${task.id} ${task.projectName} — ${errMsg}`);
529
+ updateTaskStatus(task.id, 'failed', errMsg);
530
+ const failedTask = getTask(task.id);
531
+ if (failedTask) notifyTaskFailed(failedTask).catch(() => {});
532
+ notifyTerminalSession(task, 'failed', sessionId);
533
+ reject(new Error(errMsg));
534
+ }
535
+ });
536
+
537
+ child.on('error', (err: any) => {
538
+ updateTaskStatus(task.id, 'failed', err.message);
539
+ reject(err);
540
+ });
541
+ });
542
+ }
543
+
544
+ // ─── Terminal notification ────────────────────────────────────
545
+
546
+ /**
547
+ * Notify tmux terminal sessions in the same project directory that a task completed.
548
+ * Sends a visible bell character so the user knows to resume.
549
+ */
550
+ function notifyTerminalSession(task: Task, status: 'done' | 'failed', sessionId?: string) {
551
+ // Skip pipeline tasks — they have their own notification system
552
+ try {
553
+ const { pipelineTaskIds } = require('./pipeline');
554
+ if (pipelineTaskIds.has(task.id)) return;
555
+ } catch {}
556
+
557
+ try {
558
+ const out = execSync(
559
+ `tmux list-sessions -F "#{session_name}" 2>/dev/null`,
560
+ { encoding: 'utf-8', timeout: 3000 }
561
+ ).trim();
562
+ if (!out) return;
563
+
564
+ for (const name of out.split('\n')) {
565
+ if (!name.startsWith('mw-')) continue;
566
+ try {
567
+ const cwd = execSync(
568
+ `tmux display-message -p -t ${name} '#{pane_current_path}'`,
569
+ { encoding: 'utf-8', timeout: 2000 }
570
+ ).trim();
571
+
572
+ // Match: same dir, parent dir, or child dir
573
+ const match = cwd && (
574
+ cwd === task.projectPath ||
575
+ cwd.startsWith(task.projectPath + '/') ||
576
+ task.projectPath.startsWith(cwd + '/')
577
+ );
578
+ if (!match) continue;
579
+
580
+ const paneCmd = execSync(
581
+ `tmux display-message -p -t ${name} '#{pane_current_command}'`,
582
+ { encoding: 'utf-8', timeout: 2000 }
583
+ ).trim();
584
+
585
+ if (status === 'done') {
586
+ const summary = task.prompt.slice(0, 80).replace(/"/g, "'");
587
+ const msg = `A background task just completed. Task: "${summary}". Please check git diff and continue.`;
588
+
589
+ // If a process is running (claude/node), send as input
590
+ if (paneCmd !== 'zsh' && paneCmd !== 'bash' && paneCmd !== 'fish') {
591
+ execSync(`tmux send-keys -t ${name} -- "${msg.replace(/"/g, '\\"')}" Enter`, { timeout: 2000 });
592
+ } else {
593
+ execSync(`tmux display-message -t ${name} "✅ Task ${task.id} done — changes ready"`, { timeout: 2000 });
594
+ }
595
+ } else {
596
+ execSync(`tmux display-message -t ${name} "❌ Task ${task.id} failed"`, { timeout: 2000 });
597
+ }
598
+ } catch {}
599
+ }
600
+ } catch {}
601
+ }
602
+
603
+ // ─── Helpers ─────────────────────────────────────────────────
604
+
605
+ /**
606
+ * Resolve the claude binary path. `claude` is typically a symlink to a .js file,
607
+ * which can't be spawned directly without a shell. We resolve to the real .js path
608
+ * and run it with `node`.
609
+ */
610
+ function resolveClaudePath(claudePath: string): { cmd: string; prefix: string[] } {
611
+ try {
612
+ // Try to find the real path
613
+ let resolved = claudePath;
614
+ try {
615
+ const which = execSync(`which ${claudePath}`, { encoding: 'utf-8' }).trim();
616
+ resolved = realpathSync(which);
617
+ } catch {
618
+ resolved = realpathSync(claudePath);
619
+ }
620
+
621
+ // If it's a .js file, run with node
622
+ if (resolved.endsWith('.js') || resolved.endsWith('.mjs')) {
623
+ return { cmd: process.execPath, prefix: [resolved] };
624
+ }
625
+
626
+ return { cmd: resolved, prefix: [] };
627
+ } catch {
628
+ // Fallback: use node to run it
629
+ return { cmd: process.execPath, prefix: [claudePath] };
630
+ }
631
+ }
632
+
633
+ function parseStreamJson(parsed: any): TaskLogEntry[] {
634
+ const entries: TaskLogEntry[] = [];
635
+ const ts = new Date().toISOString();
636
+
637
+ if (parsed.type === 'system' && parsed.subtype === 'init') {
638
+ entries.push({ type: 'system', subtype: 'init', content: `Model: ${parsed.model || 'unknown'}`, timestamp: ts });
639
+ return entries;
640
+ }
641
+
642
+ if (parsed.type === 'assistant' && parsed.message?.content) {
643
+ for (const block of parsed.message.content) {
644
+ if (block.type === 'text' && block.text) {
645
+ entries.push({ type: 'assistant', subtype: 'text', content: block.text, timestamp: ts });
646
+ } else if (block.type === 'tool_use') {
647
+ entries.push({
648
+ type: 'assistant',
649
+ subtype: 'tool_use',
650
+ content: typeof block.input === 'string' ? block.input : JSON.stringify(block.input || {}),
651
+ tool: block.name,
652
+ timestamp: ts,
653
+ });
654
+ } else if (block.type === 'tool_result') {
655
+ entries.push({
656
+ type: 'assistant',
657
+ subtype: 'tool_result',
658
+ content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content || ''),
659
+ timestamp: ts,
660
+ });
661
+ }
662
+ }
663
+ return entries;
664
+ }
665
+
666
+ if (parsed.type === 'result') {
667
+ entries.push({
668
+ type: 'result',
669
+ subtype: parsed.subtype || 'success',
670
+ content: typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result || ''),
671
+ timestamp: ts,
672
+ });
673
+ return entries;
674
+ }
675
+
676
+ if (parsed.type === 'rate_limit_event') return entries;
677
+
678
+ entries.push({ type: 'assistant', subtype: parsed.type || 'unknown', content: JSON.stringify(parsed), timestamp: ts });
679
+ return entries;
680
+ }
681
+
682
+ function appendLog(taskId: string, entry: TaskLogEntry) {
683
+ const row = db().prepare('SELECT log FROM tasks WHERE id = ?').get(taskId) as any;
684
+ if (!row) return;
685
+ const log: TaskLogEntry[] = JSON.parse(row.log);
686
+ log.push(entry);
687
+ db().prepare('UPDATE tasks SET log = ? WHERE id = ?').run(JSON.stringify(log), taskId);
688
+ emit(taskId, 'log', entry);
689
+ }
690
+
691
+ function updateTaskStatus(id: string, status: TaskStatus, error?: string) {
692
+ if (status === 'failed' || status === 'cancelled') {
693
+ db().prepare('UPDATE tasks SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?').run(status, error || null, id);
694
+ } else {
695
+ db().prepare('UPDATE tasks SET status = ? WHERE id = ?').run(status, id);
696
+ }
697
+ emit(id, 'status', status);
698
+ }
699
+
700
+ function rowToTask(row: any): Task {
701
+ return {
702
+ id: row.id,
703
+ projectName: row.project_name,
704
+ projectPath: row.project_path,
705
+ prompt: row.prompt,
706
+ mode: row.mode || 'prompt',
707
+ status: row.status,
708
+ priority: row.priority,
709
+ conversationId: row.conversation_id || undefined,
710
+ watchConfig: row.watch_config ? JSON.parse(row.watch_config) : undefined,
711
+ log: JSON.parse(row.log || '[]'),
712
+ resultSummary: row.result_summary || undefined,
713
+ gitDiff: row.git_diff || undefined,
714
+ gitBranch: row.git_branch || undefined,
715
+ costUSD: row.cost_usd || undefined,
716
+ error: row.error || undefined,
717
+ createdAt: toIsoUTC(row.created_at) ?? row.created_at,
718
+ startedAt: toIsoUTC(row.started_at) ?? undefined,
719
+ completedAt: toIsoUTC(row.completed_at) ?? undefined,
720
+ scheduledAt: toIsoUTC(row.scheduled_at) ?? undefined,
721
+ agent: row.agent || undefined,
722
+ } as Task;
723
+ }
724
+
725
+ // ─── Monitor task execution ──────────────────────────────────
726
+
727
+ import { getSessionFilePath, readSessionEntries, tailSessionFile, type SessionEntry } from './claude-sessions';
728
+
729
+ const activeMonitors = new Map<string, () => void>(); // taskId → cleanup fn
730
+
731
+ function startMonitorTask(task: Task) {
732
+ if (!task.conversationId || !task.watchConfig) {
733
+ updateTaskStatus(task.id, 'failed', 'Monitor task requires a session and watch config');
734
+ return;
735
+ }
736
+
737
+ const config = task.watchConfig;
738
+ const fp = getSessionFilePath(task.projectName, task.conversationId);
739
+ if (!fp) {
740
+ updateTaskStatus(task.id, 'failed', `Session file not found: ${task.conversationId}`);
741
+ return;
742
+ }
743
+
744
+ console.log(`[monitor] Starting monitor ${task.id} for ${task.projectName}/${task.conversationId.slice(0, 8)} — condition: ${config.condition}, action: ${config.action}, file: ${fp}`);
745
+
746
+ updateTaskStatus(task.id, 'running');
747
+ appendLog(task.id, {
748
+ type: 'system', subtype: 'init',
749
+ content: `Monitoring session ${task.conversationId.slice(0, 12)} — condition: ${config.condition}, action: ${config.action}`,
750
+ timestamp: new Date().toISOString(),
751
+ });
752
+
753
+ // Read initial state
754
+ const initialEntries = readSessionEntries(fp);
755
+ let lastEntryCount = initialEntries.length;
756
+ let lastActivityTime = Date.now();
757
+
758
+ // Idle check timer
759
+ let idleTimer: ReturnType<typeof setInterval> | null = null;
760
+ if (config.condition === 'idle') {
761
+ const idleMs = (config.idleMinutes || 10) * 60_000;
762
+ idleTimer = setInterval(() => {
763
+ if (Date.now() - lastActivityTime > idleMs) {
764
+ triggerMonitorAction(task, `Session idle for ${config.idleMinutes || 10} minutes`);
765
+ if (!config.repeat) stopMonitor(task.id);
766
+ }
767
+ }, 30_000);
768
+ }
769
+
770
+ // Notification throttling: batch updates and send at most once per interval
771
+ const notifyInterval = (config.notifyIntervalSeconds || 60) * 1000;
772
+ let lastNotifyTime = 0;
773
+ let pendingContext: string[] = [];
774
+ let notifyTimer: ReturnType<typeof setTimeout> | null = null;
775
+
776
+ function scheduleNotify(context: string, immediate?: boolean) {
777
+ pendingContext.push(context);
778
+ if (immediate) {
779
+ flushNotify();
780
+ return;
781
+ }
782
+ if (notifyTimer) return; // already scheduled
783
+ const elapsed = Date.now() - lastNotifyTime;
784
+ const delay = Math.max(0, notifyInterval - elapsed);
785
+ notifyTimer = setTimeout(flushNotify, delay);
786
+ }
787
+
788
+ function flushNotify() {
789
+ if (notifyTimer) { clearTimeout(notifyTimer); notifyTimer = null; }
790
+ if (pendingContext.length === 0) return;
791
+ const summary = pendingContext.length === 1
792
+ ? pendingContext[0]
793
+ : `${pendingContext.length} updates:\n\n${pendingContext.slice(-5).join('\n\n')}`;
794
+ pendingContext = [];
795
+ lastNotifyTime = Date.now();
796
+ triggerMonitorAction(task, summary);
797
+ }
798
+
799
+ // Tail the file for changes (uses fs.watch + 5s polling fallback)
800
+ const stopTail = tailSessionFile(fp, (newEntries) => {
801
+ lastActivityTime = Date.now();
802
+ lastEntryCount += newEntries.length;
803
+
804
+ // Check conditions
805
+ if (config.condition === 'change') {
806
+ scheduleNotify(summarizeNewEntries(newEntries));
807
+ if (!config.repeat) stopMonitor(task.id);
808
+ }
809
+
810
+ if (config.condition === 'keyword' && config.keyword) {
811
+ const kw = config.keyword.toLowerCase();
812
+ const matched = newEntries.find(e => e.content.toLowerCase().includes(kw));
813
+ if (matched) {
814
+ scheduleNotify(`Keyword "${config.keyword}" found: ${matched.content.slice(0, 200)}`, true);
815
+ if (!config.repeat) stopMonitor(task.id);
816
+ }
817
+ }
818
+
819
+ if (config.condition === 'error') {
820
+ const errors = newEntries.filter(e =>
821
+ e.type === 'system' && e.content.toLowerCase().includes('error')
822
+ );
823
+ if (errors.length > 0) {
824
+ scheduleNotify(`Error detected: ${errors[0].content.slice(0, 200)}`, true);
825
+ if (!config.repeat) stopMonitor(task.id);
826
+ }
827
+ }
828
+
829
+ if (config.condition === 'complete') {
830
+ // Check if last assistant entry looks like completion
831
+ const lastAssistant = [...newEntries].reverse().find(e => e.type === 'assistant_text');
832
+ if (lastAssistant) {
833
+ // Heuristic: check if there are no more tool calls after the last text
834
+ const lastIdx = newEntries.lastIndexOf(lastAssistant);
835
+ const afterToolUse = newEntries.slice(lastIdx + 1).some(e => e.type === 'tool_use');
836
+ if (!afterToolUse && newEntries.length > 2) {
837
+ // Wait a bit to see if more entries come
838
+ setTimeout(() => {
839
+ if (Date.now() - lastActivityTime > 30_000) {
840
+ scheduleNotify(`Session appears complete.\n\nLast: ${lastAssistant.content.slice(0, 300)}`, true);
841
+ if (!config.repeat) stopMonitor(task.id);
842
+ }
843
+ }, 35_000);
844
+ }
845
+ }
846
+ }
847
+ }, (err) => {
848
+ console.error(`[monitor] ${task.id} tail error:`, err.message);
849
+ appendLog(task.id, {
850
+ type: 'system', subtype: 'error',
851
+ content: `File watch error: ${err.message}`,
852
+ timestamp: new Date().toISOString(),
853
+ });
854
+ });
855
+
856
+ const cleanup = () => {
857
+ stopTail();
858
+ if (idleTimer) clearInterval(idleTimer);
859
+ flushNotify(); // send any remaining batched notifications
860
+ };
861
+
862
+ activeMonitors.set(task.id, cleanup);
863
+ }
864
+
865
+ function stopMonitor(taskId: string) {
866
+ const cleanup = activeMonitors.get(taskId);
867
+ if (cleanup) {
868
+ cleanup();
869
+ activeMonitors.delete(taskId);
870
+ }
871
+ updateTaskStatus(taskId, 'done');
872
+ }
873
+
874
+ // Also export for cancel
875
+ export function cancelMonitor(taskId: string) {
876
+ stopMonitor(taskId);
877
+ updateTaskStatus(taskId, 'cancelled');
878
+ }
879
+
880
+ async function triggerMonitorAction(task: Task, context: string) {
881
+ const config = task.watchConfig!;
882
+
883
+ appendLog(task.id, {
884
+ type: 'system', subtype: 'text',
885
+ content: `⚡ Triggered: ${context}`,
886
+ timestamp: new Date().toISOString(),
887
+ });
888
+
889
+ if (config.action === 'notify') {
890
+ // Send Telegram notification
891
+ const settings = loadSettings();
892
+ if (settings.telegramBotToken && settings.telegramChatId) {
893
+ const msg = config.actionPrompt
894
+ ? config.actionPrompt.replace('{{context}}', context)
895
+ : `📋 Monitor: ${task.projectName}/${task.conversationId?.slice(0, 8)}\n\n${context}`;
896
+ await sendTelegramDirect(settings.telegramBotToken, settings.telegramChatId, msg);
897
+ }
898
+ } else if (config.action === 'message' && config.actionPrompt && task.conversationId) {
899
+ // Send a message to the session by creating a prompt task (will queue if project is busy)
900
+ const newTask = createTask({
901
+ projectName: task.projectName,
902
+ projectPath: task.projectPath,
903
+ prompt: config.actionPrompt,
904
+ conversationId: task.conversationId,
905
+ });
906
+ const queued = runningProjects.has(task.projectName) ? ' (queued — project busy)' : '';
907
+ appendLog(task.id, {
908
+ type: 'system', subtype: 'text',
909
+ content: `Created follow-up task ${newTask.id}${queued}: ${config.actionPrompt.slice(0, 100)}`,
910
+ timestamp: new Date().toISOString(),
911
+ });
912
+ } else if (config.action === 'task' && config.actionPrompt) {
913
+ const project = config.actionProject || task.projectName;
914
+ createTask({
915
+ projectName: project,
916
+ projectPath: task.projectPath,
917
+ prompt: config.actionPrompt,
918
+ });
919
+ appendLog(task.id, {
920
+ type: 'system', subtype: 'text',
921
+ content: `Created new task for ${project}: ${config.actionPrompt.slice(0, 100)}`,
922
+ timestamp: new Date().toISOString(),
923
+ });
924
+ }
925
+ }
926
+
927
+ async function sendTelegramDirect(token: string, chatId: string, text: string) {
928
+ try {
929
+ const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
930
+ method: 'POST',
931
+ headers: { 'Content-Type': 'application/json' },
932
+ body: JSON.stringify({ chat_id: chatId, text, disable_web_page_preview: true }),
933
+ });
934
+ if (!res.ok) {
935
+ const body = await res.text();
936
+ console.error(`[monitor] Telegram send failed: ${res.status} ${body}`);
937
+ }
938
+ } catch (err) {
939
+ console.error('[monitor] Telegram send error:', err);
940
+ }
941
+ }
942
+
943
+ function summarizeNewEntries(entries: SessionEntry[]): string {
944
+ const parts: string[] = [];
945
+ for (const e of entries) {
946
+ if (e.type === 'user') parts.push(`👤 ${e.content.slice(0, 100)}`);
947
+ else if (e.type === 'assistant_text') parts.push(`🤖 ${e.content.slice(0, 150)}`);
948
+ else if (e.type === 'tool_use') parts.push(`🔧 ${e.toolName || 'tool'}`);
949
+ }
950
+ return parts.slice(0, 5).join('\n') || 'Activity detected';
951
+ }