@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,1565 @@
1
+ /**
2
+ * Pipeline Engine — DAG-based workflow orchestration on top of the Task system.
3
+ *
4
+ * Workflow YAML → Pipeline instance → Nodes executed as Tasks
5
+ * Supports: dependencies, output passing, conditional routing, parallel execution, notifications.
6
+ */
7
+
8
+ import { randomUUID } from 'node:crypto';
9
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import YAML from 'yaml';
12
+ import { createTask, getTask, onTaskEvent, taskModelOverrides } from './task-manager';
13
+ import { getProjectInfo } from './projects';
14
+ import { loadSettings } from './settings';
15
+ import { getAgent, listAgents } from './agents';
16
+ import type { Task } from '@/src/types';
17
+ import { getDataDir } from './dirs';
18
+
19
+ const PIPELINES_DIR = join(getDataDir(), 'pipelines');
20
+ const WORKFLOWS_DIR = join(getDataDir(), 'flows');
21
+
22
+ // Track pipeline task IDs so terminal notifications can skip them (persists across hot-reloads)
23
+ const pipelineTaskKey = Symbol.for('mw-pipeline-task-ids');
24
+ const gPipeline = globalThis as any;
25
+ if (!gPipeline[pipelineTaskKey]) gPipeline[pipelineTaskKey] = new Set<string>();
26
+ export const pipelineTaskIds: Set<string> = gPipeline[pipelineTaskKey];
27
+
28
+ // ─── Types ────────────────────────────────────────────────
29
+
30
+ export interface WorkflowNode {
31
+ id: string;
32
+ project: string;
33
+ prompt: string;
34
+ mode?: 'claude' | 'shell' | 'plugin'; // default: 'claude', 'shell' runs command, 'plugin' runs plugin action
35
+ agent?: string; // agent ID (default: from settings)
36
+ branch?: string; // auto checkout this branch before running (supports templates)
37
+ worktree?: boolean; // default: true. Set false to skip worktree isolation (run in project dir directly)
38
+ // Plugin mode fields
39
+ plugin?: string; // plugin ID (e.g., 'jenkins', 'docker')
40
+ pluginAction?: string; // action name (e.g., 'trigger', 'build'), defaults to plugin's defaultAction
41
+ pluginParams?: Record<string, any>; // per-use parameters
42
+ pluginWait?: boolean; // auto-run 'wait' action after main action
43
+ dependsOn: string[];
44
+ outputs: { name: string; extract: 'result' | 'git_diff' | 'stdout' | 'plugin' }[];
45
+ routes: { condition: string; next: string }[];
46
+ maxIterations: number;
47
+ }
48
+
49
+ // ─── Conversation Mode Types ──────────────────────────────
50
+
51
+ export interface ConversationAgent {
52
+ id: string; // logical ID within this conversation (e.g., 'architect', 'implementer')
53
+ agent: string; // agent registry ID (e.g., 'claude', 'codex', 'aider')
54
+ role: string; // system prompt / role description
55
+ project?: string; // project context (optional, defaults to workflow input.project)
56
+ }
57
+
58
+ export interface ConversationMessage {
59
+ round: number;
60
+ agentId: string; // logical ID from ConversationAgent
61
+ agentName: string; // display name (resolved from registry)
62
+ content: string;
63
+ timestamp: string;
64
+ taskId?: string; // backing task ID
65
+ status: 'pending' | 'running' | 'done' | 'failed';
66
+ }
67
+
68
+ export interface ConversationConfig {
69
+ agents: ConversationAgent[];
70
+ maxRounds: number; // max back-and-forth rounds
71
+ stopCondition?: string; // e.g., "all agents say DONE", "any agent says DONE"
72
+ initialPrompt: string; // the seed prompt to kick off the conversation
73
+ contextStrategy?: 'full' | 'window' | 'summary'; // how to pass history, default: 'summary'
74
+ contextWindow?: number; // for 'window'/'summary': how many recent messages to include in full (default: 4)
75
+ maxContentLength?: number; // truncate each message to this length (default: 3000)
76
+ }
77
+
78
+ // ─── Workflow ─────────────────────────────────────────────
79
+
80
+ export interface Workflow {
81
+ name: string;
82
+ type?: 'dag' | 'conversation'; // default: 'dag'
83
+ description?: string;
84
+ vars: Record<string, string>;
85
+ input: Record<string, string>; // required input fields
86
+ nodes: Record<string, WorkflowNode>;
87
+ // Conversation mode fields (only when type === 'conversation')
88
+ conversation?: ConversationConfig;
89
+ }
90
+
91
+ export type PipelineNodeStatus = 'pending' | 'running' | 'done' | 'failed' | 'skipped';
92
+
93
+ export interface PipelineNodeState {
94
+ status: PipelineNodeStatus;
95
+ taskId?: string;
96
+ outputs: Record<string, string>;
97
+ iterations: number;
98
+ startedAt?: string;
99
+ completedAt?: string;
100
+ error?: string;
101
+ }
102
+
103
+ export interface Pipeline {
104
+ id: string;
105
+ workflowName: string;
106
+ type?: 'dag' | 'conversation'; // default: 'dag'
107
+ status: 'running' | 'done' | 'failed' | 'cancelled';
108
+ input: Record<string, string>;
109
+ vars: Record<string, string>;
110
+ nodes: Record<string, PipelineNodeState>;
111
+ nodeOrder: string[]; // for UI display
112
+ createdAt: string;
113
+ completedAt?: string;
114
+ // Conversation mode state
115
+ conversation?: {
116
+ config: ConversationConfig;
117
+ messages: ConversationMessage[];
118
+ currentRound: number;
119
+ currentAgentIndex: number; // index into config.agents
120
+ };
121
+ }
122
+
123
+ // ─── Workflow Loading ─────────────────────────────────────
124
+
125
+ // ─── Built-in workflows ──────────────────────────────────
126
+
127
+ export const BUILTIN_WORKFLOWS: Record<string, string> = {
128
+ 'issue-fix-and-review': `
129
+ name: issue-fix-and-review
130
+ description: "Fetch GitHub issue → fix code → create PR → review PR → notify"
131
+ input:
132
+ issue_id: "GitHub issue number"
133
+ project: "Project name"
134
+ base_branch: "Base branch (default: auto-detect)"
135
+ extra_context: "Additional instructions for the fix (optional)"
136
+ nodes:
137
+ setup:
138
+ mode: shell
139
+ project: "{{input.project}}"
140
+ prompt: |
141
+ cd "$(git rev-parse --show-toplevel)" && \
142
+ if [ -n "$(git status --porcelain)" ]; then echo "ERROR: Working directory has uncommitted changes. Please commit or stash first." && exit 1; fi && \
143
+ ORIG_BRANCH=$(git branch --show-current || git rev-parse --short HEAD) && \
144
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || git remote get-url origin | sed 's/.*github.com[:/]//;s/.git$//') && \
145
+ BASE="{{input.base_branch}}" && \
146
+ if [ -z "$BASE" ] || [ "$BASE" = "auto-detect" ]; then BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo main); fi && \
147
+ git checkout "$BASE" 2>/dev/null || true && \
148
+ git pull origin "$BASE" 2>/dev/null || true && \
149
+ OLD_BRANCH=$(git branch --list "fix/{{input.issue_id}}-*" | head -1 | tr -d ' *') && \
150
+ if [ -n "$OLD_BRANCH" ]; then git branch -D "$OLD_BRANCH" 2>/dev/null || true; fi && \
151
+ echo "REPO=$REPO" && echo "BASE=$BASE" && echo "ORIG_BRANCH=$ORIG_BRANCH"
152
+ outputs:
153
+ - name: info
154
+ extract: stdout
155
+ fetch-issue:
156
+ mode: shell
157
+ project: "{{input.project}}"
158
+ depends_on: [setup]
159
+ prompt: |
160
+ ISSUE_ID="{{input.issue_id}}" && \
161
+ if [ -z "$ISSUE_ID" ]; then echo "__SKIP__ No issue_id provided" && exit 0; fi && \
162
+ SETUP_INFO=$'{{nodes.setup.outputs.info}}' && \
163
+ REPO=$(echo "$SETUP_INFO" | grep REPO= | cut -d= -f2) && \
164
+ gh issue view "$ISSUE_ID" --json title,body,labels,number -R "$REPO"
165
+ outputs:
166
+ - name: issue_json
167
+ extract: stdout
168
+ fix-code:
169
+ project: "{{input.project}}"
170
+ depends_on: [fetch-issue]
171
+ prompt: |
172
+ A GitHub issue needs to be fixed. Here is the issue data:
173
+
174
+ {{nodes.fetch-issue.outputs.issue_json}}
175
+
176
+ Steps:
177
+ 1. Create a new branch from the current branch (which is already on the base). Name format: fix/{{input.issue_id}}-<short-description> (e.g. fix/3-add-validation, fix/15-null-pointer). Any old branch for this issue has been cleaned up.
178
+ 2. Analyze the issue and fix the code.
179
+ 3. Stage and commit with a message referencing #{{input.issue_id}}.
180
+
181
+ Base branch info: {{nodes.setup.outputs.info}}
182
+
183
+ Additional context from user: {{input.extra_context}}
184
+ outputs:
185
+ - name: summary
186
+ extract: result
187
+ - name: diff
188
+ extract: git_diff
189
+ push-and-pr:
190
+ mode: shell
191
+ project: "{{input.project}}"
192
+ depends_on: [fix-code]
193
+ prompt: |
194
+ SETUP_INFO=$'{{nodes.setup.outputs.info}}' && \
195
+ REPO=$(echo "$SETUP_INFO" | grep REPO= | cut -d= -f2) && \
196
+ BRANCH=$(git branch --show-current) && \
197
+ git push -u origin "$BRANCH" --force-with-lease 2>&1 && \
198
+ PR_URL=$(gh pr create --title "Fix #{{input.issue_id}}" \
199
+ --body "Auto-fix by Forge Pipeline for issue #{{input.issue_id}}." -R "$REPO" 2>/dev/null || \
200
+ gh pr view "$BRANCH" --json url -q .url -R "$REPO" 2>/dev/null) && \
201
+ echo "$PR_URL"
202
+ outputs:
203
+ - name: pr_url
204
+ extract: stdout
205
+ review:
206
+ project: "{{input.project}}"
207
+ depends_on: [push-and-pr]
208
+ prompt: |
209
+ Review the code changes for issue #{{input.issue_id}}.
210
+
211
+ Fix summary: {{nodes.fix-code.outputs.summary}}
212
+
213
+ Git diff:
214
+ {{nodes.fix-code.outputs.diff}}
215
+
216
+ Check for:
217
+ - Bugs and logic errors
218
+ - Security vulnerabilities
219
+ - Performance issues
220
+ - Whether the fix actually addresses the issue
221
+
222
+ Respond with:
223
+ 1. APPROVED or CHANGES_REQUESTED
224
+ 2. Specific issues found with file paths and line numbers
225
+ outputs:
226
+ - name: review_result
227
+ extract: result
228
+ cleanup:
229
+ mode: shell
230
+ project: "{{input.project}}"
231
+ depends_on: [review]
232
+ prompt: |
233
+ SETUP_INFO=$'{{nodes.setup.outputs.info}}' && \
234
+ ORIG=$(echo "$SETUP_INFO" | grep ORIG_BRANCH= | cut -d= -f2) && \
235
+ PR_URL=$'{{nodes.push-and-pr.outputs.pr_url}}' && \
236
+ if [ -n "$(git status --porcelain)" ]; then
237
+ echo "Issue #{{input.issue_id}} — PR: $PR_URL (staying on $(git branch --show-current))"
238
+ else
239
+ git checkout "$ORIG" 2>/dev/null || true
240
+ echo "Issue #{{input.issue_id}} — PR: $PR_URL (switched back to $ORIG)"
241
+ fi
242
+ outputs:
243
+ - name: result
244
+ extract: stdout
245
+ `,
246
+ 'multi-agent-collaboration': `
247
+ name: multi-agent-collaboration
248
+ type: conversation
249
+ description: "Two agents collaborate: one designs, one implements"
250
+ input:
251
+ project: "Project name"
252
+ task: "What to build or fix"
253
+ agents:
254
+ - id: architect
255
+ agent: claude
256
+ role: "You are a software architect. Round 1: design the solution with clear steps. Later rounds: review the implementation and say DONE if satisfied."
257
+ - id: implementer
258
+ agent: claude
259
+ role: "You are a developer. Implement what the architect designs. After implementing, say DONE."
260
+ max_rounds: 3
261
+ stop_condition: "both agents say DONE"
262
+ initial_prompt: "Task: {{input.task}}"
263
+ `,
264
+ 'review-mr': `
265
+ name: review-mr
266
+ description: "Review PR — AI code review with GitHub comment"
267
+ input:
268
+ project: "Project name"
269
+ branch: "Branch name or PR number (empty = auto-detect latest open PR)"
270
+ base_branch: "Target branch (default: main)"
271
+ vars:
272
+ default_base: main
273
+ nodes:
274
+ resolve-pr:
275
+ mode: shell
276
+ project: "{{input.project}}"
277
+ worktree: false
278
+ prompt: |
279
+ INPUT_BRANCH="{{input.branch}}" && \\
280
+ BASE="{{input.base_branch}}" && \\
281
+ if [ -z "$BASE" ] || echo "$BASE" | grep -q '{{'; then BASE="main"; fi && \\
282
+ if [ -z "$INPUT_BRANCH" ] || echo "$INPUT_BRANCH" | grep -q '{{'; then \\
283
+ INPUT_BRANCH=$(gh pr list --state open --base "$BASE" --json number -q '.[0].number' 2>/dev/null); \\
284
+ if [ -z "$INPUT_BRANCH" ]; then echo "ERROR: No open PR found targeting $BASE" && exit 1; fi; \\
285
+ fi && \\
286
+ if echo "$INPUT_BRANCH" | grep -qE '^[0-9]+$'; then \\
287
+ PR_NUM="$INPUT_BRANCH"; \\
288
+ else \\
289
+ PR_NUM=$(gh pr list --state open --head "$INPUT_BRANCH" --json number -q '.[0].number' 2>/dev/null); \\
290
+ if [ -z "$PR_NUM" ]; then echo "ERROR: No open PR for branch $INPUT_BRANCH" && exit 1; fi; \\
291
+ fi && \\
292
+ echo "$PR_NUM"
293
+ outputs:
294
+ - name: pr_number
295
+ extract: stdout
296
+ fetch-diff:
297
+ mode: shell
298
+ project: "{{input.project}}"
299
+ worktree: false
300
+ depends_on: [resolve-pr]
301
+ prompt: "gh pr diff {{nodes.resolve-pr.outputs.pr_number}}"
302
+ outputs:
303
+ - name: diff
304
+ extract: stdout
305
+ fetch-files:
306
+ mode: shell
307
+ project: "{{input.project}}"
308
+ worktree: false
309
+ depends_on: [resolve-pr]
310
+ prompt: |
311
+ PR_NUM="{{nodes.resolve-pr.outputs.pr_number}}" && \\
312
+ echo "=== PR #$PR_NUM ===" && \\
313
+ gh pr view "$PR_NUM" --json title,author,additions,deletions,changedFiles,commits,body --jq '"Title: " + .title + "\\nAuthor: " + .author.login + "\\nFiles: " + (.changedFiles|tostring) + " changed, +" + (.additions|tostring) + "/-" + (.deletions|tostring) + "\\nCommits: " + (.commits|length|tostring) + "\\n\\n=== PR Description ===\\n" + (.body // "(no description)")' && \\
314
+ echo "" && \\
315
+ echo "=== Changed Files ===" && \\
316
+ gh pr diff "$PR_NUM" --name-only
317
+ outputs:
318
+ - name: stats
319
+ extract: stdout
320
+ review:
321
+ project: "{{input.project}}"
322
+ worktree: false
323
+ depends_on: [fetch-diff, fetch-files, resolve-pr]
324
+ prompt: |
325
+ You are a senior code reviewer. Perform a thorough code review of this PR.
326
+
327
+ ## PR Info & Description
328
+ {{nodes.fetch-files.outputs.stats}}
329
+
330
+ ## Diff
331
+ {{nodes.fetch-diff.outputs.diff}}
332
+
333
+ ## Review Requirements
334
+
335
+ **First**: Verify the PR description against actual changes:
336
+ - Is every claimed change actually implemented?
337
+ - Any claimed changes that are NOT in the diff?
338
+ - Any changes in the diff NOT mentioned in the description?
339
+
340
+ **Then**: Review code quality:
341
+ 1. Bug risk — logic errors, edge cases, null references
342
+ 2. Security — injection, hardcoded secrets, sensitive data exposure
343
+ 3. Performance — obvious bottlenecks
344
+ 4. Code quality — readability, naming, DRY
345
+
346
+ ## Output
347
+
348
+ Write the full review report to /tmp/forge-review-pr{{nodes.resolve-pr.outputs.pr_number}}.md in this format:
349
+
350
+ ## 🤖 Forge AI Code Review — PR #{{nodes.resolve-pr.outputs.pr_number}}
351
+
352
+ ### 📋 Summary
353
+ - Verdict: ✅ Approve / ⚠️ Request Changes / ❌ Reject
354
+ - One-line summary
355
+
356
+ ### ✅ PR Description Verification
357
+ List each change claimed in the PR description, mark ✓ implemented / ✗ not implemented / ⚠️ partial
358
+
359
+ ### 🔴 Blockers (must fix)
360
+ (write "None" if none)
361
+
362
+ ### 🟡 Suggestions
363
+
364
+ ### 🟢 Nice-to-have
365
+
366
+ ### 💡 Highlights
367
+
368
+ ---
369
+ _Generated by [Forge](https://github.com/aiwatching/forge) Pipeline_
370
+
371
+ **You MUST write the complete report to /tmp/forge-review-pr{{nodes.resolve-pr.outputs.pr_number}}.md. This is the most important step.**
372
+ outputs:
373
+ - name: report
374
+ extract: result
375
+ post-comment:
376
+ mode: shell
377
+ project: "{{input.project}}"
378
+ worktree: false
379
+ depends_on: [review, resolve-pr]
380
+ prompt: |
381
+ PR_NUM="{{nodes.resolve-pr.outputs.pr_number}}" && \\
382
+ REPORT="/tmp/forge-review-pr\${PR_NUM}.md" && \\
383
+ if [ ! -f "$REPORT" ]; then echo "ERROR: Review report not found at $REPORT" && exit 1; fi && \\
384
+ gh pr comment "$PR_NUM" --body-file "$REPORT" && \\
385
+ rm -f "$REPORT" && \\
386
+ echo "Comment posted to PR #$PR_NUM"
387
+ outputs:
388
+ - name: result
389
+ extract: stdout
390
+ `,
391
+ };
392
+
393
+ export interface WorkflowWithMeta extends Workflow {
394
+ builtin?: boolean;
395
+ }
396
+
397
+ export function listWorkflows(): WorkflowWithMeta[] {
398
+ // User workflows
399
+ const userWorkflows: WorkflowWithMeta[] = [];
400
+ if (existsSync(WORKFLOWS_DIR)) {
401
+ for (const f of readdirSync(WORKFLOWS_DIR).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
402
+ try {
403
+ userWorkflows.push({ ...parseWorkflow(readFileSync(join(WORKFLOWS_DIR, f), 'utf-8')), builtin: false });
404
+ } catch {}
405
+ }
406
+ }
407
+
408
+ // Built-in workflows (don't override user ones with same name)
409
+ const userNames = new Set(userWorkflows.map(w => w.name));
410
+ const builtins: WorkflowWithMeta[] = [];
411
+ for (const [, yaml] of Object.entries(BUILTIN_WORKFLOWS)) {
412
+ try {
413
+ const w = parseWorkflow(yaml);
414
+ if (!userNames.has(w.name)) {
415
+ builtins.push({ ...w, builtin: true });
416
+ }
417
+ } catch {}
418
+ }
419
+
420
+ return [...builtins, ...userWorkflows];
421
+ }
422
+
423
+ export function getWorkflow(name: string): WorkflowWithMeta | null {
424
+ return listWorkflows().find(w => w.name === name) || null;
425
+ }
426
+
427
+ function parseWorkflow(raw: string): Workflow {
428
+ const parsed = YAML.parse(raw);
429
+ const workflowType = parsed.type || 'dag';
430
+ const nodes: Record<string, WorkflowNode> = {};
431
+
432
+ for (const [id, def] of Object.entries(parsed.nodes || {})) {
433
+ const n = def as any;
434
+ nodes[id] = {
435
+ id,
436
+ project: n.project || '',
437
+ prompt: n.prompt || '',
438
+ mode: n.mode || (n.plugin ? 'plugin' : 'claude'),
439
+ agent: n.agent || undefined,
440
+ branch: n.branch || undefined,
441
+ worktree: n.worktree !== undefined ? n.worktree : undefined,
442
+ plugin: n.plugin || undefined,
443
+ pluginAction: n.plugin_action || n.pluginAction || undefined,
444
+ pluginParams: n.plugin_params || n.pluginParams || n.params || undefined,
445
+ pluginWait: n.plugin_wait || n.pluginWait || n.wait || false,
446
+ dependsOn: n.depends_on || n.dependsOn || [],
447
+ outputs: (n.outputs || []).map((o: any) => ({
448
+ name: o.name,
449
+ extract: o.extract || 'result',
450
+ })),
451
+ routes: (n.routes || []).map((r: any) => ({
452
+ condition: r.condition || 'default',
453
+ next: r.next,
454
+ })),
455
+ maxIterations: n.max_iterations || n.maxIterations || 3,
456
+ };
457
+ }
458
+
459
+ // Parse conversation config
460
+ let conversation: ConversationConfig | undefined;
461
+ if (workflowType === 'conversation' && parsed.agents) {
462
+ conversation = {
463
+ agents: (parsed.agents as any[]).map((a: any) => ({
464
+ id: a.id,
465
+ agent: a.agent || 'claude',
466
+ role: a.role || '',
467
+ project: a.project || undefined,
468
+ })),
469
+ maxRounds: parsed.max_rounds || parsed.maxRounds || 10,
470
+ stopCondition: parsed.stop_condition || parsed.stopCondition || undefined,
471
+ initialPrompt: parsed.initial_prompt || parsed.initialPrompt || '',
472
+ contextStrategy: parsed.context_strategy || parsed.contextStrategy || 'summary',
473
+ contextWindow: parsed.context_window || parsed.contextWindow || 4,
474
+ maxContentLength: parsed.max_content_length || parsed.maxContentLength || 3000,
475
+ };
476
+ }
477
+
478
+ return {
479
+ name: parsed.name || 'unnamed',
480
+ type: workflowType,
481
+ description: parsed.description,
482
+ vars: parsed.vars || {},
483
+ input: parsed.input || {},
484
+ nodes,
485
+ conversation,
486
+ };
487
+ }
488
+
489
+ // ─── Pipeline Persistence ─────────────────────────────────
490
+
491
+ function ensureDir() {
492
+ if (!existsSync(PIPELINES_DIR)) mkdirSync(PIPELINES_DIR, { recursive: true });
493
+ }
494
+
495
+ function savePipeline(pipeline: Pipeline) {
496
+ ensureDir();
497
+ writeFileSync(join(PIPELINES_DIR, `${pipeline.id}.json`), JSON.stringify(pipeline, null, 2));
498
+ }
499
+
500
+ export function getPipeline(id: string): Pipeline | null {
501
+ try {
502
+ return JSON.parse(readFileSync(join(PIPELINES_DIR, `${id}.json`), 'utf-8'));
503
+ } catch {
504
+ return null;
505
+ }
506
+ }
507
+
508
+ export function deletePipeline(id: string): boolean {
509
+ const filePath = join(PIPELINES_DIR, `${id}.json`);
510
+ try {
511
+ if (existsSync(filePath)) {
512
+ const { unlinkSync } = require('node:fs');
513
+ unlinkSync(filePath);
514
+ return true;
515
+ }
516
+ } catch {}
517
+ return false;
518
+ }
519
+
520
+ export function listPipelines(): Pipeline[] {
521
+ ensureDir();
522
+ return readdirSync(PIPELINES_DIR)
523
+ .filter(f => f.endsWith('.json'))
524
+ .map(f => {
525
+ try {
526
+ return JSON.parse(readFileSync(join(PIPELINES_DIR, f), 'utf-8')) as Pipeline;
527
+ } catch {
528
+ return null;
529
+ }
530
+ })
531
+ .filter(Boolean) as Pipeline[];
532
+ }
533
+
534
+ // ─── Template Resolution ──────────────────────────────────
535
+
536
+ /** Escape a string for safe embedding in single-quoted shell strings */
537
+ function shellEscape(s: string): string {
538
+ // Replace single quotes with '\'' (end quote, escaped quote, start quote)
539
+ return s.replace(/'/g, "'\\''");
540
+ }
541
+
542
+ /** Escape a string for safe embedding in $'...' shell strings (ANSI-C quoting) */
543
+ function shellEscapeAnsiC(s: string): string {
544
+ return s
545
+ .replace(/\\/g, '\\\\')
546
+ .replace(/'/g, "\\'")
547
+ .replace(/\n/g, '\\n')
548
+ .replace(/\r/g, '\\r')
549
+ .replace(/\t/g, '\\t');
550
+ }
551
+
552
+ function resolveTemplate(template: string, ctx: {
553
+ input: Record<string, string>;
554
+ vars: Record<string, string>;
555
+ nodes: Record<string, PipelineNodeState>;
556
+ }, shellMode?: boolean): string {
557
+ return template.replace(/\{\{(.*?)\}\}/g, (_, expr) => {
558
+ const path = expr.trim();
559
+ let value = '';
560
+
561
+ // {{input.xxx}}
562
+ if (path.startsWith('input.')) value = ctx.input[path.slice(6)] || '';
563
+ // {{vars.xxx}}
564
+ else if (path.startsWith('vars.')) value = ctx.vars[path.slice(5)] || '';
565
+ // {{nodes.xxx.outputs.yyy}}
566
+ else {
567
+ const nodeMatch = path.match(/^nodes\.([\w-]+)\.outputs\.([\w-]+)$/);
568
+ if (nodeMatch) {
569
+ const [, nodeId, outputName] = nodeMatch;
570
+ value = ctx.nodes[nodeId]?.outputs[outputName] || '';
571
+ } else {
572
+ return `{{${path}}}`;
573
+ }
574
+ }
575
+
576
+ return shellMode ? shellEscapeAnsiC(value) : value;
577
+ });
578
+ }
579
+
580
+ // ─── Project-level pipeline lock ─────────────────────────
581
+ const projectPipelineLocks = new Map<string, string>(); // projectPath → pipelineId
582
+
583
+ function acquireProjectLock(projectPath: string, pipelineId: string): boolean {
584
+ const existing = projectPipelineLocks.get(projectPath);
585
+ if (existing && existing !== pipelineId) {
586
+ // Check if the existing pipeline is still running
587
+ const p = getPipeline(existing);
588
+ if (p && p.status === 'running') return false;
589
+ // Stale lock, clear it
590
+ }
591
+ projectPipelineLocks.set(projectPath, pipelineId);
592
+ return true;
593
+ }
594
+
595
+ function releaseProjectLock(projectPath: string, pipelineId: string) {
596
+ if (projectPipelineLocks.get(projectPath) === pipelineId) {
597
+ projectPipelineLocks.delete(projectPath);
598
+ }
599
+ }
600
+
601
+ // ─── Pipeline Execution ───────────────────────────────────
602
+
603
+ export function startPipeline(workflowName: string, input: Record<string, string>): Pipeline {
604
+ const workflow = getWorkflow(workflowName);
605
+ if (!workflow) throw new Error(`Workflow not found: ${workflowName}`);
606
+
607
+ // Conversation mode — separate execution path
608
+ if (workflow.type === 'conversation' && workflow.conversation) {
609
+ return startConversationPipeline(workflow, input);
610
+ }
611
+
612
+ const id = randomUUID().slice(0, 8);
613
+ const nodes: Record<string, PipelineNodeState> = {};
614
+ const nodeOrder = topologicalSort(workflow.nodes);
615
+
616
+ for (const nodeId of nodeOrder) {
617
+ nodes[nodeId] = {
618
+ status: 'pending',
619
+ outputs: {},
620
+ iterations: 0,
621
+ };
622
+ }
623
+
624
+ const pipeline: Pipeline = {
625
+ id,
626
+ workflowName,
627
+ status: 'running',
628
+ input,
629
+ vars: { ...workflow.vars },
630
+ nodes,
631
+ nodeOrder,
632
+ createdAt: new Date().toISOString(),
633
+ };
634
+
635
+ savePipeline(pipeline);
636
+
637
+ // Start nodes that have no dependencies
638
+ scheduleReadyNodes(pipeline, workflow);
639
+
640
+ // Listen for task completions
641
+ setupTaskListener(pipeline.id);
642
+
643
+ return pipeline;
644
+ }
645
+
646
+ // ─── Conversation State Type (extracted to avoid Turbopack parse issues) ──
647
+ type ConversationState = {
648
+ config: ConversationConfig;
649
+ messages: ConversationMessage[];
650
+ currentRound: number;
651
+ currentAgentIndex: number;
652
+ };
653
+
654
+ // ─── Conversation Mode Execution ──────────────────────────
655
+
656
+ function startConversationPipeline(workflow: Workflow, input: Record<string, string>): Pipeline {
657
+ const conv = workflow.conversation!;
658
+ const id = randomUUID().slice(0, 8);
659
+
660
+ // Resolve agent display names
661
+ const agentNames: Record<string, string> = {};
662
+ const allAgents = listAgents();
663
+ for (const ca of conv.agents) {
664
+ const found = allAgents.find(a => a.id === ca.agent);
665
+ agentNames[ca.id] = found?.name || ca.agent;
666
+ }
667
+
668
+ const pipeline: Pipeline = {
669
+ id,
670
+ workflowName: workflow.name,
671
+ type: 'conversation',
672
+ status: 'running',
673
+ input,
674
+ vars: { ...workflow.vars },
675
+ nodes: {},
676
+ nodeOrder: [],
677
+ createdAt: new Date().toISOString(),
678
+ conversation: {
679
+ config: {
680
+ ...conv,
681
+ // Store resolved initial prompt so buildConversationContext uses it
682
+ initialPrompt: conv.initialPrompt.replace(/\{\{input\.(\w+)\}\}/g, (_, key) => input[key] || ''),
683
+ },
684
+ messages: [],
685
+ currentRound: 1,
686
+ currentAgentIndex: 0,
687
+ },
688
+ };
689
+
690
+ savePipeline(pipeline);
691
+
692
+ const resolvedPrompt = pipeline.conversation!.config.initialPrompt;
693
+
694
+ // Start the first round
695
+ scheduleNextConversationTurn(pipeline, resolvedPrompt, agentNames);
696
+
697
+ return pipeline;
698
+ }
699
+
700
+ function scheduleNextConversationTurn(pipeline: Pipeline, contextForAgent: string, agentNames?: Record<string, string>) {
701
+ const conv = pipeline.conversation!;
702
+ const config = conv.config;
703
+ const agentDef = config.agents[conv.currentAgentIndex];
704
+
705
+ if (!agentDef) {
706
+ pipeline.status = 'failed';
707
+ pipeline.completedAt = new Date().toISOString();
708
+ savePipeline(pipeline);
709
+ return;
710
+ }
711
+
712
+ // Resolve project
713
+ const projectName = agentDef.project || pipeline.input.project || '';
714
+ const projectInfo = getProjectInfo(projectName);
715
+ if (!projectInfo) {
716
+ pipeline.status = 'failed';
717
+ pipeline.completedAt = new Date().toISOString();
718
+ savePipeline(pipeline);
719
+ notifyPipelineComplete(pipeline);
720
+ return;
721
+ }
722
+
723
+ // Build the prompt: role context + conversation history + new message
724
+ const rolePrefix = agentDef.role ? `[Your role: ${agentDef.role}]\n\n` : '';
725
+ const fullPrompt = `${rolePrefix}${contextForAgent}`;
726
+
727
+ // Create a task for this agent's turn
728
+ const task = createTask({
729
+ projectName: projectInfo.name,
730
+ projectPath: projectInfo.path,
731
+ prompt: fullPrompt,
732
+ mode: 'prompt',
733
+ agent: agentDef.agent,
734
+ conversationId: '', // fresh session — no resume for conversation mode
735
+ });
736
+ pipelineTaskIds.add(task.id);
737
+
738
+ // Add pending message
739
+ const names = agentNames || resolveAgentNames(config.agents);
740
+ conv.messages.push({
741
+ round: conv.currentRound,
742
+ agentId: agentDef.id,
743
+ agentName: names[agentDef.id] || agentDef.agent,
744
+ content: '',
745
+ timestamp: new Date().toISOString(),
746
+ taskId: task.id,
747
+ status: 'running',
748
+ });
749
+
750
+ savePipeline(pipeline);
751
+
752
+ // Listen for this task to complete
753
+ setupConversationTaskListener(pipeline.id, task.id);
754
+ }
755
+
756
+ function resolveAgentNames(agents: ConversationAgent[]): Record<string, string> {
757
+ const allAgents = listAgents();
758
+ const names: Record<string, string> = {};
759
+ for (const ca of agents) {
760
+ const found = allAgents.find(a => a.id === ca.agent);
761
+ names[ca.id] = found?.name || ca.agent;
762
+ }
763
+ return names;
764
+ }
765
+
766
+ function setupConversationTaskListener(pipelineId: string, taskId: string) {
767
+ const cleanup = onTaskEvent((evtTaskId, event, data) => {
768
+ if (evtTaskId !== taskId) return;
769
+ if (event !== 'status') return;
770
+ if (data !== 'done' && data !== 'failed') return;
771
+
772
+ cleanup(); // one-shot listener
773
+
774
+ const pipeline = getPipeline(pipelineId);
775
+ if (!pipeline || pipeline.status !== 'running' || !pipeline.conversation) return;
776
+
777
+ const conv = pipeline.conversation;
778
+ const config = conv.config;
779
+ const msgIndex = conv.messages.findIndex(m => m.taskId === taskId);
780
+ if (msgIndex < 0) return;
781
+
782
+ const task = getTask(taskId);
783
+
784
+ if (data === 'failed' || !task) {
785
+ conv.messages[msgIndex].status = 'failed';
786
+ conv.messages[msgIndex].content = task?.error || 'Task failed';
787
+ pipeline.status = 'failed';
788
+ pipeline.completedAt = new Date().toISOString();
789
+ savePipeline(pipeline);
790
+ notifyPipelineComplete(pipeline);
791
+ return;
792
+ }
793
+
794
+ // Task completed — extract response
795
+ const response = task.resultSummary || '';
796
+ conv.messages[msgIndex].status = 'done';
797
+ conv.messages[msgIndex].content = response;
798
+
799
+ // Check stop condition
800
+ if (checkConversationStopCondition(conv, response)) {
801
+ finishConversation(pipeline, 'done');
802
+ return;
803
+ }
804
+
805
+ // Move to next agent in round, or next round
806
+ conv.currentAgentIndex++;
807
+ if (conv.currentAgentIndex >= config.agents.length) {
808
+ // Completed a full round
809
+ conv.currentAgentIndex = 0;
810
+ conv.currentRound++;
811
+
812
+ if (conv.currentRound > config.maxRounds) {
813
+ finishConversation(pipeline, 'done');
814
+ return;
815
+ }
816
+ }
817
+
818
+ savePipeline(pipeline);
819
+
820
+ // Build context for next agent: accumulate conversation history
821
+ const contextForNext = buildConversationContext(conv);
822
+ scheduleNextConversationTurn(pipeline, contextForNext);
823
+ });
824
+ }
825
+
826
+ /**
827
+ * Build context string for the next agent in conversation.
828
+ *
829
+ * Three strategies:
830
+ * - 'full' : pass ALL history as-is (token-heavy, good for short convos)
831
+ * - 'window' : pass only the last N messages in full, drop older ones
832
+ * - 'summary' : pass older messages as one-line summaries + last N in full (default)
833
+ */
834
+ function buildConversationContext(conv: ConversationState): string {
835
+ const config = conv.config;
836
+ const strategy = config.contextStrategy || 'summary';
837
+ const windowSize = config.contextWindow || 4;
838
+ const maxLen = config.maxContentLength || 3000;
839
+
840
+ const doneMessages = conv.messages.filter(m => m.status === 'done' && m.content);
841
+
842
+ let context = `[Conversation — Round ${conv.currentRound}]\n\n`;
843
+ context += `Task: ${config.initialPrompt}\n\n`;
844
+
845
+ if (doneMessages.length === 0) {
846
+ context += `--- Your Turn ---\nYou are the first to respond. Please address the task above. If you believe the task is complete, include "DONE" in your response.`;
847
+ return context;
848
+ }
849
+
850
+ context += `--- Conversation History ---\n\n`;
851
+
852
+ if (strategy === 'full') {
853
+ // Full: all messages, truncated per maxLen
854
+ for (const msg of doneMessages) {
855
+ context += formatMessage(msg, config, maxLen);
856
+ }
857
+ } else if (strategy === 'window') {
858
+ // Window: only last N messages
859
+ const recent = doneMessages.slice(-windowSize);
860
+ if (doneMessages.length > windowSize) {
861
+ context += `[... ${doneMessages.length - windowSize} earlier messages omitted ...]\n\n`;
862
+ }
863
+ for (const msg of recent) {
864
+ context += formatMessage(msg, config, maxLen);
865
+ }
866
+ } else {
867
+ // Summary (default): older messages as one-line summaries, recent in full
868
+ const cutoff = doneMessages.length - windowSize;
869
+ if (cutoff > 0) {
870
+ context += `[Previous rounds summary]\n`;
871
+ for (let i = 0; i < cutoff; i++) {
872
+ const msg = doneMessages[i];
873
+ const summary = extractSummaryLine(msg.content);
874
+ context += ` R${msg.round} ${msg.agentName}: ${summary}\n`;
875
+ }
876
+ context += `\n`;
877
+ }
878
+ // Recent messages in full
879
+ const recent = doneMessages.slice(Math.max(0, cutoff));
880
+ for (const msg of recent) {
881
+ context += formatMessage(msg, config, maxLen);
882
+ }
883
+ }
884
+
885
+ context += `--- Your Turn ---\nRespond based on the conversation above. If you believe the task is complete, include "DONE" in your response.`;
886
+ return context;
887
+ }
888
+
889
+ function formatMessage(msg: ConversationMessage, config: ConversationConfig, maxLen: number): string {
890
+ const agentDef = config.agents.find(a => a.id === msg.agentId);
891
+ const label = `${msg.agentName} (${agentDef?.id || '?'})`;
892
+ const content = msg.content.length > maxLen
893
+ ? msg.content.slice(0, maxLen) + '\n[... truncated]'
894
+ : msg.content;
895
+ return `[${label} — Round ${msg.round}]:\n${content}\n\n`;
896
+ }
897
+
898
+ /** Extract a one-line summary from agent output (first meaningful line or first 120 chars) */
899
+ function extractSummaryLine(content: string): string {
900
+ const lines = content.split('\n').map(l => l.trim()).filter(l => l.length > 10);
901
+ const first = lines[0] || content.slice(0, 120);
902
+ return first.length > 120 ? first.slice(0, 117) + '...' : first;
903
+ }
904
+
905
+ function checkConversationStopCondition(conv: ConversationState, latestResponse: string): boolean {
906
+ const condition = conv.config.stopCondition;
907
+ if (!condition) return false;
908
+
909
+ const lower = condition.toLowerCase();
910
+
911
+ // "any agent says DONE"
912
+ if (lower.includes('any') && lower.includes('done')) {
913
+ return latestResponse.toUpperCase().includes('DONE');
914
+ }
915
+
916
+ // "all agents say DONE" / "both agents say DONE"
917
+ if ((lower.includes('all') || lower.includes('both')) && lower.includes('done')) {
918
+ // Only check messages from the CURRENT round — don't mix rounds
919
+ const currentRound = conv.currentRound;
920
+ const agentIds = conv.config.agents.map(a => a.id);
921
+ const roundMessages = new Map<string, string>();
922
+ for (const msg of conv.messages) {
923
+ if (msg.status === 'done' && msg.round === currentRound && msg.agentId !== 'user') {
924
+ roundMessages.set(msg.agentId, msg.content);
925
+ }
926
+ }
927
+ // All agents in this round must have responded AND said DONE
928
+ return agentIds.every(id => {
929
+ const content = roundMessages.get(id);
930
+ return content && content.toUpperCase().includes('DONE');
931
+ });
932
+ }
933
+
934
+ // Default: check if latest response contains DONE
935
+ return latestResponse.toUpperCase().includes('DONE');
936
+ }
937
+
938
+ /** Cleanly finish a conversation — cancel any still-running tasks, mark messages */
939
+ function finishConversation(pipeline: Pipeline, status: 'done' | 'failed') {
940
+ const conv = pipeline.conversation!;
941
+ for (const msg of conv.messages) {
942
+ if (msg.status === 'running' && msg.taskId) {
943
+ // Cancel the running task
944
+ try { const { cancelTask } = require('./task-manager'); cancelTask(msg.taskId); } catch {}
945
+ msg.status = status === 'done' ? 'done' : 'failed';
946
+ if (!msg.content) msg.content = status === 'done' ? '(conversation ended)' : '(conversation failed)';
947
+ }
948
+ if (msg.status === 'pending') {
949
+ msg.status = 'failed';
950
+ }
951
+ }
952
+ pipeline.status = status;
953
+ pipeline.completedAt = new Date().toISOString();
954
+ savePipeline(pipeline);
955
+ notifyPipelineComplete(pipeline);
956
+ }
957
+
958
+ /** Cancel a conversation pipeline */
959
+ export function cancelConversation(pipelineId: string): boolean {
960
+ const pipeline = getPipeline(pipelineId);
961
+ if (!pipeline || pipeline.status !== 'running' || !pipeline.conversation) return false;
962
+
963
+ // Cancel any running task
964
+ for (const msg of pipeline.conversation.messages) {
965
+ if (msg.status === 'running' && msg.taskId) {
966
+ const { cancelTask } = require('./task-manager');
967
+ cancelTask(msg.taskId);
968
+ }
969
+ if (msg.status === 'pending') msg.status = 'failed';
970
+ }
971
+
972
+ pipeline.status = 'cancelled';
973
+ pipeline.completedAt = new Date().toISOString();
974
+ savePipeline(pipeline);
975
+ return true;
976
+ }
977
+
978
+ /**
979
+ * Inject a user message into a running conversation.
980
+ * Waits for current agent to finish, then sends the injected message
981
+ * as additional context to the specified agent on the next turn.
982
+ */
983
+ export function injectConversationMessage(pipelineId: string, targetAgentId: string, message: string): boolean {
984
+ const pipeline = getPipeline(pipelineId);
985
+ if (!pipeline || pipeline.status !== 'running' || !pipeline.conversation) {
986
+ throw new Error('Pipeline not running or not a conversation');
987
+ }
988
+
989
+ const conv = pipeline.conversation;
990
+ const agentDef = conv.config.agents.find(a => a.id === targetAgentId);
991
+ if (!agentDef) throw new Error(`Agent not found: ${targetAgentId}`);
992
+
993
+ // Add a "user" message to the conversation
994
+ conv.messages.push({
995
+ round: conv.currentRound,
996
+ agentId: 'user',
997
+ agentName: 'Operator',
998
+ content: `[@${targetAgentId}] ${message}`,
999
+ timestamp: new Date().toISOString(),
1000
+ status: 'done',
1001
+ });
1002
+
1003
+ savePipeline(pipeline);
1004
+
1005
+ // If no agent is currently running, immediately schedule the target agent
1006
+ const hasRunning = conv.messages.some(m => m.status === 'running');
1007
+ if (!hasRunning) {
1008
+ // Point to the target agent for next turn
1009
+ const targetIdx = conv.config.agents.findIndex(a => a.id === targetAgentId);
1010
+ if (targetIdx >= 0) {
1011
+ conv.currentAgentIndex = targetIdx;
1012
+ savePipeline(pipeline);
1013
+ const context = buildConversationContext(conv);
1014
+ scheduleNextConversationTurn(pipeline, context);
1015
+ }
1016
+ }
1017
+ // If an agent IS running, the injected message will be included in the next context build
1018
+
1019
+ return true;
1020
+ }
1021
+
1022
+ // ─── Conversation Recovery ────────────────────────────────
1023
+
1024
+ function recoverConversationPipeline(pipeline: Pipeline) {
1025
+ const conv = pipeline.conversation!;
1026
+ const runningMsg = conv.messages.find(m => m.status === 'running');
1027
+ if (!runningMsg || !runningMsg.taskId) return;
1028
+
1029
+ const task = getTask(runningMsg.taskId);
1030
+ if (!task) {
1031
+ // Task gone — mark as done with empty content, try next turn
1032
+ runningMsg.status = 'done';
1033
+ runningMsg.content = '(no response — task was cleaned up)';
1034
+ savePipeline(pipeline);
1035
+ advanceConversation(pipeline);
1036
+ return;
1037
+ }
1038
+ if (task.status === 'done') {
1039
+ runningMsg.status = 'done';
1040
+ runningMsg.content = task.resultSummary || '';
1041
+ savePipeline(pipeline);
1042
+ advanceConversation(pipeline);
1043
+ } else if (task.status === 'failed' || task.status === 'cancelled') {
1044
+ runningMsg.status = 'failed';
1045
+ runningMsg.content = task.error || 'Task failed';
1046
+ pipeline.status = 'failed';
1047
+ pipeline.completedAt = new Date().toISOString();
1048
+ savePipeline(pipeline);
1049
+ } else {
1050
+ // Still running — re-attach listener
1051
+ setupConversationTaskListener(pipeline.id, runningMsg.taskId);
1052
+ }
1053
+ }
1054
+
1055
+ function advanceConversation(pipeline: Pipeline) {
1056
+ const conv = pipeline.conversation!;
1057
+ const config = conv.config;
1058
+ const lastDoneMsg = [...conv.messages].reverse().find(m => m.status === 'done');
1059
+
1060
+ if (lastDoneMsg && checkConversationStopCondition(conv, lastDoneMsg.content)) {
1061
+ finishConversation(pipeline, 'done');
1062
+ return;
1063
+ }
1064
+
1065
+ conv.currentAgentIndex++;
1066
+ if (conv.currentAgentIndex >= config.agents.length) {
1067
+ conv.currentAgentIndex = 0;
1068
+ conv.currentRound++;
1069
+ if (conv.currentRound > config.maxRounds) {
1070
+ finishConversation(pipeline, 'done');
1071
+ return;
1072
+ }
1073
+ }
1074
+
1075
+ savePipeline(pipeline);
1076
+ const contextForNext = buildConversationContext(conv);
1077
+ scheduleNextConversationTurn(pipeline, contextForNext);
1078
+ }
1079
+
1080
+ // ─── Recovery: check for stuck pipelines ──────────────────
1081
+
1082
+ function recoverStuckPipelines() {
1083
+ const pipelines = listPipelines().filter(p => p.status === 'running');
1084
+ for (const pipeline of pipelines) {
1085
+ // Conversation mode recovery
1086
+ if (pipeline.type === 'conversation' && pipeline.conversation) {
1087
+ recoverConversationPipeline(pipeline);
1088
+ continue;
1089
+ }
1090
+
1091
+ const workflow = getWorkflow(pipeline.workflowName);
1092
+ if (!workflow) continue;
1093
+
1094
+ let changed = false;
1095
+ for (const [nodeId, node] of Object.entries(pipeline.nodes)) {
1096
+ if (node.status === 'running' && node.taskId) {
1097
+ const task = getTask(node.taskId);
1098
+ if (!task) {
1099
+ // Task gone — mark node as done (task completed and was cleaned up)
1100
+ node.status = 'done';
1101
+ node.completedAt = new Date().toISOString();
1102
+ changed = true;
1103
+ } else if (task.status === 'done') {
1104
+ // Extract outputs
1105
+ const nodeDef = workflow.nodes[nodeId];
1106
+ if (nodeDef) {
1107
+ for (const outputDef of nodeDef.outputs) {
1108
+ if (outputDef.extract === 'result' || outputDef.extract === 'stdout') node.outputs[outputDef.name] = task.resultSummary || '';
1109
+ else if (outputDef.extract === 'git_diff') node.outputs[outputDef.name] = task.gitDiff || '';
1110
+ }
1111
+ }
1112
+ node.status = 'done';
1113
+ node.completedAt = new Date().toISOString();
1114
+ changed = true;
1115
+ } else if (task.status === 'failed' || task.status === 'cancelled') {
1116
+ node.status = 'failed';
1117
+ node.error = task.error || 'Task failed';
1118
+ node.completedAt = new Date().toISOString();
1119
+ changed = true;
1120
+ }
1121
+ }
1122
+ }
1123
+
1124
+ if (changed) {
1125
+ savePipeline(pipeline);
1126
+ // Re-setup listener and schedule next nodes
1127
+ setupTaskListener(pipeline.id);
1128
+ scheduleReadyNodes(pipeline, workflow);
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ // Run recovery every 30 seconds
1134
+ setInterval(recoverStuckPipelines, 30_000);
1135
+ // Also run once on load
1136
+ setTimeout(recoverStuckPipelines, 5000);
1137
+
1138
+ export function cancelPipeline(id: string): boolean {
1139
+ const pipeline = getPipeline(id);
1140
+ if (!pipeline || pipeline.status !== 'running') return false;
1141
+
1142
+ // Conversation mode
1143
+ if (pipeline.type === 'conversation') {
1144
+ return cancelConversation(id);
1145
+ }
1146
+
1147
+ pipeline.status = 'cancelled';
1148
+ pipeline.completedAt = new Date().toISOString();
1149
+
1150
+ // Cancel all running tasks
1151
+ for (const [, node] of Object.entries(pipeline.nodes)) {
1152
+ if (node.status === 'running' && node.taskId) {
1153
+ const { cancelTask } = require('./task-manager');
1154
+ cancelTask(node.taskId);
1155
+ }
1156
+ if (node.status === 'pending') node.status = 'skipped';
1157
+ }
1158
+
1159
+ savePipeline(pipeline);
1160
+ return true;
1161
+ }
1162
+
1163
+ // ─── Node Scheduling ──────────────────────────────────────
1164
+
1165
+ async function scheduleReadyNodes(pipeline: Pipeline, workflow: Workflow) {
1166
+ const ctx = { input: pipeline.input, vars: pipeline.vars, nodes: pipeline.nodes };
1167
+
1168
+ for (const nodeId of pipeline.nodeOrder) {
1169
+ const nodeState = pipeline.nodes[nodeId];
1170
+ if (nodeState.status !== 'pending') continue;
1171
+
1172
+ const nodeDef = workflow.nodes[nodeId];
1173
+ if (!nodeDef) continue;
1174
+
1175
+ // Check all dependencies are done
1176
+ const depsReady = nodeDef.dependsOn.every(dep => {
1177
+ const depState = pipeline.nodes[dep];
1178
+ return depState && depState.status === 'done';
1179
+ });
1180
+
1181
+ // Check if any dependency failed (skip this node)
1182
+ const depsFailed = nodeDef.dependsOn.some(dep => {
1183
+ const depState = pipeline.nodes[dep];
1184
+ return depState && (depState.status === 'failed' || depState.status === 'skipped');
1185
+ });
1186
+
1187
+ if (depsFailed) {
1188
+ nodeState.status = 'skipped';
1189
+ savePipeline(pipeline);
1190
+ continue;
1191
+ }
1192
+
1193
+ if (!depsReady) continue;
1194
+
1195
+ // Resolve templates
1196
+ const isShell = nodeDef.mode === 'shell';
1197
+ const project = resolveTemplate(nodeDef.project, ctx);
1198
+ const prompt = resolveTemplate(nodeDef.prompt, ctx, isShell);
1199
+
1200
+ const projectInfo = getProjectInfo(project);
1201
+ if (!projectInfo) {
1202
+ nodeState.status = 'failed';
1203
+ nodeState.error = `Project not found: ${project}`;
1204
+ savePipeline(pipeline);
1205
+ notifyStep(pipeline, nodeId, 'failed', nodeState.error);
1206
+ continue;
1207
+ }
1208
+
1209
+ // All pipeline steps use worktree for isolated execution.
1210
+ // Shell steps receive env vars: FORGE_WORKTREE, FORGE_WORKTREE_BRANCH, FORGE_PROJECT_ROOT.
1211
+ // Set worktree: false on a node to skip (e.g. read-only gh commands that don't need isolation).
1212
+ let effectivePath = projectInfo.path;
1213
+ const useWorktree = nodeDef.worktree !== false;
1214
+ const branchName = nodeDef.branch ? resolveTemplate(nodeDef.branch, ctx) : `pipeline/${pipeline.id.slice(0, 8)}`;
1215
+ if (useWorktree) try {
1216
+ const { execSync } = require('node:child_process');
1217
+ const worktreePath = `${projectInfo.path}/.forge/worktrees/${branchName.replace(/\//g, '-')}`;
1218
+ const { mkdirSync } = require('node:fs');
1219
+ mkdirSync(`${projectInfo.path}/.forge/worktrees`, { recursive: true });
1220
+
1221
+ // Create branch if needed
1222
+ try { execSync(`git branch ${branchName}`, { cwd: projectInfo.path, stdio: 'pipe' }); } catch {}
1223
+
1224
+ // Create or reuse worktree
1225
+ try {
1226
+ execSync(`git worktree add "${worktreePath}" ${branchName}`, { cwd: projectInfo.path, stdio: 'pipe' });
1227
+ console.log(`[pipeline] Created worktree: ${worktreePath} (branch: ${branchName})`);
1228
+ } catch {
1229
+ const { existsSync } = require('node:fs');
1230
+ if (existsSync(worktreePath)) {
1231
+ console.log(`[pipeline] Reusing worktree: ${worktreePath}`);
1232
+ } else {
1233
+ try { execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectInfo.path, stdio: 'pipe' }); } catch {}
1234
+ execSync(`git worktree add "${worktreePath}" ${branchName}`, { cwd: projectInfo.path, stdio: 'pipe' });
1235
+ console.log(`[pipeline] Recreated worktree: ${worktreePath}`);
1236
+ }
1237
+ }
1238
+
1239
+ effectivePath = worktreePath;
1240
+ (nodeState as any).worktreePath = worktreePath;
1241
+ (nodeState as any).worktreeBranch = branchName;
1242
+ } catch (e: any) {
1243
+ console.warn(`[pipeline] Worktree creation failed, falling back to project dir: ${e.message}`);
1244
+ }
1245
+
1246
+ // ── Plugin mode: execute plugin action directly ──
1247
+ if (nodeDef.mode === 'plugin' && nodeDef.plugin) {
1248
+ nodeState.status = 'running';
1249
+ nodeState.startedAt = new Date().toISOString();
1250
+ savePipeline(pipeline);
1251
+ notifyStep(pipeline, nodeId, 'running');
1252
+
1253
+ try {
1254
+ const { getInstalledPlugin } = await import('./plugins/registry');
1255
+ const { executePluginWithWait } = await import('./plugins/executor');
1256
+
1257
+ const inst = getInstalledPlugin(nodeDef.plugin);
1258
+ if (!inst) throw new Error(`Plugin "${nodeDef.plugin}" not installed`);
1259
+ if (!inst.enabled) throw new Error(`Plugin "${nodeDef.plugin}" is disabled`);
1260
+
1261
+ // Resolve template params
1262
+ const resolvedParams: Record<string, any> = {};
1263
+ for (const [k, v] of Object.entries(nodeDef.pluginParams || {})) {
1264
+ resolvedParams[k] = typeof v === 'string' ? resolveTemplate(v, ctx) : v;
1265
+ }
1266
+
1267
+ const actionName = nodeDef.pluginAction || inst.definition.defaultAction || Object.keys(inst.definition.actions)[0];
1268
+ const result = await executePluginWithWait(inst, actionName, resolvedParams, nodeDef.pluginWait);
1269
+
1270
+ if (result.ok) {
1271
+ nodeState.status = 'done';
1272
+ nodeState.completedAt = new Date().toISOString();
1273
+ // Store plugin outputs
1274
+ for (const [name, value] of Object.entries(result.output)) {
1275
+ nodeState.outputs[name] = typeof value === 'string' ? value : JSON.stringify(value);
1276
+ }
1277
+ savePipeline(pipeline);
1278
+ notifyStep(pipeline, nodeId, 'done');
1279
+ console.log(`[pipeline] Plugin ${nodeDef.plugin}.${actionName}: done (${result.duration}ms)`);
1280
+ } else {
1281
+ throw new Error(result.error || 'Plugin action failed');
1282
+ }
1283
+ } catch (err: any) {
1284
+ nodeState.status = 'failed';
1285
+ nodeState.error = err.message;
1286
+ nodeState.completedAt = new Date().toISOString();
1287
+ savePipeline(pipeline);
1288
+ notifyStep(pipeline, nodeId, 'failed', err.message);
1289
+ console.error(`[pipeline] Plugin ${nodeDef.plugin}: failed — ${err.message}`);
1290
+ }
1291
+ continue;
1292
+ }
1293
+
1294
+ // ── Shell/Agent mode: create task ──
1295
+ // Inject worktree info into shell prompt so git commands work correctly
1296
+ const taskMode = nodeDef.mode === 'shell' ? 'shell' : 'prompt';
1297
+ let effectivePrompt = prompt;
1298
+ if (taskMode === 'shell' && effectivePath !== projectInfo.path) {
1299
+ // Prepend env vars so shell scripts know they're in a worktree
1300
+ const envPrefix = `export FORGE_WORKTREE="${effectivePath}" FORGE_WORKTREE_BRANCH="${(nodeState as any).worktreeBranch || branchName}" FORGE_PROJECT_ROOT="${projectInfo.path}" && `;
1301
+ effectivePrompt = envPrefix + prompt;
1302
+ }
1303
+ const task = createTask({
1304
+ projectName: projectInfo.name,
1305
+ projectPath: effectivePath,
1306
+ prompt: effectivePrompt,
1307
+ mode: taskMode as any,
1308
+ agent: nodeDef.agent || undefined,
1309
+ });
1310
+ pipelineTaskIds.add(task.id);
1311
+ // Pipeline tasks use the same model selection as normal tasks:
1312
+ // agent model > global taskModel. No separate pipelineModel override.
1313
+
1314
+ nodeState.status = 'running';
1315
+ nodeState.taskId = task.id;
1316
+ nodeState.iterations++;
1317
+ nodeState.startedAt = new Date().toISOString();
1318
+ savePipeline(pipeline);
1319
+
1320
+ notifyStep(pipeline, nodeId, 'running');
1321
+ }
1322
+
1323
+ // Check if pipeline is complete
1324
+ checkPipelineCompletion(pipeline);
1325
+ }
1326
+
1327
+ function checkPipelineCompletion(pipeline: Pipeline) {
1328
+ const states = Object.values(pipeline.nodes);
1329
+ const allDone = states.every(s => s.status === 'done' || s.status === 'skipped' || s.status === 'failed');
1330
+
1331
+ if (allDone && pipeline.status === 'running') {
1332
+ const anyFailed = states.some(s => s.status === 'failed');
1333
+ pipeline.status = anyFailed ? 'failed' : 'done';
1334
+ pipeline.completedAt = new Date().toISOString();
1335
+ savePipeline(pipeline);
1336
+ notifyPipelineComplete(pipeline);
1337
+
1338
+ // Sync run status to project pipeline runs
1339
+ try {
1340
+ const { syncRunStatus } = require('./pipeline-scheduler');
1341
+ syncRunStatus(pipeline.id);
1342
+ } catch {}
1343
+
1344
+ // Log worktree info for user review
1345
+ for (const [nodeId, state] of Object.entries(pipeline.nodes)) {
1346
+ const wt = (state as any).worktreePath;
1347
+ const branch = (state as any).worktreeBranch;
1348
+ if (wt && branch) {
1349
+ console.log(`[pipeline] Worktree preserved: ${wt} (branch: ${branch}) — review changes, then: git worktree remove "${wt}"`);
1350
+ }
1351
+ }
1352
+
1353
+ // Release project lock
1354
+ const workflow = getWorkflow(pipeline.workflowName);
1355
+ if (workflow) {
1356
+ const projectNames = new Set(Object.values(workflow.nodes).map(n => n.project));
1357
+ for (const pName of projectNames) {
1358
+ const pInfo = getProjectInfo(resolveTemplate(pName, { input: pipeline.input, vars: pipeline.vars, nodes: pipeline.nodes }));
1359
+ if (pInfo) releaseProjectLock(pInfo.path, pipeline.id);
1360
+ }
1361
+ }
1362
+ }
1363
+ }
1364
+
1365
+ // ─── Task Event Listener ──────────────────────────────────
1366
+
1367
+ const activeListeners = new Set<string>();
1368
+
1369
+ function setupTaskListener(pipelineId: string) {
1370
+ if (activeListeners.has(pipelineId)) return;
1371
+ activeListeners.add(pipelineId);
1372
+
1373
+ const cleanup = onTaskEvent((taskId, event, data) => {
1374
+ if (event !== 'status') return;
1375
+ if (data !== 'done' && data !== 'failed') return;
1376
+
1377
+ const pipeline = getPipeline(pipelineId);
1378
+ if (!pipeline || pipeline.status !== 'running') {
1379
+ cleanup();
1380
+ activeListeners.delete(pipelineId);
1381
+ return;
1382
+ }
1383
+
1384
+ // Find the node for this task
1385
+ const nodeEntry = Object.entries(pipeline.nodes).find(([, n]) => n.taskId === taskId);
1386
+ if (!nodeEntry) return;
1387
+
1388
+ const [nodeId, nodeState] = nodeEntry;
1389
+ const workflow = getWorkflow(pipeline.workflowName);
1390
+ if (!workflow) return;
1391
+
1392
+ const nodeDef = workflow.nodes[nodeId];
1393
+ const task = getTask(taskId);
1394
+
1395
+ if (data === 'done' && task) {
1396
+ // Extract outputs
1397
+ for (const outputDef of nodeDef.outputs) {
1398
+ if (outputDef.extract === 'result') {
1399
+ nodeState.outputs[outputDef.name] = task.resultSummary || '';
1400
+ } else if (outputDef.extract === 'stdout') {
1401
+ nodeState.outputs[outputDef.name] = task.resultSummary || '';
1402
+ } else if (outputDef.extract === 'git_diff') {
1403
+ nodeState.outputs[outputDef.name] = task.gitDiff || '';
1404
+ }
1405
+ }
1406
+
1407
+ // Convention: if stdout contains __SKIP__, mark node as skipped (downstream nodes will also skip)
1408
+ const outputStr = task.resultSummary || '';
1409
+ if (outputStr.includes('__SKIP__')) {
1410
+ nodeState.status = 'skipped';
1411
+ nodeState.completedAt = new Date().toISOString();
1412
+ savePipeline(pipeline);
1413
+ scheduleReadyNodes(pipeline, workflow);
1414
+ checkPipelineCompletion(pipeline);
1415
+ return;
1416
+ }
1417
+
1418
+ // Check routes for conditional next step
1419
+ if (nodeDef.routes.length > 0) {
1420
+ const nextNode = evaluateRoutes(nodeDef.routes, nodeState.outputs, pipeline);
1421
+ if (nextNode && nextNode !== nodeId) {
1422
+ // Route to next node — mark this as done
1423
+ nodeState.status = 'done';
1424
+ nodeState.completedAt = new Date().toISOString();
1425
+ // Reset next node to pending so it gets scheduled
1426
+ if (pipeline.nodes[nextNode] && pipeline.nodes[nextNode].status !== 'done') {
1427
+ pipeline.nodes[nextNode].status = 'pending';
1428
+ }
1429
+ } else if (nextNode === nodeId) {
1430
+ // Loop back — check iteration limit
1431
+ if (nodeState.iterations < nodeDef.maxIterations) {
1432
+ nodeState.status = 'pending';
1433
+ nodeState.taskId = undefined;
1434
+ } else {
1435
+ nodeState.status = 'done';
1436
+ nodeState.completedAt = new Date().toISOString();
1437
+ }
1438
+ } else {
1439
+ nodeState.status = 'done';
1440
+ nodeState.completedAt = new Date().toISOString();
1441
+ }
1442
+ } else {
1443
+ nodeState.status = 'done';
1444
+ nodeState.completedAt = new Date().toISOString();
1445
+ }
1446
+
1447
+ savePipeline(pipeline);
1448
+ // No per-step done notification — only notify on start and failure
1449
+ } else if (data === 'failed') {
1450
+ nodeState.status = 'failed';
1451
+ nodeState.error = task?.error || 'Task failed';
1452
+ nodeState.completedAt = new Date().toISOString();
1453
+ savePipeline(pipeline);
1454
+ notifyStep(pipeline, nodeId, 'failed', nodeState.error);
1455
+ }
1456
+
1457
+ // Schedule next ready nodes
1458
+ scheduleReadyNodes(pipeline, workflow);
1459
+ });
1460
+ }
1461
+
1462
+ function evaluateRoutes(
1463
+ routes: { condition: string; next: string }[],
1464
+ outputs: Record<string, string>,
1465
+ pipeline: Pipeline
1466
+ ): string | null {
1467
+ for (const route of routes) {
1468
+ if (route.condition === 'default') return route.next;
1469
+
1470
+ // Simple "contains" check: {{outputs.xxx contains 'YYY'}}
1471
+ const containsMatch = route.condition.match(/\{\{outputs\.(\w+)\s+contains\s+'([^']+)'\}\}/);
1472
+ if (containsMatch) {
1473
+ const [, outputName, keyword] = containsMatch;
1474
+ if (outputs[outputName]?.includes(keyword)) return route.next;
1475
+ continue;
1476
+ }
1477
+
1478
+ // Default: treat as truthy check
1479
+ return route.next;
1480
+ }
1481
+ return null;
1482
+ }
1483
+
1484
+ // ─── Topological Sort ─────────────────────────────────────
1485
+
1486
+ function topologicalSort(nodes: Record<string, WorkflowNode>): string[] {
1487
+ const sorted: string[] = [];
1488
+ const visited = new Set<string>();
1489
+ const visiting = new Set<string>();
1490
+
1491
+ function visit(id: string) {
1492
+ if (visited.has(id)) return;
1493
+ if (visiting.has(id)) return; // cycle — skip
1494
+ visiting.add(id);
1495
+
1496
+ const node = nodes[id];
1497
+ if (node) {
1498
+ for (const dep of node.dependsOn) {
1499
+ visit(dep);
1500
+ }
1501
+ // Also add route targets
1502
+ for (const route of node.routes) {
1503
+ if (nodes[route.next] && !node.dependsOn.includes(route.next)) {
1504
+ // Don't visit route targets in topo sort to avoid cycles
1505
+ }
1506
+ }
1507
+ }
1508
+
1509
+ visiting.delete(id);
1510
+ visited.add(id);
1511
+ sorted.push(id);
1512
+ }
1513
+
1514
+ for (const id of Object.keys(nodes)) {
1515
+ visit(id);
1516
+ }
1517
+
1518
+ return sorted;
1519
+ }
1520
+
1521
+ // ─── Notifications ────────────────────────────────────────
1522
+
1523
+ async function notifyStep(pipeline: Pipeline, nodeId: string, status: string, error?: string) {
1524
+ const settings = loadSettings();
1525
+ if (!settings.telegramBotToken || !settings.telegramChatId) return;
1526
+
1527
+ const icon = status === 'done' ? '✅' : status === 'failed' ? '❌' : status === 'running' ? '🔄' : '⏳';
1528
+ const msg = `${icon} Pipeline ${pipeline.id}/${nodeId}: ${status}${error ? `\n${error}` : ''}`;
1529
+
1530
+ try {
1531
+ await fetch(`https://api.telegram.org/bot${settings.telegramBotToken}/sendMessage`, {
1532
+ method: 'POST',
1533
+ headers: { 'Content-Type': 'application/json' },
1534
+ body: JSON.stringify({
1535
+ chat_id: settings.telegramChatId.split(',')[0].trim(),
1536
+ text: msg,
1537
+ disable_web_page_preview: true,
1538
+ }),
1539
+ });
1540
+ } catch {}
1541
+ }
1542
+
1543
+ async function notifyPipelineComplete(pipeline: Pipeline) {
1544
+ const settings = loadSettings();
1545
+ if (!settings.telegramBotToken || !settings.telegramChatId) return;
1546
+
1547
+ const icon = pipeline.status === 'done' ? '🎉' : '💥';
1548
+ const nodes = Object.entries(pipeline.nodes)
1549
+ .map(([id, n]) => ` ${n.status === 'done' ? '✅' : n.status === 'failed' ? '❌' : '⏭'} ${id}`)
1550
+ .join('\n');
1551
+
1552
+ const msg = `${icon} Pipeline ${pipeline.id} (${pipeline.workflowName}) ${pipeline.status}\n\n${nodes}`;
1553
+
1554
+ try {
1555
+ await fetch(`https://api.telegram.org/bot${settings.telegramBotToken}/sendMessage`, {
1556
+ method: 'POST',
1557
+ headers: { 'Content-Type': 'application/json' },
1558
+ body: JSON.stringify({
1559
+ chat_id: settings.telegramChatId.split(',')[0].trim(),
1560
+ text: msg,
1561
+ disable_web_page_preview: true,
1562
+ }),
1563
+ });
1564
+ } catch {}
1565
+ }