automagik-forge 0.1.11 → 0.1.13

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 (301) hide show
  1. package/.cargo/config.toml +13 -0
  2. package/.claude/commands/commit.md +376 -0
  3. package/.claude/commands/prompt.md +871 -0
  4. package/.env.example +20 -0
  5. package/.github/actions/setup-node/action.yml +29 -0
  6. package/.github/images/automagik-logo.png +0 -0
  7. package/.github/workflows/pre-release.yml +470 -0
  8. package/.github/workflows/publish.yml +145 -0
  9. package/.github/workflows/test.yml +63 -0
  10. package/.mcp.json +57 -0
  11. package/AGENT.md +40 -0
  12. package/CLAUDE.md +40 -0
  13. package/CODE-OF-CONDUCT.md +89 -0
  14. package/Cargo.toml +19 -0
  15. package/Dockerfile +43 -0
  16. package/LICENSE +201 -0
  17. package/Makefile +97 -0
  18. package/README.md +447 -143
  19. package/backend/.sqlx/query-01b7e2bac1261d8be3d03c03df3e5220590da6c31c77f161074fc62752d63881.json +12 -0
  20. package/backend/.sqlx/query-03f2b02ba6dc5ea2b3cf6b1004caea0ad6bcc10ebd63f441d321a389f026e263.json +12 -0
  21. package/backend/.sqlx/query-0923b77d137a29fc54d399a873ff15fc4af894490bc65a4d344a7575cb0d8643.json +12 -0
  22. package/backend/.sqlx/query-0f808bcdb63c5f180836e448dd64c435c51758b2fc54a52ce9e67495b1ab200e.json +68 -0
  23. package/backend/.sqlx/query-1268afe9ca849daa6722e3df7ca8e9e61f0d37052e782bb5452ab8e1018d9b63.json +12 -0
  24. package/backend/.sqlx/query-1b082630a9622f8667ee7a9aba2c2d3176019a68c6bb83d33008594821415a57.json +12 -0
  25. package/backend/.sqlx/query-1c7b06ba1e112abf6b945a2ff08a0b40ec23f3738c2e7399f067b558cf8d490e.json +12 -0
  26. package/backend/.sqlx/query-1f619f01f46859a64ded531dd0ef61abacfe62e758abe7030a6aa745140b95ca.json +104 -0
  27. package/backend/.sqlx/query-1fca1ce14b4b20205364cd1f1f45ebe1d2e30cd745e59e189d56487b5639dfbb.json +12 -0
  28. package/backend/.sqlx/query-212828320e8d871ab9d83705a040b23bcf0393dc7252177fc539a74657f578ef.json +32 -0
  29. package/backend/.sqlx/query-290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b.json +56 -0
  30. package/backend/.sqlx/query-2b471d2c2e8ffbe0cd42d2a91b814c0d79f9d09200f147e3cea33ba4ce673c8a.json +68 -0
  31. package/backend/.sqlx/query-354a48c705bb9bb2048c1b7f10fcb714e23f9db82b7a4ea6932486197b2ede6a.json +92 -0
  32. package/backend/.sqlx/query-36c9e3dd10648e94b949db5c91a774ecb1e10a899ef95da74066eccedca4d8b2.json +12 -0
  33. package/backend/.sqlx/query-36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94.json +56 -0
  34. package/backend/.sqlx/query-3a5b3c98a55ca183ab20c74708e3d7e579dda37972c059e7515c4ceee4bd8dd3.json +62 -0
  35. package/backend/.sqlx/query-3d0a1cabf2a52e9d90cdfd29c509ca89aeb448d0c1d2446c65cd43db40735e86.json +62 -0
  36. package/backend/.sqlx/query-3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031.json +56 -0
  37. package/backend/.sqlx/query-4049ca413b285a05aca6b25385e9c8185575f01e9069e4e8581aa45d713f612f.json +32 -0
  38. package/backend/.sqlx/query-412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1.json +104 -0
  39. package/backend/.sqlx/query-417a8b1ff4e51de82aea0159a3b97932224dc325b23476cb84153d690227fd8b.json +62 -0
  40. package/backend/.sqlx/query-461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2.json +56 -0
  41. package/backend/.sqlx/query-58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227.json +92 -0
  42. package/backend/.sqlx/query-5a886026d75d515c01f347cc203c8d99dd04c61dc468e2e4c5aa548436d13834.json +62 -0
  43. package/backend/.sqlx/query-5b902137b11022d2e1a5c4f6a9c83fec1a856c6a710aff831abd2382ede76b43.json +12 -0
  44. package/backend/.sqlx/query-5ed1238e52e59bb5f76c0f153fd99a14093f7ce2585bf9843585608f17ec575b.json +104 -0
  45. package/backend/.sqlx/query-6e8b860b14decfc2227dc57213f38442943d3fbef5c8418fd6b634c6e0f5e2ea.json +104 -0
  46. package/backend/.sqlx/query-6ec414276994c4ccb2433eaa5b1b342168557d17ddf5a52dac84cb1b59b9de8f.json +68 -0
  47. package/backend/.sqlx/query-6ecfa16d0cf825aacf233544b5baf151e9adfdca26c226ad71020d291fd802d5.json +62 -0
  48. package/backend/.sqlx/query-72509d252c39fce77520aa816cb2acbc1fb35dc2605e7be893610599b2427f2e.json +62 -0
  49. package/backend/.sqlx/query-75239b2da188f749707d77f3c1544332ca70db3d6d6743b2601dc0d167536437.json +62 -0
  50. package/backend/.sqlx/query-83d10e29f8478aff33434f9ac67068e013b888b953a2657e2bb72a6f619d04f2.json +50 -0
  51. package/backend/.sqlx/query-8610803360ea18b9b9d078a6981ea56abfbfe84e6354fc1d5ae4c622e01410ed.json +68 -0
  52. package/backend/.sqlx/query-86d03eb70eef39c59296416867f2ee66c9f7cd8b7f961fbda2f89fc0a1c442c2.json +12 -0
  53. package/backend/.sqlx/query-87d0feb5a6b442bad9c60068ea7569599cc6fc91a0e2692ecb42e93b03201b9d.json +68 -0
  54. package/backend/.sqlx/query-8a67b3b3337248f06a57bdf8a908f7ef23177431eaed82dc08c94c3e5944340e.json +12 -0
  55. package/backend/.sqlx/query-8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3.json +12 -0
  56. package/backend/.sqlx/query-90fd607fcb2dca72239ff25e618e21e174b195991eaa33722cbf5f76da84cfab.json +62 -0
  57. package/backend/.sqlx/query-92e8bdbcd80c5ff3db7a35cf79492048803ef305cbdef0d0a1fe5dc881ca8c71.json +104 -0
  58. package/backend/.sqlx/query-93a1605f90e9672dad29b472b6ad85fa9a55ea3ffa5abcb8724b09d61be254ca.json +20 -0
  59. package/backend/.sqlx/query-9472c8fb477958167f5fae40b85ac44252468c5226b2cdd7770f027332eed6d7.json +104 -0
  60. package/backend/.sqlx/query-96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2.json +56 -0
  61. package/backend/.sqlx/query-9edb2c01e91fd0f0fe7b56e988c7ae0393150f50be3f419a981e035c0121dfc7.json +104 -0
  62. package/backend/.sqlx/query-a157cf00616f703bfba21927f1eb1c9eec2a81c02da15f66efdba0b6c375de1b.json +26 -0
  63. package/backend/.sqlx/query-a31fff84f3b8e532fd1160447d89d700f06ae08821fee00c9a5b60492b05259c.json +62 -0
  64. package/backend/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json +12 -0
  65. package/backend/.sqlx/query-a6d2961718dbc3b1a925e549f49a159c561bef58c105529275f274b27e2eba5b.json +104 -0
  66. package/backend/.sqlx/query-a9e93d5b09b29faf66e387e4d7596a792d81e75c4d3726e83c2963e8d7c9b56f.json +104 -0
  67. package/backend/.sqlx/query-ac5247c8d7fb86e4650c4b0eb9420031614c831b7b085083bac20c1af314c538.json +12 -0
  68. package/backend/.sqlx/query-afef9467be74c411c4cb119a8b2b1aea53049877dfc30cc60b486134ba4b4c9f.json +68 -0
  69. package/backend/.sqlx/query-b2b2c6b4d0b1a347b5c4cb63c3a46a265d4db53be9554989a814b069d0af82f2.json +62 -0
  70. package/backend/.sqlx/query-c50d2ff0b12e5bcc81e371089ee2d007e233e7db93aefba4fef08e7aa68f5ab7.json +20 -0
  71. package/backend/.sqlx/query-c614e6056b244ca07f1b9d44e7edc9d5819225c6f8d9e077070c6e518a17f50b.json +12 -0
  72. package/backend/.sqlx/query-c67259be8bf4ee0cfd32167b2aa3b7fe9192809181a8171bf1c2d6df731967ae.json +12 -0
  73. package/backend/.sqlx/query-d2d0a1b985ebbca6a2b3e882a221a219f3199890fa640afc946ef1a792d6d8de.json +12 -0
  74. package/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json +20 -0
  75. package/backend/.sqlx/query-d3b9ea1de1576af71b312924ce7f4ea8ae5dbe2ac138ea3b4470f2d5cd734846.json +12 -0
  76. package/backend/.sqlx/query-ed8456646fa69ddd412441955f06ff22bfb790f29466450735e0b8bb1bc4ec94.json +12 -0
  77. package/backend/Cargo.toml +71 -0
  78. package/backend/build.rs +32 -0
  79. package/backend/migrations/20250617183714_init.sql +44 -0
  80. package/backend/migrations/20250620212427_execution_processes.sql +25 -0
  81. package/backend/migrations/20250620214100_remove_stdout_stderr_from_task_attempts.sql +28 -0
  82. package/backend/migrations/20250621120000_relate_activities_to_execution_processes.sql +23 -0
  83. package/backend/migrations/20250623120000_executor_sessions.sql +17 -0
  84. package/backend/migrations/20250623130000_add_executor_type_to_execution_processes.sql +4 -0
  85. package/backend/migrations/20250625000000_add_dev_script_to_projects.sql +4 -0
  86. package/backend/migrations/20250701000000_add_branch_to_task_attempts.sql +2 -0
  87. package/backend/migrations/20250701000001_add_pr_tracking_to_task_attempts.sql +5 -0
  88. package/backend/migrations/20250701120000_add_assistant_message_to_executor_sessions.sql +2 -0
  89. package/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql +2 -0
  90. package/backend/migrations/20250709000000_add_worktree_deleted_flag.sql +2 -0
  91. package/backend/migrations/20250710000000_add_setup_completion.sql +3 -0
  92. package/backend/migrations/20250715154859_add_task_templates.sql +25 -0
  93. package/backend/migrations/20250716143725_add_default_templates.sql +174 -0
  94. package/backend/migrations/20250716161432_update_executor_names_to_kebab_case.sql +20 -0
  95. package/backend/migrations/20250716170000_add_parent_task_to_tasks.sql +7 -0
  96. package/backend/migrations/20250717000000_drop_task_attempt_activities.sql +9 -0
  97. package/backend/migrations/20250719000000_add_cleanup_script_to_projects.sql +2 -0
  98. package/backend/migrations/20250720000000_add_cleanupscript_to_process_type_constraint.sql +25 -0
  99. package/backend/migrations/20250723000000_add_wish_to_tasks.sql +7 -0
  100. package/backend/migrations/20250724000000_remove_unique_wish_constraint.sql +5 -0
  101. package/backend/scripts/toast-notification.ps1 +23 -0
  102. package/backend/sounds/abstract-sound1.wav +0 -0
  103. package/backend/sounds/abstract-sound2.wav +0 -0
  104. package/backend/sounds/abstract-sound3.wav +0 -0
  105. package/backend/sounds/abstract-sound4.wav +0 -0
  106. package/backend/sounds/cow-mooing.wav +0 -0
  107. package/backend/sounds/phone-vibration.wav +0 -0
  108. package/backend/sounds/rooster.wav +0 -0
  109. package/backend/src/app_state.rs +218 -0
  110. package/backend/src/bin/generate_types.rs +189 -0
  111. package/backend/src/bin/mcp_task_server.rs +191 -0
  112. package/backend/src/execution_monitor.rs +1193 -0
  113. package/backend/src/executor.rs +1053 -0
  114. package/backend/src/executors/amp.rs +697 -0
  115. package/backend/src/executors/ccr.rs +91 -0
  116. package/backend/src/executors/charm_opencode.rs +113 -0
  117. package/backend/src/executors/claude.rs +887 -0
  118. package/backend/src/executors/cleanup_script.rs +124 -0
  119. package/backend/src/executors/dev_server.rs +53 -0
  120. package/backend/src/executors/echo.rs +79 -0
  121. package/backend/src/executors/gemini/config.rs +67 -0
  122. package/backend/src/executors/gemini/streaming.rs +363 -0
  123. package/backend/src/executors/gemini.rs +765 -0
  124. package/backend/src/executors/mod.rs +23 -0
  125. package/backend/src/executors/opencode_ai.rs +113 -0
  126. package/backend/src/executors/setup_script.rs +130 -0
  127. package/backend/src/executors/sst_opencode/filter.rs +184 -0
  128. package/backend/src/executors/sst_opencode/tools.rs +139 -0
  129. package/backend/src/executors/sst_opencode.rs +756 -0
  130. package/backend/src/lib.rs +45 -0
  131. package/backend/src/main.rs +324 -0
  132. package/backend/src/mcp/mod.rs +1 -0
  133. package/backend/src/mcp/task_server.rs +850 -0
  134. package/backend/src/middleware/mod.rs +3 -0
  135. package/backend/src/middleware/model_loaders.rs +242 -0
  136. package/backend/src/models/api_response.rs +36 -0
  137. package/backend/src/models/config.rs +375 -0
  138. package/backend/src/models/execution_process.rs +430 -0
  139. package/backend/src/models/executor_session.rs +225 -0
  140. package/backend/src/models/mod.rs +12 -0
  141. package/backend/src/models/project.rs +356 -0
  142. package/backend/src/models/task.rs +345 -0
  143. package/backend/src/models/task_attempt.rs +1214 -0
  144. package/backend/src/models/task_template.rs +146 -0
  145. package/backend/src/openapi.rs +93 -0
  146. package/backend/src/routes/auth.rs +297 -0
  147. package/backend/src/routes/config.rs +385 -0
  148. package/backend/src/routes/filesystem.rs +228 -0
  149. package/backend/src/routes/health.rs +16 -0
  150. package/backend/src/routes/mod.rs +9 -0
  151. package/backend/src/routes/projects.rs +562 -0
  152. package/backend/src/routes/stream.rs +244 -0
  153. package/backend/src/routes/task_attempts.rs +1172 -0
  154. package/backend/src/routes/task_templates.rs +229 -0
  155. package/backend/src/routes/tasks.rs +353 -0
  156. package/backend/src/services/analytics.rs +216 -0
  157. package/backend/src/services/git_service.rs +1321 -0
  158. package/backend/src/services/github_service.rs +307 -0
  159. package/backend/src/services/mod.rs +13 -0
  160. package/backend/src/services/notification_service.rs +263 -0
  161. package/backend/src/services/pr_monitor.rs +214 -0
  162. package/backend/src/services/process_service.rs +940 -0
  163. package/backend/src/utils/path.rs +96 -0
  164. package/backend/src/utils/shell.rs +19 -0
  165. package/backend/src/utils/text.rs +24 -0
  166. package/backend/src/utils/worktree_manager.rs +578 -0
  167. package/backend/src/utils.rs +125 -0
  168. package/backend/test.db +0 -0
  169. package/build-npm-package.sh +61 -0
  170. package/dev_assets_seed/config.json +19 -0
  171. package/frontend/.eslintrc.json +25 -0
  172. package/frontend/.prettierrc.json +8 -0
  173. package/frontend/components.json +17 -0
  174. package/frontend/index.html +19 -0
  175. package/frontend/package-lock.json +7321 -0
  176. package/frontend/package.json +61 -0
  177. package/frontend/postcss.config.js +6 -0
  178. package/frontend/public/android-chrome-192x192.png +0 -0
  179. package/frontend/public/android-chrome-512x512.png +0 -0
  180. package/frontend/public/apple-touch-icon.png +0 -0
  181. package/frontend/public/automagik-forge-logo-dark.svg +3 -0
  182. package/frontend/public/automagik-forge-logo.svg +3 -0
  183. package/frontend/public/automagik-forge-screenshot-overview.png +0 -0
  184. package/frontend/public/favicon-16x16.png +0 -0
  185. package/frontend/public/favicon-32x32.png +0 -0
  186. package/frontend/public/favicon.ico +0 -0
  187. package/frontend/public/site.webmanifest +1 -0
  188. package/frontend/public/viba-kanban-favicon.png +0 -0
  189. package/frontend/src/App.tsx +157 -0
  190. package/frontend/src/components/DisclaimerDialog.tsx +106 -0
  191. package/frontend/src/components/GitHubLoginDialog.tsx +314 -0
  192. package/frontend/src/components/OnboardingDialog.tsx +185 -0
  193. package/frontend/src/components/PrivacyOptInDialog.tsx +130 -0
  194. package/frontend/src/components/ProvidePatDialog.tsx +98 -0
  195. package/frontend/src/components/TaskTemplateManager.tsx +336 -0
  196. package/frontend/src/components/config-provider.tsx +119 -0
  197. package/frontend/src/components/context/TaskDetailsContextProvider.tsx +470 -0
  198. package/frontend/src/components/context/taskDetailsContext.ts +125 -0
  199. package/frontend/src/components/keyboard-shortcuts-demo.tsx +35 -0
  200. package/frontend/src/components/layout/navbar.tsx +86 -0
  201. package/frontend/src/components/logo.tsx +44 -0
  202. package/frontend/src/components/projects/ProjectCard.tsx +155 -0
  203. package/frontend/src/components/projects/project-detail.tsx +251 -0
  204. package/frontend/src/components/projects/project-form-fields.tsx +238 -0
  205. package/frontend/src/components/projects/project-form.tsx +301 -0
  206. package/frontend/src/components/projects/project-list.tsx +200 -0
  207. package/frontend/src/components/projects/projects-page.tsx +20 -0
  208. package/frontend/src/components/tasks/BranchSelector.tsx +169 -0
  209. package/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx +94 -0
  210. package/frontend/src/components/tasks/EditorSelectionDialog.tsx +119 -0
  211. package/frontend/src/components/tasks/TaskCard.tsx +154 -0
  212. package/frontend/src/components/tasks/TaskDetails/CollapsibleToolbar.tsx +33 -0
  213. package/frontend/src/components/tasks/TaskDetails/DiffCard.tsx +109 -0
  214. package/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx +135 -0
  215. package/frontend/src/components/tasks/TaskDetails/DiffFile.tsx +296 -0
  216. package/frontend/src/components/tasks/TaskDetails/DiffTab.tsx +32 -0
  217. package/frontend/src/components/tasks/TaskDetails/DisplayConversationEntry.tsx +392 -0
  218. package/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx +256 -0
  219. package/frontend/src/components/tasks/TaskDetails/LogsTab/ConversationEntry.tsx +56 -0
  220. package/frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx +92 -0
  221. package/frontend/src/components/tasks/TaskDetails/LogsTab/Prompt.tsx +22 -0
  222. package/frontend/src/components/tasks/TaskDetails/LogsTab/SetupScriptRunning.tsx +49 -0
  223. package/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +186 -0
  224. package/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +288 -0
  225. package/frontend/src/components/tasks/TaskDetails/RelatedTasksTab.tsx +216 -0
  226. package/frontend/src/components/tasks/TaskDetails/TabNavigation.tsx +93 -0
  227. package/frontend/src/components/tasks/TaskDetailsHeader.tsx +169 -0
  228. package/frontend/src/components/tasks/TaskDetailsPanel.tsx +126 -0
  229. package/frontend/src/components/tasks/TaskDetailsToolbar.tsx +302 -0
  230. package/frontend/src/components/tasks/TaskFollowUpSection.tsx +130 -0
  231. package/frontend/src/components/tasks/TaskFormDialog.tsx +400 -0
  232. package/frontend/src/components/tasks/TaskKanbanBoard.tsx +180 -0
  233. package/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx +259 -0
  234. package/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx +243 -0
  235. package/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx +899 -0
  236. package/frontend/src/components/tasks/index.ts +2 -0
  237. package/frontend/src/components/theme-provider.tsx +82 -0
  238. package/frontend/src/components/theme-toggle.tsx +36 -0
  239. package/frontend/src/components/ui/alert.tsx +59 -0
  240. package/frontend/src/components/ui/auto-expanding-textarea.tsx +70 -0
  241. package/frontend/src/components/ui/badge.tsx +36 -0
  242. package/frontend/src/components/ui/button.tsx +56 -0
  243. package/frontend/src/components/ui/card.tsx +86 -0
  244. package/frontend/src/components/ui/checkbox.tsx +44 -0
  245. package/frontend/src/components/ui/chip.tsx +25 -0
  246. package/frontend/src/components/ui/dialog.tsx +124 -0
  247. package/frontend/src/components/ui/dropdown-menu.tsx +198 -0
  248. package/frontend/src/components/ui/file-search-textarea.tsx +292 -0
  249. package/frontend/src/components/ui/folder-picker.tsx +279 -0
  250. package/frontend/src/components/ui/input.tsx +25 -0
  251. package/frontend/src/components/ui/label.tsx +24 -0
  252. package/frontend/src/components/ui/loader.tsx +26 -0
  253. package/frontend/src/components/ui/markdown-renderer.tsx +75 -0
  254. package/frontend/src/components/ui/select.tsx +160 -0
  255. package/frontend/src/components/ui/separator.tsx +31 -0
  256. package/frontend/src/components/ui/shadcn-io/kanban/index.tsx +185 -0
  257. package/frontend/src/components/ui/table.tsx +117 -0
  258. package/frontend/src/components/ui/tabs.tsx +53 -0
  259. package/frontend/src/components/ui/textarea.tsx +22 -0
  260. package/frontend/src/components/ui/tooltip.tsx +28 -0
  261. package/frontend/src/hooks/useNormalizedConversation.ts +440 -0
  262. package/frontend/src/index.css +225 -0
  263. package/frontend/src/lib/api.ts +630 -0
  264. package/frontend/src/lib/keyboard-shortcuts.ts +266 -0
  265. package/frontend/src/lib/responsive-config.ts +70 -0
  266. package/frontend/src/lib/types.ts +39 -0
  267. package/frontend/src/lib/utils.ts +10 -0
  268. package/frontend/src/main.tsx +50 -0
  269. package/frontend/src/pages/McpServers.tsx +418 -0
  270. package/frontend/src/pages/Settings.tsx +610 -0
  271. package/frontend/src/pages/project-tasks.tsx +575 -0
  272. package/frontend/src/pages/projects.tsx +18 -0
  273. package/frontend/src/vite-env.d.ts +1 -0
  274. package/frontend/tailwind.config.js +125 -0
  275. package/frontend/tsconfig.json +26 -0
  276. package/frontend/tsconfig.node.json +10 -0
  277. package/frontend/vite.config.ts +33 -0
  278. package/npx-cli/README.md +159 -0
  279. package/npx-cli/automagik-forge-0.0.55.tgz +0 -0
  280. package/npx-cli/automagik-forge-0.1.0.tgz +0 -0
  281. package/{dist/linux-x64/automagik-forge.zip → npx-cli/automagik-forge-0.1.10.tgz} +0 -0
  282. package/npx-cli/package.json +17 -0
  283. package/npx-cli/vibe-kanban-0.0.55.tgz +0 -0
  284. package/package.json +23 -13
  285. package/pnpm-workspace.yaml +2 -0
  286. package/rust-toolchain.toml +11 -0
  287. package/rustfmt.toml +3 -0
  288. package/scripts/load-env.js +43 -0
  289. package/scripts/mcp_test.js +374 -0
  290. package/scripts/prepare-db.js +45 -0
  291. package/scripts/setup-dev-environment.js +274 -0
  292. package/scripts/start-mcp-sse.js +70 -0
  293. package/scripts/test-debug.js +32 -0
  294. package/scripts/test-mcp-sse.js +138 -0
  295. package/scripts/test-simple.js +44 -0
  296. package/scripts/test-wish-final.js +179 -0
  297. package/scripts/test-wish-system.js +221 -0
  298. package/shared/types.ts +182 -0
  299. package/test-npm-package.sh +42 -0
  300. package/dist/linux-x64/automagik-forge-mcp.zip +0 -0
  301. /package/{bin → npx-cli/bin}/cli.js +0 -0
@@ -0,0 +1,1172 @@
1
+ use axum::{
2
+ extract::{Query, State},
3
+ http::StatusCode,
4
+ middleware::from_fn_with_state,
5
+ response::Json as ResponseJson,
6
+ routing::get,
7
+ Extension, Json, Router,
8
+ };
9
+ use serde::{Deserialize, Serialize};
10
+ use sqlx::SqlitePool;
11
+ use ts_rs::TS;
12
+ use utoipa;
13
+ use uuid::Uuid;
14
+
15
+ use crate::{
16
+ app_state::AppState,
17
+ executor::{
18
+ ActionType, ExecutorConfig, NormalizedConversation, NormalizedEntry, NormalizedEntryType,
19
+ },
20
+ middleware::{load_execution_process_with_context_middleware, load_task_attempt_middleware},
21
+ models::{
22
+ config::Config,
23
+ execution_process::{
24
+ ExecutionProcess, ExecutionProcessStatus, ExecutionProcessSummary, ExecutionProcessType,
25
+ },
26
+ project::Project,
27
+ task::{Task, TaskStatus},
28
+ task_attempt::{
29
+ BranchStatus, CreateFollowUpAttempt, CreatePrParams, CreateTaskAttempt, TaskAttempt,
30
+ TaskAttemptState, WorktreeDiff,
31
+ },
32
+ ApiResponse,
33
+ },
34
+ };
35
+
36
+ #[derive(Debug, Deserialize, Serialize)]
37
+ pub struct RebaseTaskAttemptRequest {
38
+ pub new_base_branch: Option<String>,
39
+ }
40
+
41
+ #[derive(Debug, Deserialize, Serialize)]
42
+ pub struct CreateGitHubPRRequest {
43
+ pub title: String,
44
+ pub body: Option<String>,
45
+ pub base_branch: Option<String>,
46
+ }
47
+
48
+ #[derive(Debug, Serialize)]
49
+ pub struct FollowUpResponse {
50
+ pub message: String,
51
+ pub actual_attempt_id: Uuid,
52
+ pub created_new_attempt: bool,
53
+ }
54
+
55
+ #[derive(Debug, Serialize, TS)]
56
+ #[ts(export)]
57
+ pub struct ProcessLogsResponse {
58
+ pub id: Uuid,
59
+ pub process_type: ExecutionProcessType,
60
+ pub command: String,
61
+ pub executor_type: Option<String>,
62
+ pub status: ExecutionProcessStatus,
63
+ pub normalized_conversation: NormalizedConversation,
64
+ }
65
+
66
+ // Helper to normalize logs for a process (extracted from get_execution_process_normalized_logs)
67
+ async fn normalize_process_logs(
68
+ db_pool: &SqlitePool,
69
+ process: &ExecutionProcess,
70
+ ) -> NormalizedConversation {
71
+ use crate::models::{
72
+ execution_process::ExecutionProcessType, executor_session::ExecutorSession,
73
+ };
74
+ let executor_session = ExecutorSession::find_by_execution_process_id(db_pool, process.id)
75
+ .await
76
+ .ok()
77
+ .flatten();
78
+
79
+ let has_stdout = process
80
+ .stdout
81
+ .as_ref()
82
+ .map(|s| !s.trim().is_empty())
83
+ .unwrap_or(false);
84
+ let has_stderr = process
85
+ .stderr
86
+ .as_ref()
87
+ .map(|s| !s.trim().is_empty())
88
+ .unwrap_or(false);
89
+
90
+ if !has_stdout && !has_stderr {
91
+ return NormalizedConversation {
92
+ entries: vec![],
93
+ session_id: None,
94
+ executor_type: process
95
+ .executor_type
96
+ .clone()
97
+ .unwrap_or("unknown".to_string()),
98
+ prompt: executor_session.as_ref().and_then(|s| s.prompt.clone()),
99
+ summary: executor_session.as_ref().and_then(|s| s.summary.clone()),
100
+ };
101
+ }
102
+
103
+ // Parse stdout as JSONL using executor normalization
104
+ let mut stdout_entries = Vec::new();
105
+ if let Some(stdout) = &process.stdout {
106
+ if !stdout.trim().is_empty() {
107
+ let executor_type = process.executor_type.as_deref().unwrap_or("unknown");
108
+ let executor_config = if process.process_type == ExecutionProcessType::SetupScript {
109
+ ExecutorConfig::SetupScript {
110
+ script: executor_session
111
+ .as_ref()
112
+ .and_then(|s| s.prompt.clone())
113
+ .unwrap_or_else(|| "setup script".to_string()),
114
+ }
115
+ } else {
116
+ match executor_type.to_string().parse() {
117
+ Ok(config) => config,
118
+ Err(_) => {
119
+ return NormalizedConversation {
120
+ entries: vec![],
121
+ session_id: None,
122
+ executor_type: executor_type.to_string(),
123
+ prompt: executor_session.as_ref().and_then(|s| s.prompt.clone()),
124
+ summary: executor_session.as_ref().and_then(|s| s.summary.clone()),
125
+ };
126
+ }
127
+ }
128
+ };
129
+ let executor = executor_config.create_executor();
130
+ let working_dir_path = match std::fs::canonicalize(&process.working_directory) {
131
+ Ok(canonical_path) => canonical_path.to_string_lossy().to_string(),
132
+ Err(_) => process.working_directory.clone(),
133
+ };
134
+ if let Ok(normalized) = executor.normalize_logs(stdout, &working_dir_path) {
135
+ stdout_entries = normalized.entries;
136
+ }
137
+ }
138
+ }
139
+ // Parse stderr chunks separated by boundary markers
140
+ let mut stderr_entries = Vec::new();
141
+ if let Some(stderr) = &process.stderr {
142
+ let trimmed = stderr.trim();
143
+ if !trimmed.is_empty() {
144
+ let chunks: Vec<&str> = trimmed.split("---STDERR_CHUNK_BOUNDARY---").collect();
145
+ for chunk in chunks {
146
+ let chunk_trimmed = chunk.trim();
147
+ if !chunk_trimmed.is_empty() {
148
+ let filtered_content = chunk_trimmed.replace("---STDERR_CHUNK_BOUNDARY---", "");
149
+ if !filtered_content.trim().is_empty() {
150
+ stderr_entries.push(NormalizedEntry {
151
+ timestamp: Some(chrono::Utc::now().to_rfc3339()),
152
+ entry_type: NormalizedEntryType::ErrorMessage,
153
+ content: filtered_content.trim().to_string(),
154
+ metadata: None,
155
+ });
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ let mut all_entries = Vec::new();
162
+ all_entries.extend(stdout_entries);
163
+ all_entries.extend(stderr_entries);
164
+ all_entries.sort_by(|a, b| match (&a.timestamp, &b.timestamp) {
165
+ (Some(a_ts), Some(b_ts)) => a_ts.cmp(b_ts),
166
+ (Some(_), None) => std::cmp::Ordering::Less,
167
+ (None, Some(_)) => std::cmp::Ordering::Greater,
168
+ (None, None) => std::cmp::Ordering::Equal,
169
+ });
170
+ let executor_type = if process.process_type == ExecutionProcessType::SetupScript {
171
+ "setup-script".to_string()
172
+ } else {
173
+ process
174
+ .executor_type
175
+ .clone()
176
+ .unwrap_or("unknown".to_string())
177
+ };
178
+ NormalizedConversation {
179
+ entries: all_entries,
180
+ session_id: None,
181
+ executor_type,
182
+ prompt: executor_session.as_ref().and_then(|s| s.prompt.clone()),
183
+ summary: executor_session.as_ref().and_then(|s| s.summary.clone()),
184
+ }
185
+ }
186
+
187
+ /// Get all normalized logs for all execution processes of a task attempt
188
+ pub async fn get_task_attempt_all_logs(
189
+ Extension(_project): Extension<Project>,
190
+ Extension(_task): Extension<Task>,
191
+ Extension(task_attempt): Extension<TaskAttempt>,
192
+ State(app_state): State<AppState>,
193
+ ) -> Result<Json<ApiResponse<Vec<ProcessLogsResponse>>>, StatusCode> {
194
+ // Fetch all execution processes for this attempt
195
+ let processes = match ExecutionProcess::find_by_task_attempt_id(
196
+ &app_state.db_pool,
197
+ task_attempt.id,
198
+ )
199
+ .await
200
+ {
201
+ Ok(list) => list,
202
+ Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
203
+ };
204
+ // For each process, normalize logs
205
+ let mut result = Vec::new();
206
+ for process in processes {
207
+ let normalized_conversation = normalize_process_logs(&app_state.db_pool, &process).await;
208
+ result.push(ProcessLogsResponse {
209
+ id: process.id,
210
+ process_type: process.process_type.clone(),
211
+ command: process.command.clone(),
212
+ executor_type: process.executor_type.clone(),
213
+ status: process.status.clone(),
214
+ normalized_conversation,
215
+ });
216
+ }
217
+ Ok(Json(ApiResponse::success(result)))
218
+ }
219
+
220
+ #[utoipa::path(
221
+ get,
222
+ path = "/api/projects/{project_id}/tasks/{task_id}/attempts",
223
+ params(
224
+ ("project_id" = String, Path, description = "Project ID"),
225
+ ("task_id" = String, Path, description = "Task ID")
226
+ ),
227
+ responses(
228
+ (status = 200, description = "List all task attempts", body = ApiResponse<Vec<TaskAttempt>>),
229
+ (status = 404, description = "Project or task not found"),
230
+ (status = 500, description = "Internal server error")
231
+ ),
232
+ tag = "task_attempts"
233
+ )]
234
+ pub async fn get_task_attempts(
235
+ Extension(_project): Extension<Project>,
236
+ Extension(task): Extension<Task>,
237
+ State(app_state): State<AppState>,
238
+ ) -> Result<ResponseJson<ApiResponse<Vec<TaskAttempt>>>, StatusCode> {
239
+ match TaskAttempt::find_by_task_id(&app_state.db_pool, task.id).await {
240
+ Ok(attempts) => Ok(ResponseJson(ApiResponse::success(attempts))),
241
+ Err(e) => {
242
+ tracing::error!("Failed to fetch task attempts for task {}: {}", task.id, e);
243
+ Err(StatusCode::INTERNAL_SERVER_ERROR)
244
+ }
245
+ }
246
+ }
247
+
248
+ #[utoipa::path(
249
+ post,
250
+ path = "/api/projects/{project_id}/tasks/{task_id}/attempts",
251
+ params(
252
+ ("project_id" = String, Path, description = "Project ID"),
253
+ ("task_id" = String, Path, description = "Task ID")
254
+ ),
255
+ request_body = CreateTaskAttempt,
256
+ responses(
257
+ (status = 200, description = "Task attempt created successfully", body = ApiResponse<TaskAttempt>),
258
+ (status = 404, description = "Project or task not found"),
259
+ (status = 400, description = "Invalid input"),
260
+ (status = 500, description = "Internal server error")
261
+ ),
262
+ tag = "task_attempts"
263
+ )]
264
+ pub async fn create_task_attempt(
265
+ Extension(_project): Extension<Project>,
266
+ Extension(task): Extension<Task>,
267
+ State(app_state): State<AppState>,
268
+ Json(payload): Json<CreateTaskAttempt>,
269
+ ) -> Result<ResponseJson<ApiResponse<TaskAttempt>>, StatusCode> {
270
+ let executor_string = payload.executor.as_ref().map(|exec| exec.to_string());
271
+
272
+ match TaskAttempt::create(&app_state.db_pool, &payload, task.id).await {
273
+ Ok(attempt) => {
274
+ app_state
275
+ .track_analytics_event(
276
+ "task_attempt_started",
277
+ Some(serde_json::json!({
278
+ "task_id": task.id.to_string(),
279
+ "executor_type": executor_string.as_deref().unwrap_or("default"),
280
+ "attempt_id": attempt.id.to_string(),
281
+ })),
282
+ )
283
+ .await;
284
+
285
+ // Start execution asynchronously (don't block the response)
286
+ let app_state_clone = app_state.clone();
287
+ let attempt_id = attempt.id;
288
+ let task_id = task.id;
289
+ let project_id = _project.id;
290
+ tokio::spawn(async move {
291
+ if let Err(e) = TaskAttempt::start_execution(
292
+ &app_state_clone.db_pool,
293
+ &app_state_clone,
294
+ attempt_id,
295
+ task_id,
296
+ project_id,
297
+ )
298
+ .await
299
+ {
300
+ tracing::error!(
301
+ "Failed to start execution for task attempt {}: {}",
302
+ attempt_id,
303
+ e
304
+ );
305
+ }
306
+ });
307
+
308
+ Ok(ResponseJson(ApiResponse::success(attempt)))
309
+ }
310
+ Err(e) => {
311
+ tracing::error!("Failed to create task attempt: {}", e);
312
+ Err(StatusCode::INTERNAL_SERVER_ERROR)
313
+ }
314
+ }
315
+ }
316
+
317
+ pub async fn get_task_attempt_diff(
318
+ Extension(project): Extension<Project>,
319
+ Extension(task): Extension<Task>,
320
+ Extension(task_attempt): Extension<TaskAttempt>,
321
+ State(app_state): State<AppState>,
322
+ ) -> Result<ResponseJson<ApiResponse<WorktreeDiff>>, StatusCode> {
323
+ match TaskAttempt::get_diff(&app_state.db_pool, task_attempt.id, task.id, project.id).await {
324
+ Ok(diff) => Ok(ResponseJson(ApiResponse::success(diff))),
325
+ Err(e) => {
326
+ tracing::error!(
327
+ "Failed to get diff for task attempt {}: {}",
328
+ task_attempt.id,
329
+ e
330
+ );
331
+ Err(StatusCode::INTERNAL_SERVER_ERROR)
332
+ }
333
+ }
334
+ }
335
+
336
+ #[axum::debug_handler]
337
+ pub async fn merge_task_attempt(
338
+ Extension(project): Extension<Project>,
339
+ Extension(task): Extension<Task>,
340
+ Extension(task_attempt): Extension<TaskAttempt>,
341
+ State(app_state): State<AppState>,
342
+ ) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
343
+ match TaskAttempt::merge_changes(&app_state.db_pool, task_attempt.id, task.id, project.id).await
344
+ {
345
+ Ok(_) => {
346
+ // Update task status to Done
347
+ if let Err(e) = Task::update_status(
348
+ &app_state.db_pool,
349
+ task.id,
350
+ project.id,
351
+ crate::models::task::TaskStatus::Done,
352
+ )
353
+ .await
354
+ {
355
+ tracing::error!("Failed to update task status to Done after merge: {}", e);
356
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
357
+ }
358
+
359
+ // Track task attempt merged event
360
+ app_state
361
+ .track_analytics_event(
362
+ "task_attempt_merged",
363
+ Some(serde_json::json!({
364
+ "task_id": task.id.to_string(),
365
+ "project_id": project.id.to_string(),
366
+ "attempt_id": task_attempt.id.to_string(),
367
+ })),
368
+ )
369
+ .await;
370
+
371
+ Ok(ResponseJson(ApiResponse::success(())))
372
+ }
373
+ Err(e) => {
374
+ tracing::error!("Failed to merge task attempt {}: {}", task_attempt.id, e);
375
+ Ok(ResponseJson(ApiResponse::error(&format!(
376
+ "Failed to merge: {}",
377
+ e
378
+ ))))
379
+ }
380
+ }
381
+ }
382
+
383
+ pub async fn create_github_pr(
384
+ Extension(project): Extension<Project>,
385
+ Extension(task): Extension<Task>,
386
+ Extension(task_attempt): Extension<TaskAttempt>,
387
+ State(app_state): State<AppState>,
388
+ Json(request): Json<CreateGitHubPRRequest>,
389
+ ) -> Result<ResponseJson<ApiResponse<String>>, StatusCode> {
390
+ // Load the user's GitHub configuration
391
+ let config = match Config::load(&crate::utils::config_path()) {
392
+ Ok(config) => config,
393
+ Err(e) => {
394
+ tracing::error!("Failed to load config: {}", e);
395
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
396
+ }
397
+ };
398
+
399
+ let github_token = match config.github.token {
400
+ Some(token) => token,
401
+ None => {
402
+ return Ok(ResponseJson(ApiResponse::error(
403
+ "GitHub authentication not configured. Please sign in with GitHub.",
404
+ )));
405
+ }
406
+ };
407
+
408
+ // Get the task attempt to access the stored base branch
409
+ let attempt = &task_attempt;
410
+
411
+ let base_branch = request.base_branch.unwrap_or_else(|| {
412
+ // Use the stored base branch from the task attempt as the default
413
+ // Fall back to config default or "main" only if stored base branch is somehow invalid
414
+ if !attempt.base_branch.trim().is_empty() {
415
+ attempt.base_branch.clone()
416
+ } else {
417
+ config
418
+ .github
419
+ .default_pr_base
420
+ .unwrap_or_else(|| "main".to_string())
421
+ }
422
+ });
423
+
424
+ match TaskAttempt::create_github_pr(
425
+ &app_state.db_pool,
426
+ CreatePrParams {
427
+ attempt_id: task_attempt.id,
428
+ task_id: task.id,
429
+ project_id: project.id,
430
+ github_token: &config.github.pat.unwrap_or(github_token),
431
+ title: &request.title,
432
+ body: request.body.as_deref(),
433
+ base_branch: Some(&base_branch),
434
+ },
435
+ )
436
+ .await
437
+ {
438
+ Ok(pr_url) => {
439
+ app_state
440
+ .track_analytics_event(
441
+ "github_pr_created",
442
+ Some(serde_json::json!({
443
+ "task_id": task.id.to_string(),
444
+ "project_id": project.id.to_string(),
445
+ "attempt_id": task_attempt.id.to_string(),
446
+ })),
447
+ )
448
+ .await;
449
+
450
+ Ok(ResponseJson(ApiResponse::success(pr_url)))
451
+ }
452
+ Err(e) => {
453
+ tracing::error!(
454
+ "Failed to create GitHub PR for attempt {}: {}",
455
+ task_attempt.id,
456
+ e
457
+ );
458
+ let message = match &e {
459
+ crate::models::task_attempt::TaskAttemptError::GitHubService(
460
+ crate::services::GitHubServiceError::TokenInvalid,
461
+ ) => Some("github_token_invalid".to_string()),
462
+ crate::models::task_attempt::TaskAttemptError::GitService(
463
+ crate::services::git_service::GitServiceError::Git(err),
464
+ ) if err
465
+ .message()
466
+ .contains("too many redirects or authentication replays") =>
467
+ {
468
+ Some("insufficient_github_permissions".to_string()) // PAT is invalid
469
+ }
470
+ crate::models::task_attempt::TaskAttemptError::GitService(
471
+ crate::services::git_service::GitServiceError::Git(err),
472
+ ) if err.message().contains("status code: 403") => {
473
+ Some("insufficient_github_permissions".to_string())
474
+ }
475
+ crate::models::task_attempt::TaskAttemptError::GitService(
476
+ crate::services::git_service::GitServiceError::Git(err),
477
+ ) if err.message().contains("status code: 404") => {
478
+ Some("github_repo_not_found_or_no_access".to_string())
479
+ }
480
+ _ => Some(format!("Failed to create PR: {}", e)),
481
+ };
482
+ Ok(ResponseJson(ApiResponse::error(
483
+ message.as_deref().unwrap_or("Unknown error"),
484
+ )))
485
+ }
486
+ }
487
+ }
488
+
489
+ #[derive(serde::Deserialize)]
490
+ pub struct OpenEditorRequest {
491
+ editor_type: Option<String>,
492
+ }
493
+
494
+ pub async fn open_task_attempt_in_editor(
495
+ Extension(_project): Extension<Project>,
496
+ Extension(_task): Extension<Task>,
497
+ Extension(task_attempt): Extension<TaskAttempt>,
498
+ State(app_state): State<AppState>,
499
+ Json(payload): Json<Option<OpenEditorRequest>>,
500
+ ) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
501
+ // Get the task attempt to access the worktree path
502
+ let attempt = &task_attempt;
503
+
504
+ // Get editor command from config or override
505
+ let editor_command = {
506
+ let config_guard = app_state.get_config().read().await;
507
+ if let Some(ref request) = payload {
508
+ if let Some(ref editor_type) = request.editor_type {
509
+ // Create a temporary editor config with the override
510
+ use crate::models::config::{EditorConfig, EditorType};
511
+ let override_editor_type = match editor_type.as_str() {
512
+ "vscode" => EditorType::VSCode,
513
+ "cursor" => EditorType::Cursor,
514
+ "windsurf" => EditorType::Windsurf,
515
+ "intellij" => EditorType::IntelliJ,
516
+ "zed" => EditorType::Zed,
517
+ "custom" => EditorType::Custom,
518
+ _ => config_guard.editor.editor_type.clone(),
519
+ };
520
+ let temp_config = EditorConfig {
521
+ editor_type: override_editor_type,
522
+ custom_command: config_guard.editor.custom_command.clone(),
523
+ };
524
+ temp_config.get_command()
525
+ } else {
526
+ config_guard.editor.get_command()
527
+ }
528
+ } else {
529
+ config_guard.editor.get_command()
530
+ }
531
+ };
532
+
533
+ // Open editor in the worktree directory
534
+ let mut cmd = std::process::Command::new(&editor_command[0]);
535
+ for arg in &editor_command[1..] {
536
+ cmd.arg(arg);
537
+ }
538
+ cmd.arg(&attempt.worktree_path);
539
+
540
+ match cmd.spawn() {
541
+ Ok(_) => {
542
+ tracing::info!(
543
+ "Opened editor ({}) for task attempt {} at path: {}",
544
+ editor_command.join(" "),
545
+ task_attempt.id,
546
+ attempt.worktree_path
547
+ );
548
+ Ok(ResponseJson(ApiResponse::success(())))
549
+ }
550
+ Err(e) => {
551
+ tracing::error!(
552
+ "Failed to open editor ({}) for attempt {}: {}",
553
+ editor_command.join(" "),
554
+ task_attempt.id,
555
+ e
556
+ );
557
+ Err(StatusCode::INTERNAL_SERVER_ERROR)
558
+ }
559
+ }
560
+ }
561
+
562
+ pub async fn get_task_attempt_branch_status(
563
+ Extension(project): Extension<Project>,
564
+ Extension(task): Extension<Task>,
565
+ Extension(task_attempt): Extension<TaskAttempt>,
566
+ State(app_state): State<AppState>,
567
+ ) -> Result<ResponseJson<ApiResponse<BranchStatus>>, StatusCode> {
568
+ match TaskAttempt::get_branch_status(&app_state.db_pool, task_attempt.id, task.id, project.id)
569
+ .await
570
+ {
571
+ Ok(status) => Ok(ResponseJson(ApiResponse::success(status))),
572
+ Err(e) => {
573
+ tracing::error!(
574
+ "Failed to get branch status for task attempt {}: {}",
575
+ task_attempt.id,
576
+ e
577
+ );
578
+ Err(StatusCode::INTERNAL_SERVER_ERROR)
579
+ }
580
+ }
581
+ }
582
+
583
+ #[axum::debug_handler]
584
+ pub async fn rebase_task_attempt(
585
+ Extension(project): Extension<Project>,
586
+ Extension(task): Extension<Task>,
587
+ Extension(task_attempt): Extension<TaskAttempt>,
588
+ State(app_state): State<AppState>,
589
+ request_body: Option<Json<RebaseTaskAttemptRequest>>,
590
+ ) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
591
+ // Extract new base branch from request body if provided
592
+ let new_base_branch = request_body.and_then(|body| body.new_base_branch.clone());
593
+
594
+ match TaskAttempt::rebase_attempt(
595
+ &app_state.db_pool,
596
+ task_attempt.id,
597
+ task.id,
598
+ project.id,
599
+ new_base_branch,
600
+ )
601
+ .await
602
+ {
603
+ Ok(_new_base_commit) => Ok(ResponseJson(ApiResponse::success(()))),
604
+ Err(e) => {
605
+ tracing::error!("Failed to rebase task attempt {}: {}", task_attempt.id, e);
606
+ Ok(ResponseJson(ApiResponse::error(&e.to_string())))
607
+ }
608
+ }
609
+ }
610
+
611
+ pub async fn get_task_attempt_execution_processes(
612
+ Extension(_project): Extension<Project>,
613
+ Extension(_task): Extension<Task>,
614
+ Extension(task_attempt): Extension<TaskAttempt>,
615
+ State(app_state): State<AppState>,
616
+ ) -> Result<ResponseJson<ApiResponse<Vec<ExecutionProcessSummary>>>, StatusCode> {
617
+ match ExecutionProcess::find_summaries_by_task_attempt_id(&app_state.db_pool, task_attempt.id)
618
+ .await
619
+ {
620
+ Ok(processes) => Ok(ResponseJson(ApiResponse::success(processes))),
621
+ Err(e) => {
622
+ tracing::error!(
623
+ "Failed to fetch execution processes for attempt {}: {}",
624
+ task_attempt.id,
625
+ e
626
+ );
627
+ Err(StatusCode::INTERNAL_SERVER_ERROR)
628
+ }
629
+ }
630
+ }
631
+
632
+ pub async fn get_execution_process(
633
+ Extension(execution_process): Extension<ExecutionProcess>,
634
+ ) -> Result<ResponseJson<ApiResponse<ExecutionProcess>>, StatusCode> {
635
+ Ok(ResponseJson(ApiResponse::success(execution_process)))
636
+ }
637
+
638
+ #[axum::debug_handler]
639
+ pub async fn stop_all_execution_processes(
640
+ Extension(_project): Extension<Project>,
641
+ Extension(_task): Extension<Task>,
642
+ Extension(task_attempt): Extension<TaskAttempt>,
643
+ State(app_state): State<AppState>,
644
+ ) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
645
+ // Get all execution processes for the task attempt
646
+ let processes = match ExecutionProcess::find_by_task_attempt_id(
647
+ &app_state.db_pool,
648
+ task_attempt.id,
649
+ )
650
+ .await
651
+ {
652
+ Ok(processes) => processes,
653
+ Err(e) => {
654
+ tracing::error!(
655
+ "Failed to fetch execution processes for attempt {}: {}",
656
+ task_attempt.id,
657
+ e
658
+ );
659
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
660
+ }
661
+ };
662
+
663
+ let mut stopped_count = 0;
664
+ let mut errors = Vec::new();
665
+
666
+ // Stop all running processes
667
+ for process in processes {
668
+ match app_state.stop_running_execution_by_id(process.id).await {
669
+ Ok(true) => {
670
+ stopped_count += 1;
671
+
672
+ // Update the execution process status in the database
673
+ if let Err(e) = ExecutionProcess::update_completion(
674
+ &app_state.db_pool,
675
+ process.id,
676
+ crate::models::execution_process::ExecutionProcessStatus::Killed,
677
+ None,
678
+ )
679
+ .await
680
+ {
681
+ tracing::error!("Failed to update execution process status: {}", e);
682
+ errors.push(format!("Failed to update process {} status", process.id));
683
+ } else {
684
+ // Process stopped successfully
685
+ }
686
+ }
687
+ Ok(false) => {
688
+ // Process was not running, which is fine
689
+ }
690
+ Err(e) => {
691
+ tracing::error!("Failed to stop execution process {}: {}", process.id, e);
692
+ errors.push(format!("Failed to stop process {}: {}", process.id, e));
693
+ }
694
+ }
695
+ }
696
+
697
+ if !errors.is_empty() {
698
+ return Ok(ResponseJson(ApiResponse::error(&format!(
699
+ "Stopped {} processes, but encountered errors: {}",
700
+ stopped_count,
701
+ errors.join(", ")
702
+ ))));
703
+ }
704
+
705
+ if stopped_count == 0 {
706
+ return Ok(ResponseJson(ApiResponse::success(())));
707
+ }
708
+
709
+ Ok(ResponseJson(ApiResponse::success(())))
710
+ }
711
+
712
+ #[axum::debug_handler]
713
+ pub async fn stop_execution_process(
714
+ Extension(_project): Extension<Project>,
715
+ Extension(_task): Extension<Task>,
716
+ Extension(_task_attempt): Extension<TaskAttempt>,
717
+ Extension(execution_process): Extension<ExecutionProcess>,
718
+ State(app_state): State<AppState>,
719
+ ) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
720
+ // Stop the specific execution process
721
+ let stopped = match app_state
722
+ .stop_running_execution_by_id(execution_process.id)
723
+ .await
724
+ {
725
+ Ok(stopped) => stopped,
726
+ Err(e) => {
727
+ tracing::error!(
728
+ "Failed to stop execution process {}: {}",
729
+ execution_process.id,
730
+ e
731
+ );
732
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
733
+ }
734
+ };
735
+
736
+ if !stopped {
737
+ return Ok(ResponseJson(ApiResponse::success(())));
738
+ }
739
+
740
+ // Update the execution process status in the database
741
+ if let Err(e) = ExecutionProcess::update_completion(
742
+ &app_state.db_pool,
743
+ execution_process.id,
744
+ crate::models::execution_process::ExecutionProcessStatus::Killed,
745
+ None,
746
+ )
747
+ .await
748
+ {
749
+ tracing::error!("Failed to update execution process status: {}", e);
750
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
751
+ }
752
+
753
+ // Process stopped successfully
754
+
755
+ Ok(ResponseJson(ApiResponse::success(())))
756
+ }
757
+
758
+ #[derive(serde::Deserialize)]
759
+ pub struct DeleteFileQuery {
760
+ file_path: String,
761
+ }
762
+
763
+ #[axum::debug_handler]
764
+ pub async fn delete_task_attempt_file(
765
+ Extension(project): Extension<Project>,
766
+ Extension(task): Extension<Task>,
767
+ Extension(task_attempt): Extension<TaskAttempt>,
768
+ Query(query): Query<DeleteFileQuery>,
769
+ State(app_state): State<AppState>,
770
+ ) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
771
+ match TaskAttempt::delete_file(
772
+ &app_state.db_pool,
773
+ task_attempt.id,
774
+ task.id,
775
+ project.id,
776
+ &query.file_path,
777
+ )
778
+ .await
779
+ {
780
+ Ok(_commit_id) => Ok(ResponseJson(ApiResponse::success(()))),
781
+ Err(e) => {
782
+ tracing::error!(
783
+ "Failed to delete file '{}' from task attempt {}: {}",
784
+ query.file_path,
785
+ task_attempt.id,
786
+ e
787
+ );
788
+ Ok(ResponseJson(ApiResponse::error(&e.to_string())))
789
+ }
790
+ }
791
+ }
792
+
793
+ pub async fn create_followup_attempt(
794
+ Extension(project): Extension<Project>,
795
+ Extension(task): Extension<Task>,
796
+ Extension(task_attempt): Extension<TaskAttempt>,
797
+ State(app_state): State<AppState>,
798
+ Json(payload): Json<CreateFollowUpAttempt>,
799
+ ) -> Result<ResponseJson<ApiResponse<FollowUpResponse>>, StatusCode> {
800
+ // Start follow-up execution synchronously to catch errors
801
+ match TaskAttempt::start_followup_execution(
802
+ &app_state.db_pool,
803
+ &app_state,
804
+ task_attempt.id,
805
+ task.id,
806
+ project.id,
807
+ &payload.prompt,
808
+ )
809
+ .await
810
+ {
811
+ Ok(actual_attempt_id) => {
812
+ let created_new_attempt = actual_attempt_id != task_attempt.id;
813
+ let message = if created_new_attempt {
814
+ format!(
815
+ "Follow-up execution started on new attempt {} (original worktree was deleted)",
816
+ actual_attempt_id
817
+ )
818
+ } else {
819
+ "Follow-up execution started successfully".to_string()
820
+ };
821
+
822
+ Ok(ResponseJson(ApiResponse::success(FollowUpResponse {
823
+ message: message.clone(),
824
+ actual_attempt_id,
825
+ created_new_attempt,
826
+ })))
827
+ }
828
+ Err(e) => {
829
+ tracing::error!(
830
+ "Failed to start follow-up execution for task attempt {}: {}",
831
+ task_attempt.id,
832
+ e
833
+ );
834
+ Err(StatusCode::INTERNAL_SERVER_ERROR)
835
+ }
836
+ }
837
+ }
838
+
839
+ pub async fn start_dev_server(
840
+ Extension(project): Extension<Project>,
841
+ Extension(task): Extension<Task>,
842
+ Extension(task_attempt): Extension<TaskAttempt>,
843
+ State(app_state): State<AppState>,
844
+ ) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
845
+ // Stop any existing dev servers for this project
846
+ let existing_dev_servers =
847
+ match ExecutionProcess::find_running_dev_servers_by_project(&app_state.db_pool, project.id)
848
+ .await
849
+ {
850
+ Ok(servers) => servers,
851
+ Err(e) => {
852
+ tracing::error!(
853
+ "Failed to find running dev servers for project {}: {}",
854
+ project.id,
855
+ e
856
+ );
857
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
858
+ }
859
+ };
860
+
861
+ for dev_server in existing_dev_servers {
862
+ tracing::info!(
863
+ "Stopping existing dev server {} for project {}",
864
+ dev_server.id,
865
+ project.id
866
+ );
867
+
868
+ // Stop the running process
869
+ if let Err(e) = app_state.stop_running_execution_by_id(dev_server.id).await {
870
+ tracing::error!("Failed to stop dev server {}: {}", dev_server.id, e);
871
+ } else {
872
+ // Update the execution process status in the database
873
+ if let Err(e) = ExecutionProcess::update_completion(
874
+ &app_state.db_pool,
875
+ dev_server.id,
876
+ crate::models::execution_process::ExecutionProcessStatus::Killed,
877
+ None,
878
+ )
879
+ .await
880
+ {
881
+ tracing::error!(
882
+ "Failed to update dev server {} status: {}",
883
+ dev_server.id,
884
+ e
885
+ );
886
+ }
887
+ }
888
+ }
889
+
890
+ // Start dev server execution
891
+ match TaskAttempt::start_dev_server(
892
+ &app_state.db_pool,
893
+ &app_state,
894
+ task_attempt.id,
895
+ task.id,
896
+ project.id,
897
+ )
898
+ .await
899
+ {
900
+ Ok(_) => Ok(ResponseJson(ApiResponse::success(()))),
901
+ Err(e) => {
902
+ tracing::error!(
903
+ "Failed to start dev server for task attempt {}: {}",
904
+ task_attempt.id,
905
+ e
906
+ );
907
+ Ok(ResponseJson(ApiResponse::error(&e.to_string())))
908
+ }
909
+ }
910
+ }
911
+
912
+ pub async fn get_task_attempt_execution_state(
913
+ Extension(project): Extension<Project>,
914
+ Extension(task): Extension<Task>,
915
+ Extension(task_attempt): Extension<TaskAttempt>,
916
+ State(app_state): State<AppState>,
917
+ ) -> Result<ResponseJson<ApiResponse<TaskAttemptState>>, StatusCode> {
918
+ // Get the execution state
919
+ match TaskAttempt::get_execution_state(&app_state.db_pool, task_attempt.id, task.id, project.id)
920
+ .await
921
+ {
922
+ Ok(state) => Ok(ResponseJson(ApiResponse::success(state))),
923
+ Err(e) => {
924
+ tracing::error!(
925
+ "Failed to get execution state for task attempt {}: {}",
926
+ task_attempt.id,
927
+ e
928
+ );
929
+ Err(StatusCode::INTERNAL_SERVER_ERROR)
930
+ }
931
+ }
932
+ }
933
+
934
+ /// Find plan content with context by searching through multiple processes in the same attempt
935
+ async fn find_plan_content_with_context(
936
+ pool: &SqlitePool,
937
+ attempt_id: Uuid,
938
+ ) -> Result<String, StatusCode> {
939
+ // Get all execution processes for this attempt
940
+ let execution_processes =
941
+ match ExecutionProcess::find_by_task_attempt_id(pool, attempt_id).await {
942
+ Ok(processes) => processes,
943
+ Err(e) => {
944
+ tracing::error!(
945
+ "Failed to fetch execution processes for attempt {}: {}",
946
+ attempt_id,
947
+ e
948
+ );
949
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
950
+ }
951
+ };
952
+
953
+ // Look for claudeplan processes (most recent first)
954
+ for claudeplan_process in execution_processes
955
+ .iter()
956
+ .rev()
957
+ .filter(|p| p.executor_type.as_deref() == Some("claude-plan"))
958
+ {
959
+ if let Some(stdout) = &claudeplan_process.stdout {
960
+ if !stdout.trim().is_empty() {
961
+ // Create executor and normalize logs
962
+ let executor_config = ExecutorConfig::ClaudePlan;
963
+ let executor = executor_config.create_executor();
964
+
965
+ // Use working directory for normalization
966
+ let working_dir_path =
967
+ match std::fs::canonicalize(&claudeplan_process.working_directory) {
968
+ Ok(canonical_path) => canonical_path.to_string_lossy().to_string(),
969
+ Err(_) => claudeplan_process.working_directory.clone(),
970
+ };
971
+
972
+ // Normalize logs and extract plan content
973
+ match executor.normalize_logs(stdout, &working_dir_path) {
974
+ Ok(normalized_conversation) => {
975
+ // Search for plan content in the normalized conversation
976
+ if let Some(plan_content) = normalized_conversation
977
+ .entries
978
+ .iter()
979
+ .rev()
980
+ .find_map(|entry| {
981
+ if let NormalizedEntryType::ToolUse {
982
+ action_type: ActionType::PlanPresentation { plan },
983
+ ..
984
+ } = &entry.entry_type
985
+ {
986
+ Some(plan.clone())
987
+ } else {
988
+ None
989
+ }
990
+ })
991
+ {
992
+ return Ok(plan_content);
993
+ }
994
+ }
995
+ Err(_) => {
996
+ continue;
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+
1003
+ tracing::error!(
1004
+ "No claudeplan content found in any process in attempt {}",
1005
+ attempt_id
1006
+ );
1007
+ Err(StatusCode::NOT_FOUND)
1008
+ }
1009
+
1010
+ pub async fn approve_plan(
1011
+ Extension(project): Extension<Project>,
1012
+ Extension(task): Extension<Task>,
1013
+ Extension(task_attempt): Extension<TaskAttempt>,
1014
+ State(app_state): State<AppState>,
1015
+ ) -> Result<ResponseJson<ApiResponse<FollowUpResponse>>, StatusCode> {
1016
+ let current_task = &task;
1017
+
1018
+ // Find plan content with context across the task hierarchy
1019
+ let plan_content = find_plan_content_with_context(&app_state.db_pool, task_attempt.id).await?;
1020
+
1021
+ use crate::models::task::CreateTask;
1022
+ let new_task_id = Uuid::new_v4();
1023
+ let create_task_data = CreateTask {
1024
+ project_id: project.id,
1025
+ title: format!("Execute Plan: {}", current_task.title),
1026
+ description: Some(plan_content),
1027
+ wish_id: current_task.wish_id.clone(),
1028
+ parent_task_attempt: Some(task_attempt.id),
1029
+ };
1030
+
1031
+ let new_task = match Task::create(&app_state.db_pool, &create_task_data, new_task_id).await {
1032
+ Ok(task) => task,
1033
+ Err(e) => {
1034
+ tracing::error!("Failed to create new task: {}", e);
1035
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
1036
+ }
1037
+ };
1038
+
1039
+ // Mark original task as completed since it now has children
1040
+ if let Err(e) =
1041
+ Task::update_status(&app_state.db_pool, task.id, project.id, TaskStatus::Done).await
1042
+ {
1043
+ tracing::error!("Failed to update original task status to Done: {}", e);
1044
+ return Err(StatusCode::INTERNAL_SERVER_ERROR);
1045
+ } else {
1046
+ tracing::info!(
1047
+ "Original task {} marked as Done after plan approval (has children)",
1048
+ task.id
1049
+ );
1050
+ }
1051
+
1052
+ Ok(ResponseJson(ApiResponse::success(FollowUpResponse {
1053
+ message: format!("Plan approved and new task created: {}", new_task.title),
1054
+ actual_attempt_id: new_task_id, // Return the new task ID
1055
+ created_new_attempt: true,
1056
+ })))
1057
+ }
1058
+
1059
+ pub async fn get_task_attempt_details(
1060
+ Extension(task_attempt): Extension<TaskAttempt>,
1061
+ ) -> Result<ResponseJson<ApiResponse<TaskAttempt>>, StatusCode> {
1062
+ Ok(ResponseJson(ApiResponse::success(task_attempt)))
1063
+ }
1064
+
1065
+ pub async fn get_task_attempt_children(
1066
+ Extension(task_attempt): Extension<TaskAttempt>,
1067
+ Extension(project): Extension<Project>,
1068
+ State(app_state): State<AppState>,
1069
+ ) -> Result<ResponseJson<ApiResponse<Vec<Task>>>, StatusCode> {
1070
+ match Task::find_related_tasks_by_attempt_id(&app_state.db_pool, task_attempt.id, project.id)
1071
+ .await
1072
+ {
1073
+ Ok(related_tasks) => Ok(ResponseJson(ApiResponse::success(related_tasks))),
1074
+ Err(e) => {
1075
+ tracing::error!(
1076
+ "Failed to fetch children for task attempt {}: {}",
1077
+ task_attempt.id,
1078
+ e
1079
+ );
1080
+ Err(StatusCode::INTERNAL_SERVER_ERROR)
1081
+ }
1082
+ }
1083
+ }
1084
+
1085
+ pub fn task_attempts_list_router(_state: AppState) -> Router<AppState> {
1086
+ Router::new().route(
1087
+ "/projects/:project_id/tasks/:task_id/attempts",
1088
+ get(get_task_attempts).post(create_task_attempt),
1089
+ )
1090
+ }
1091
+
1092
+ pub fn task_attempts_with_id_router(_state: AppState) -> Router<AppState> {
1093
+ use axum::routing::post;
1094
+
1095
+ Router::new()
1096
+ .route(
1097
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/diff",
1098
+ get(get_task_attempt_diff),
1099
+ )
1100
+ .route(
1101
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/merge",
1102
+ post(merge_task_attempt),
1103
+ )
1104
+ .route(
1105
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/branch-status",
1106
+ get(get_task_attempt_branch_status),
1107
+ )
1108
+ .route(
1109
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/rebase",
1110
+ post(rebase_task_attempt),
1111
+ )
1112
+ .route(
1113
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/open-editor",
1114
+ post(open_task_attempt_in_editor),
1115
+ )
1116
+ .route(
1117
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/delete-file",
1118
+ post(delete_task_attempt_file),
1119
+ )
1120
+ .route(
1121
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/create-pr",
1122
+ post(create_github_pr),
1123
+ )
1124
+ .route(
1125
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/execution-processes",
1126
+ get(get_task_attempt_execution_processes),
1127
+ )
1128
+ .route(
1129
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/stop",
1130
+ post(stop_all_execution_processes),
1131
+ )
1132
+ .merge(
1133
+ Router::new()
1134
+ .route(
1135
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/execution-processes/:process_id/stop",
1136
+ post(stop_execution_process),
1137
+ )
1138
+ .route_layer(from_fn_with_state(_state.clone(), load_execution_process_with_context_middleware))
1139
+ )
1140
+ .route(
1141
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/logs",
1142
+ get(get_task_attempt_all_logs),
1143
+ )
1144
+ .route(
1145
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/follow-up",
1146
+ post(create_followup_attempt),
1147
+ )
1148
+ .route(
1149
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/start-dev-server",
1150
+ post(start_dev_server),
1151
+ )
1152
+ .route(
1153
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id",
1154
+ get(get_task_attempt_execution_state),
1155
+ )
1156
+ .route(
1157
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/approve-plan",
1158
+ post(approve_plan),
1159
+ )
1160
+ .route(
1161
+ "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/children",
1162
+ get(get_task_attempt_children),
1163
+ )
1164
+ .merge(
1165
+ Router::new()
1166
+ .route(
1167
+ "/attempts/:attempt_id/details",
1168
+ get(get_task_attempt_details),
1169
+ )
1170
+ .route_layer(from_fn_with_state(_state.clone(), load_task_attempt_middleware))
1171
+ )
1172
+ }