automagik-forge 0.1.13 → 0.1.14

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/README.md +143 -447
  2. package/dist/linux-x64/automagik-forge-mcp.zip +0 -0
  3. package/{npx-cli/automagik-forge-0.0.55.tgz → dist/linux-x64/automagik-forge.zip} +0 -0
  4. package/package.json +13 -23
  5. package/.cargo/config.toml +0 -13
  6. package/.claude/commands/commit.md +0 -376
  7. package/.claude/commands/prompt.md +0 -871
  8. package/.env.example +0 -20
  9. package/.github/actions/setup-node/action.yml +0 -29
  10. package/.github/images/automagik-logo.png +0 -0
  11. package/.github/workflows/pre-release.yml +0 -470
  12. package/.github/workflows/publish.yml +0 -145
  13. package/.github/workflows/test.yml +0 -63
  14. package/.mcp.json +0 -57
  15. package/AGENT.md +0 -40
  16. package/CLAUDE.md +0 -40
  17. package/CODE-OF-CONDUCT.md +0 -89
  18. package/Cargo.toml +0 -19
  19. package/Dockerfile +0 -43
  20. package/LICENSE +0 -201
  21. package/Makefile +0 -97
  22. package/backend/.sqlx/query-01b7e2bac1261d8be3d03c03df3e5220590da6c31c77f161074fc62752d63881.json +0 -12
  23. package/backend/.sqlx/query-03f2b02ba6dc5ea2b3cf6b1004caea0ad6bcc10ebd63f441d321a389f026e263.json +0 -12
  24. package/backend/.sqlx/query-0923b77d137a29fc54d399a873ff15fc4af894490bc65a4d344a7575cb0d8643.json +0 -12
  25. package/backend/.sqlx/query-0f808bcdb63c5f180836e448dd64c435c51758b2fc54a52ce9e67495b1ab200e.json +0 -68
  26. package/backend/.sqlx/query-1268afe9ca849daa6722e3df7ca8e9e61f0d37052e782bb5452ab8e1018d9b63.json +0 -12
  27. package/backend/.sqlx/query-1b082630a9622f8667ee7a9aba2c2d3176019a68c6bb83d33008594821415a57.json +0 -12
  28. package/backend/.sqlx/query-1c7b06ba1e112abf6b945a2ff08a0b40ec23f3738c2e7399f067b558cf8d490e.json +0 -12
  29. package/backend/.sqlx/query-1f619f01f46859a64ded531dd0ef61abacfe62e758abe7030a6aa745140b95ca.json +0 -104
  30. package/backend/.sqlx/query-1fca1ce14b4b20205364cd1f1f45ebe1d2e30cd745e59e189d56487b5639dfbb.json +0 -12
  31. package/backend/.sqlx/query-212828320e8d871ab9d83705a040b23bcf0393dc7252177fc539a74657f578ef.json +0 -32
  32. package/backend/.sqlx/query-290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b.json +0 -56
  33. package/backend/.sqlx/query-2b471d2c2e8ffbe0cd42d2a91b814c0d79f9d09200f147e3cea33ba4ce673c8a.json +0 -68
  34. package/backend/.sqlx/query-354a48c705bb9bb2048c1b7f10fcb714e23f9db82b7a4ea6932486197b2ede6a.json +0 -92
  35. package/backend/.sqlx/query-36c9e3dd10648e94b949db5c91a774ecb1e10a899ef95da74066eccedca4d8b2.json +0 -12
  36. package/backend/.sqlx/query-36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94.json +0 -56
  37. package/backend/.sqlx/query-3a5b3c98a55ca183ab20c74708e3d7e579dda37972c059e7515c4ceee4bd8dd3.json +0 -62
  38. package/backend/.sqlx/query-3d0a1cabf2a52e9d90cdfd29c509ca89aeb448d0c1d2446c65cd43db40735e86.json +0 -62
  39. package/backend/.sqlx/query-3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031.json +0 -56
  40. package/backend/.sqlx/query-4049ca413b285a05aca6b25385e9c8185575f01e9069e4e8581aa45d713f612f.json +0 -32
  41. package/backend/.sqlx/query-412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1.json +0 -104
  42. package/backend/.sqlx/query-417a8b1ff4e51de82aea0159a3b97932224dc325b23476cb84153d690227fd8b.json +0 -62
  43. package/backend/.sqlx/query-461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2.json +0 -56
  44. package/backend/.sqlx/query-58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227.json +0 -92
  45. package/backend/.sqlx/query-5a886026d75d515c01f347cc203c8d99dd04c61dc468e2e4c5aa548436d13834.json +0 -62
  46. package/backend/.sqlx/query-5b902137b11022d2e1a5c4f6a9c83fec1a856c6a710aff831abd2382ede76b43.json +0 -12
  47. package/backend/.sqlx/query-5ed1238e52e59bb5f76c0f153fd99a14093f7ce2585bf9843585608f17ec575b.json +0 -104
  48. package/backend/.sqlx/query-6e8b860b14decfc2227dc57213f38442943d3fbef5c8418fd6b634c6e0f5e2ea.json +0 -104
  49. package/backend/.sqlx/query-6ec414276994c4ccb2433eaa5b1b342168557d17ddf5a52dac84cb1b59b9de8f.json +0 -68
  50. package/backend/.sqlx/query-6ecfa16d0cf825aacf233544b5baf151e9adfdca26c226ad71020d291fd802d5.json +0 -62
  51. package/backend/.sqlx/query-72509d252c39fce77520aa816cb2acbc1fb35dc2605e7be893610599b2427f2e.json +0 -62
  52. package/backend/.sqlx/query-75239b2da188f749707d77f3c1544332ca70db3d6d6743b2601dc0d167536437.json +0 -62
  53. package/backend/.sqlx/query-83d10e29f8478aff33434f9ac67068e013b888b953a2657e2bb72a6f619d04f2.json +0 -50
  54. package/backend/.sqlx/query-8610803360ea18b9b9d078a6981ea56abfbfe84e6354fc1d5ae4c622e01410ed.json +0 -68
  55. package/backend/.sqlx/query-86d03eb70eef39c59296416867f2ee66c9f7cd8b7f961fbda2f89fc0a1c442c2.json +0 -12
  56. package/backend/.sqlx/query-87d0feb5a6b442bad9c60068ea7569599cc6fc91a0e2692ecb42e93b03201b9d.json +0 -68
  57. package/backend/.sqlx/query-8a67b3b3337248f06a57bdf8a908f7ef23177431eaed82dc08c94c3e5944340e.json +0 -12
  58. package/backend/.sqlx/query-8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3.json +0 -12
  59. package/backend/.sqlx/query-90fd607fcb2dca72239ff25e618e21e174b195991eaa33722cbf5f76da84cfab.json +0 -62
  60. package/backend/.sqlx/query-92e8bdbcd80c5ff3db7a35cf79492048803ef305cbdef0d0a1fe5dc881ca8c71.json +0 -104
  61. package/backend/.sqlx/query-93a1605f90e9672dad29b472b6ad85fa9a55ea3ffa5abcb8724b09d61be254ca.json +0 -20
  62. package/backend/.sqlx/query-9472c8fb477958167f5fae40b85ac44252468c5226b2cdd7770f027332eed6d7.json +0 -104
  63. package/backend/.sqlx/query-96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2.json +0 -56
  64. package/backend/.sqlx/query-9edb2c01e91fd0f0fe7b56e988c7ae0393150f50be3f419a981e035c0121dfc7.json +0 -104
  65. package/backend/.sqlx/query-a157cf00616f703bfba21927f1eb1c9eec2a81c02da15f66efdba0b6c375de1b.json +0 -26
  66. package/backend/.sqlx/query-a31fff84f3b8e532fd1160447d89d700f06ae08821fee00c9a5b60492b05259c.json +0 -62
  67. package/backend/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json +0 -12
  68. package/backend/.sqlx/query-a6d2961718dbc3b1a925e549f49a159c561bef58c105529275f274b27e2eba5b.json +0 -104
  69. package/backend/.sqlx/query-a9e93d5b09b29faf66e387e4d7596a792d81e75c4d3726e83c2963e8d7c9b56f.json +0 -104
  70. package/backend/.sqlx/query-ac5247c8d7fb86e4650c4b0eb9420031614c831b7b085083bac20c1af314c538.json +0 -12
  71. package/backend/.sqlx/query-afef9467be74c411c4cb119a8b2b1aea53049877dfc30cc60b486134ba4b4c9f.json +0 -68
  72. package/backend/.sqlx/query-b2b2c6b4d0b1a347b5c4cb63c3a46a265d4db53be9554989a814b069d0af82f2.json +0 -62
  73. package/backend/.sqlx/query-c50d2ff0b12e5bcc81e371089ee2d007e233e7db93aefba4fef08e7aa68f5ab7.json +0 -20
  74. package/backend/.sqlx/query-c614e6056b244ca07f1b9d44e7edc9d5819225c6f8d9e077070c6e518a17f50b.json +0 -12
  75. package/backend/.sqlx/query-c67259be8bf4ee0cfd32167b2aa3b7fe9192809181a8171bf1c2d6df731967ae.json +0 -12
  76. package/backend/.sqlx/query-d2d0a1b985ebbca6a2b3e882a221a219f3199890fa640afc946ef1a792d6d8de.json +0 -12
  77. package/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json +0 -20
  78. package/backend/.sqlx/query-d3b9ea1de1576af71b312924ce7f4ea8ae5dbe2ac138ea3b4470f2d5cd734846.json +0 -12
  79. package/backend/.sqlx/query-ed8456646fa69ddd412441955f06ff22bfb790f29466450735e0b8bb1bc4ec94.json +0 -12
  80. package/backend/Cargo.toml +0 -71
  81. package/backend/build.rs +0 -32
  82. package/backend/migrations/20250617183714_init.sql +0 -44
  83. package/backend/migrations/20250620212427_execution_processes.sql +0 -25
  84. package/backend/migrations/20250620214100_remove_stdout_stderr_from_task_attempts.sql +0 -28
  85. package/backend/migrations/20250621120000_relate_activities_to_execution_processes.sql +0 -23
  86. package/backend/migrations/20250623120000_executor_sessions.sql +0 -17
  87. package/backend/migrations/20250623130000_add_executor_type_to_execution_processes.sql +0 -4
  88. package/backend/migrations/20250625000000_add_dev_script_to_projects.sql +0 -4
  89. package/backend/migrations/20250701000000_add_branch_to_task_attempts.sql +0 -2
  90. package/backend/migrations/20250701000001_add_pr_tracking_to_task_attempts.sql +0 -5
  91. package/backend/migrations/20250701120000_add_assistant_message_to_executor_sessions.sql +0 -2
  92. package/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql +0 -2
  93. package/backend/migrations/20250709000000_add_worktree_deleted_flag.sql +0 -2
  94. package/backend/migrations/20250710000000_add_setup_completion.sql +0 -3
  95. package/backend/migrations/20250715154859_add_task_templates.sql +0 -25
  96. package/backend/migrations/20250716143725_add_default_templates.sql +0 -174
  97. package/backend/migrations/20250716161432_update_executor_names_to_kebab_case.sql +0 -20
  98. package/backend/migrations/20250716170000_add_parent_task_to_tasks.sql +0 -7
  99. package/backend/migrations/20250717000000_drop_task_attempt_activities.sql +0 -9
  100. package/backend/migrations/20250719000000_add_cleanup_script_to_projects.sql +0 -2
  101. package/backend/migrations/20250720000000_add_cleanupscript_to_process_type_constraint.sql +0 -25
  102. package/backend/migrations/20250723000000_add_wish_to_tasks.sql +0 -7
  103. package/backend/migrations/20250724000000_remove_unique_wish_constraint.sql +0 -5
  104. package/backend/scripts/toast-notification.ps1 +0 -23
  105. package/backend/sounds/abstract-sound1.wav +0 -0
  106. package/backend/sounds/abstract-sound2.wav +0 -0
  107. package/backend/sounds/abstract-sound3.wav +0 -0
  108. package/backend/sounds/abstract-sound4.wav +0 -0
  109. package/backend/sounds/cow-mooing.wav +0 -0
  110. package/backend/sounds/phone-vibration.wav +0 -0
  111. package/backend/sounds/rooster.wav +0 -0
  112. package/backend/src/app_state.rs +0 -218
  113. package/backend/src/bin/generate_types.rs +0 -189
  114. package/backend/src/bin/mcp_task_server.rs +0 -191
  115. package/backend/src/execution_monitor.rs +0 -1193
  116. package/backend/src/executor.rs +0 -1053
  117. package/backend/src/executors/amp.rs +0 -697
  118. package/backend/src/executors/ccr.rs +0 -91
  119. package/backend/src/executors/charm_opencode.rs +0 -113
  120. package/backend/src/executors/claude.rs +0 -887
  121. package/backend/src/executors/cleanup_script.rs +0 -124
  122. package/backend/src/executors/dev_server.rs +0 -53
  123. package/backend/src/executors/echo.rs +0 -79
  124. package/backend/src/executors/gemini/config.rs +0 -67
  125. package/backend/src/executors/gemini/streaming.rs +0 -363
  126. package/backend/src/executors/gemini.rs +0 -765
  127. package/backend/src/executors/mod.rs +0 -23
  128. package/backend/src/executors/opencode_ai.rs +0 -113
  129. package/backend/src/executors/setup_script.rs +0 -130
  130. package/backend/src/executors/sst_opencode/filter.rs +0 -184
  131. package/backend/src/executors/sst_opencode/tools.rs +0 -139
  132. package/backend/src/executors/sst_opencode.rs +0 -756
  133. package/backend/src/lib.rs +0 -45
  134. package/backend/src/main.rs +0 -324
  135. package/backend/src/mcp/mod.rs +0 -1
  136. package/backend/src/mcp/task_server.rs +0 -850
  137. package/backend/src/middleware/mod.rs +0 -3
  138. package/backend/src/middleware/model_loaders.rs +0 -242
  139. package/backend/src/models/api_response.rs +0 -36
  140. package/backend/src/models/config.rs +0 -375
  141. package/backend/src/models/execution_process.rs +0 -430
  142. package/backend/src/models/executor_session.rs +0 -225
  143. package/backend/src/models/mod.rs +0 -12
  144. package/backend/src/models/project.rs +0 -356
  145. package/backend/src/models/task.rs +0 -345
  146. package/backend/src/models/task_attempt.rs +0 -1214
  147. package/backend/src/models/task_template.rs +0 -146
  148. package/backend/src/openapi.rs +0 -93
  149. package/backend/src/routes/auth.rs +0 -297
  150. package/backend/src/routes/config.rs +0 -385
  151. package/backend/src/routes/filesystem.rs +0 -228
  152. package/backend/src/routes/health.rs +0 -16
  153. package/backend/src/routes/mod.rs +0 -9
  154. package/backend/src/routes/projects.rs +0 -562
  155. package/backend/src/routes/stream.rs +0 -244
  156. package/backend/src/routes/task_attempts.rs +0 -1172
  157. package/backend/src/routes/task_templates.rs +0 -229
  158. package/backend/src/routes/tasks.rs +0 -353
  159. package/backend/src/services/analytics.rs +0 -216
  160. package/backend/src/services/git_service.rs +0 -1321
  161. package/backend/src/services/github_service.rs +0 -307
  162. package/backend/src/services/mod.rs +0 -13
  163. package/backend/src/services/notification_service.rs +0 -263
  164. package/backend/src/services/pr_monitor.rs +0 -214
  165. package/backend/src/services/process_service.rs +0 -940
  166. package/backend/src/utils/path.rs +0 -96
  167. package/backend/src/utils/shell.rs +0 -19
  168. package/backend/src/utils/text.rs +0 -24
  169. package/backend/src/utils/worktree_manager.rs +0 -578
  170. package/backend/src/utils.rs +0 -125
  171. package/backend/test.db +0 -0
  172. package/build-npm-package.sh +0 -61
  173. package/dev_assets_seed/config.json +0 -19
  174. package/frontend/.eslintrc.json +0 -25
  175. package/frontend/.prettierrc.json +0 -8
  176. package/frontend/components.json +0 -17
  177. package/frontend/index.html +0 -19
  178. package/frontend/package-lock.json +0 -7321
  179. package/frontend/package.json +0 -61
  180. package/frontend/postcss.config.js +0 -6
  181. package/frontend/public/android-chrome-192x192.png +0 -0
  182. package/frontend/public/android-chrome-512x512.png +0 -0
  183. package/frontend/public/apple-touch-icon.png +0 -0
  184. package/frontend/public/automagik-forge-logo-dark.svg +0 -3
  185. package/frontend/public/automagik-forge-logo.svg +0 -3
  186. package/frontend/public/automagik-forge-screenshot-overview.png +0 -0
  187. package/frontend/public/favicon-16x16.png +0 -0
  188. package/frontend/public/favicon-32x32.png +0 -0
  189. package/frontend/public/favicon.ico +0 -0
  190. package/frontend/public/site.webmanifest +0 -1
  191. package/frontend/public/viba-kanban-favicon.png +0 -0
  192. package/frontend/src/App.tsx +0 -157
  193. package/frontend/src/components/DisclaimerDialog.tsx +0 -106
  194. package/frontend/src/components/GitHubLoginDialog.tsx +0 -314
  195. package/frontend/src/components/OnboardingDialog.tsx +0 -185
  196. package/frontend/src/components/PrivacyOptInDialog.tsx +0 -130
  197. package/frontend/src/components/ProvidePatDialog.tsx +0 -98
  198. package/frontend/src/components/TaskTemplateManager.tsx +0 -336
  199. package/frontend/src/components/config-provider.tsx +0 -119
  200. package/frontend/src/components/context/TaskDetailsContextProvider.tsx +0 -470
  201. package/frontend/src/components/context/taskDetailsContext.ts +0 -125
  202. package/frontend/src/components/keyboard-shortcuts-demo.tsx +0 -35
  203. package/frontend/src/components/layout/navbar.tsx +0 -86
  204. package/frontend/src/components/logo.tsx +0 -44
  205. package/frontend/src/components/projects/ProjectCard.tsx +0 -155
  206. package/frontend/src/components/projects/project-detail.tsx +0 -251
  207. package/frontend/src/components/projects/project-form-fields.tsx +0 -238
  208. package/frontend/src/components/projects/project-form.tsx +0 -301
  209. package/frontend/src/components/projects/project-list.tsx +0 -200
  210. package/frontend/src/components/projects/projects-page.tsx +0 -20
  211. package/frontend/src/components/tasks/BranchSelector.tsx +0 -169
  212. package/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx +0 -94
  213. package/frontend/src/components/tasks/EditorSelectionDialog.tsx +0 -119
  214. package/frontend/src/components/tasks/TaskCard.tsx +0 -154
  215. package/frontend/src/components/tasks/TaskDetails/CollapsibleToolbar.tsx +0 -33
  216. package/frontend/src/components/tasks/TaskDetails/DiffCard.tsx +0 -109
  217. package/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx +0 -135
  218. package/frontend/src/components/tasks/TaskDetails/DiffFile.tsx +0 -296
  219. package/frontend/src/components/tasks/TaskDetails/DiffTab.tsx +0 -32
  220. package/frontend/src/components/tasks/TaskDetails/DisplayConversationEntry.tsx +0 -392
  221. package/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx +0 -256
  222. package/frontend/src/components/tasks/TaskDetails/LogsTab/ConversationEntry.tsx +0 -56
  223. package/frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx +0 -92
  224. package/frontend/src/components/tasks/TaskDetails/LogsTab/Prompt.tsx +0 -22
  225. package/frontend/src/components/tasks/TaskDetails/LogsTab/SetupScriptRunning.tsx +0 -49
  226. package/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +0 -186
  227. package/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +0 -288
  228. package/frontend/src/components/tasks/TaskDetails/RelatedTasksTab.tsx +0 -216
  229. package/frontend/src/components/tasks/TaskDetails/TabNavigation.tsx +0 -93
  230. package/frontend/src/components/tasks/TaskDetailsHeader.tsx +0 -169
  231. package/frontend/src/components/tasks/TaskDetailsPanel.tsx +0 -126
  232. package/frontend/src/components/tasks/TaskDetailsToolbar.tsx +0 -302
  233. package/frontend/src/components/tasks/TaskFollowUpSection.tsx +0 -130
  234. package/frontend/src/components/tasks/TaskFormDialog.tsx +0 -400
  235. package/frontend/src/components/tasks/TaskKanbanBoard.tsx +0 -180
  236. package/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx +0 -259
  237. package/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx +0 -243
  238. package/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx +0 -899
  239. package/frontend/src/components/tasks/index.ts +0 -2
  240. package/frontend/src/components/theme-provider.tsx +0 -82
  241. package/frontend/src/components/theme-toggle.tsx +0 -36
  242. package/frontend/src/components/ui/alert.tsx +0 -59
  243. package/frontend/src/components/ui/auto-expanding-textarea.tsx +0 -70
  244. package/frontend/src/components/ui/badge.tsx +0 -36
  245. package/frontend/src/components/ui/button.tsx +0 -56
  246. package/frontend/src/components/ui/card.tsx +0 -86
  247. package/frontend/src/components/ui/checkbox.tsx +0 -44
  248. package/frontend/src/components/ui/chip.tsx +0 -25
  249. package/frontend/src/components/ui/dialog.tsx +0 -124
  250. package/frontend/src/components/ui/dropdown-menu.tsx +0 -198
  251. package/frontend/src/components/ui/file-search-textarea.tsx +0 -292
  252. package/frontend/src/components/ui/folder-picker.tsx +0 -279
  253. package/frontend/src/components/ui/input.tsx +0 -25
  254. package/frontend/src/components/ui/label.tsx +0 -24
  255. package/frontend/src/components/ui/loader.tsx +0 -26
  256. package/frontend/src/components/ui/markdown-renderer.tsx +0 -75
  257. package/frontend/src/components/ui/select.tsx +0 -160
  258. package/frontend/src/components/ui/separator.tsx +0 -31
  259. package/frontend/src/components/ui/shadcn-io/kanban/index.tsx +0 -185
  260. package/frontend/src/components/ui/table.tsx +0 -117
  261. package/frontend/src/components/ui/tabs.tsx +0 -53
  262. package/frontend/src/components/ui/textarea.tsx +0 -22
  263. package/frontend/src/components/ui/tooltip.tsx +0 -28
  264. package/frontend/src/hooks/useNormalizedConversation.ts +0 -440
  265. package/frontend/src/index.css +0 -225
  266. package/frontend/src/lib/api.ts +0 -630
  267. package/frontend/src/lib/keyboard-shortcuts.ts +0 -266
  268. package/frontend/src/lib/responsive-config.ts +0 -70
  269. package/frontend/src/lib/types.ts +0 -39
  270. package/frontend/src/lib/utils.ts +0 -10
  271. package/frontend/src/main.tsx +0 -50
  272. package/frontend/src/pages/McpServers.tsx +0 -418
  273. package/frontend/src/pages/Settings.tsx +0 -610
  274. package/frontend/src/pages/project-tasks.tsx +0 -575
  275. package/frontend/src/pages/projects.tsx +0 -18
  276. package/frontend/src/vite-env.d.ts +0 -1
  277. package/frontend/tailwind.config.js +0 -125
  278. package/frontend/tsconfig.json +0 -26
  279. package/frontend/tsconfig.node.json +0 -10
  280. package/frontend/vite.config.ts +0 -33
  281. package/npx-cli/README.md +0 -159
  282. package/npx-cli/automagik-forge-0.1.0.tgz +0 -0
  283. package/npx-cli/automagik-forge-0.1.10.tgz +0 -0
  284. package/npx-cli/package.json +0 -17
  285. package/npx-cli/vibe-kanban-0.0.55.tgz +0 -0
  286. package/pnpm-workspace.yaml +0 -2
  287. package/rust-toolchain.toml +0 -11
  288. package/rustfmt.toml +0 -3
  289. package/scripts/load-env.js +0 -43
  290. package/scripts/mcp_test.js +0 -374
  291. package/scripts/prepare-db.js +0 -45
  292. package/scripts/setup-dev-environment.js +0 -274
  293. package/scripts/start-mcp-sse.js +0 -70
  294. package/scripts/test-debug.js +0 -32
  295. package/scripts/test-mcp-sse.js +0 -138
  296. package/scripts/test-simple.js +0 -44
  297. package/scripts/test-wish-final.js +0 -179
  298. package/scripts/test-wish-system.js +0 -221
  299. package/shared/types.ts +0 -182
  300. package/test-npm-package.sh +0 -42
  301. /package/{npx-cli/bin → bin}/cli.js +0 -0
@@ -1,1053 +0,0 @@
1
- use std::str::FromStr;
2
-
3
- use async_trait::async_trait;
4
- use serde::{Deserialize, Serialize};
5
- use tokio::io::{AsyncBufReadExt, BufReader};
6
- use ts_rs::TS;
7
- use utoipa::ToSchema;
8
- use uuid::Uuid;
9
-
10
- use crate::executors::{
11
- AmpExecutor, CCRExecutor, CharmOpencodeExecutor, ClaudeExecutor, EchoExecutor, GeminiExecutor,
12
- OpencodeAiExecutor, SetupScriptExecutor, SstOpencodeExecutor,
13
- };
14
-
15
- // Constants for database streaming - fast for near-real-time updates
16
- const STDOUT_UPDATE_THRESHOLD: usize = 1;
17
- const BUFFER_SIZE_THRESHOLD: usize = 256;
18
-
19
- /// Normalized conversation representation for different executor formats
20
- #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
21
- #[ts(export)]
22
- pub struct NormalizedConversation {
23
- pub entries: Vec<NormalizedEntry>,
24
- pub session_id: Option<String>,
25
- pub executor_type: String,
26
- pub prompt: Option<String>,
27
- pub summary: Option<String>,
28
- }
29
-
30
- /// Individual entry in a normalized conversation
31
- #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
32
- #[ts(export)]
33
- pub struct NormalizedEntry {
34
- pub timestamp: Option<String>,
35
- pub entry_type: NormalizedEntryType,
36
- pub content: String,
37
- #[ts(skip)]
38
- pub metadata: Option<serde_json::Value>,
39
- }
40
-
41
- /// Types of entries in a normalized conversation
42
- #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
43
- #[serde(tag = "type", rename_all = "snake_case")]
44
- #[ts(export)]
45
- pub enum NormalizedEntryType {
46
- UserMessage,
47
- AssistantMessage,
48
- ToolUse {
49
- tool_name: String,
50
- action_type: ActionType,
51
- },
52
- SystemMessage,
53
- ErrorMessage,
54
- Thinking,
55
- }
56
-
57
- /// Types of tool actions that can be performed
58
- #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
59
- #[serde(tag = "action", rename_all = "snake_case")]
60
- #[ts(export)]
61
- pub enum ActionType {
62
- FileRead { path: String },
63
- FileWrite { path: String },
64
- CommandRun { command: String },
65
- Search { query: String },
66
- WebFetch { url: String },
67
- TaskCreate { description: String },
68
- PlanPresentation { plan: String },
69
- Other { description: String },
70
- }
71
-
72
- /// Context information for spawn failures to provide comprehensive error details
73
- #[derive(Debug, Clone)]
74
- pub struct SpawnContext {
75
- /// The type of executor that failed (e.g., "Claude", "Amp", "Echo")
76
- pub executor_type: String,
77
- /// The command that failed to spawn
78
- pub command: String,
79
- /// Command line arguments
80
- pub args: Vec<String>,
81
- /// Working directory where the command was executed
82
- pub working_dir: String,
83
- /// Task ID if available
84
- pub task_id: Option<Uuid>,
85
- /// Task title for user-friendly context
86
- pub task_title: Option<String>,
87
- /// Additional executor-specific context
88
- pub additional_context: Option<String>,
89
- }
90
-
91
- impl SpawnContext {
92
- /// Set the executor type (required field not available in Command)
93
- pub fn with_executor_type(mut self, executor_type: impl Into<String>) -> Self {
94
- self.executor_type = executor_type.into();
95
- self
96
- }
97
-
98
- /// Add task context (optional, not available in Command)
99
- pub fn with_task(mut self, task_id: Uuid, task_title: Option<String>) -> Self {
100
- self.task_id = Some(task_id);
101
- self.task_title = task_title;
102
- self
103
- }
104
-
105
- /// Add additional context information (optional, not available in Command)
106
- pub fn with_context(mut self, context: impl Into<String>) -> Self {
107
- self.additional_context = Some(context.into());
108
- self
109
- }
110
-
111
- /// Create SpawnContext from Command, then use builder methods for additional context
112
- pub fn from_command(
113
- command: &tokio::process::Command,
114
- executor_type: impl Into<String>,
115
- ) -> Self {
116
- Self::from(command).with_executor_type(executor_type)
117
- }
118
-
119
- /// Finalize the context and create an ExecutorError
120
- pub fn spawn_error(self, error: std::io::Error) -> ExecutorError {
121
- ExecutorError::spawn_failed(error, self)
122
- }
123
- }
124
-
125
- /// Extract SpawnContext from a tokio::process::Command
126
- /// This automatically captures all available information from the Command object
127
- impl From<&tokio::process::Command> for SpawnContext {
128
- fn from(command: &tokio::process::Command) -> Self {
129
- let program = command.as_std().get_program().to_string_lossy().to_string();
130
- let args = command
131
- .as_std()
132
- .get_args()
133
- .map(|s| s.to_string_lossy().to_string())
134
- .collect();
135
-
136
- let working_dir = command
137
- .as_std()
138
- .get_current_dir()
139
- .map(|p| p.to_string_lossy().to_string())
140
- .unwrap_or_else(|| "current_dir".to_string());
141
-
142
- Self {
143
- executor_type: "Unknown".to_string(), // Must be set using with_executor_type()
144
- command: program,
145
- args,
146
- working_dir,
147
- task_id: None,
148
- task_title: None,
149
- additional_context: None,
150
- }
151
- }
152
- }
153
-
154
- #[derive(Debug)]
155
- pub enum ExecutorError {
156
- SpawnFailed {
157
- error: std::io::Error,
158
- context: SpawnContext,
159
- },
160
- TaskNotFound,
161
- DatabaseError(sqlx::Error),
162
- ContextCollectionFailed(String),
163
- GitError(String),
164
- InvalidSessionId(String),
165
- FollowUpNotSupported,
166
- }
167
-
168
- impl std::fmt::Display for ExecutorError {
169
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170
- match self {
171
- ExecutorError::SpawnFailed { error, context } => {
172
- write!(f, "Failed to spawn {} process", context.executor_type)?;
173
-
174
- // Add task context if available
175
- if let Some(ref title) = context.task_title {
176
- write!(f, " for task '{}'", title)?;
177
- } else if let Some(task_id) = context.task_id {
178
- write!(f, " for task {}", task_id)?;
179
- }
180
-
181
- // Add command details
182
- write!(f, ": command '{}' ", context.command)?;
183
- if !context.args.is_empty() {
184
- write!(f, "with args [{}] ", context.args.join(", "))?;
185
- }
186
-
187
- // Add working directory
188
- write!(f, "in directory '{}' ", context.working_dir)?;
189
-
190
- // Add additional context if provided
191
- if let Some(ref additional) = context.additional_context {
192
- write!(f, "({}) ", additional)?;
193
- }
194
-
195
- // Finally, add the underlying error
196
- write!(f, "- {}", error)
197
- }
198
- ExecutorError::TaskNotFound => write!(f, "Task not found"),
199
- ExecutorError::DatabaseError(e) => write!(f, "Database error: {}", e),
200
- ExecutorError::ContextCollectionFailed(msg) => {
201
- write!(f, "Context collection failed: {}", msg)
202
- }
203
- ExecutorError::GitError(msg) => write!(f, "Git operation error: {}", msg),
204
- ExecutorError::InvalidSessionId(msg) => write!(f, "Invalid session_id: {}", msg),
205
- ExecutorError::FollowUpNotSupported => {
206
- write!(f, "This executor does not support follow-up sessions")
207
- }
208
- }
209
- }
210
- }
211
-
212
- impl std::error::Error for ExecutorError {}
213
-
214
- impl From<sqlx::Error> for ExecutorError {
215
- fn from(err: sqlx::Error) -> Self {
216
- ExecutorError::DatabaseError(err)
217
- }
218
- }
219
-
220
- impl From<crate::models::task_attempt::TaskAttemptError> for ExecutorError {
221
- fn from(err: crate::models::task_attempt::TaskAttemptError) -> Self {
222
- match err {
223
- crate::models::task_attempt::TaskAttemptError::Database(e) => {
224
- ExecutorError::DatabaseError(e)
225
- }
226
- crate::models::task_attempt::TaskAttemptError::Git(e) => {
227
- ExecutorError::GitError(format!("Git operation failed: {}", e))
228
- }
229
- crate::models::task_attempt::TaskAttemptError::TaskNotFound => {
230
- ExecutorError::TaskNotFound
231
- }
232
- crate::models::task_attempt::TaskAttemptError::ProjectNotFound => {
233
- ExecutorError::ContextCollectionFailed("Project not found".to_string())
234
- }
235
- crate::models::task_attempt::TaskAttemptError::ValidationError(msg) => {
236
- ExecutorError::ContextCollectionFailed(format!("Validation failed: {}", msg))
237
- }
238
- crate::models::task_attempt::TaskAttemptError::BranchNotFound(branch) => {
239
- ExecutorError::GitError(format!("Branch '{}' not found", branch))
240
- }
241
- crate::models::task_attempt::TaskAttemptError::GitService(e) => {
242
- ExecutorError::GitError(format!("Git service error: {}", e))
243
- }
244
- crate::models::task_attempt::TaskAttemptError::GitHubService(e) => {
245
- ExecutorError::GitError(format!("GitHub service error: {}", e))
246
- }
247
- }
248
- }
249
- }
250
-
251
- impl ExecutorError {
252
- /// Create a new SpawnFailed error with context
253
- pub fn spawn_failed(error: std::io::Error, context: SpawnContext) -> Self {
254
- ExecutorError::SpawnFailed { error, context }
255
- }
256
- }
257
-
258
- /// Trait for coding agents that can execute tasks, normalize logs, and support follow-up sessions
259
- #[async_trait]
260
- pub trait Executor: Send + Sync {
261
- /// Spawn the command for a given task attempt
262
- async fn spawn(
263
- &self,
264
- pool: &sqlx::SqlitePool,
265
- task_id: Uuid,
266
- worktree_path: &str,
267
- ) -> Result<command_group::AsyncGroupChild, ExecutorError>;
268
-
269
- /// Spawn a follow-up session for executors that support it
270
- ///
271
- /// This method is used to continue an existing session with a new prompt.
272
- /// Not all executors support follow-up sessions, so the default implementation
273
- /// returns an error.
274
- async fn spawn_followup(
275
- &self,
276
- _pool: &sqlx::SqlitePool,
277
- _task_id: Uuid,
278
- _session_id: &str,
279
- _prompt: &str,
280
- _worktree_path: &str,
281
- ) -> Result<command_group::AsyncGroupChild, ExecutorError> {
282
- Err(ExecutorError::FollowUpNotSupported)
283
- }
284
-
285
- /// Normalize executor logs into a standard format
286
- fn normalize_logs(
287
- &self,
288
- _logs: &str,
289
- _worktree_path: &str,
290
- ) -> Result<NormalizedConversation, String> {
291
- // Default implementation returns empty conversation
292
- Ok(NormalizedConversation {
293
- entries: vec![],
294
- session_id: None,
295
- executor_type: "unknown".to_string(),
296
- prompt: None,
297
- summary: None,
298
- })
299
- }
300
-
301
- #[allow(clippy::result_large_err)]
302
- fn setup_streaming(
303
- &self,
304
- child: &mut command_group::AsyncGroupChild,
305
- pool: &sqlx::SqlitePool,
306
- attempt_id: Uuid,
307
- execution_process_id: Uuid,
308
- ) -> Result<(), ExecutorError> {
309
- let stdout = child
310
- .inner()
311
- .stdout
312
- .take()
313
- .expect("Failed to take stdout from child process");
314
- let stderr = child
315
- .inner()
316
- .stderr
317
- .take()
318
- .expect("Failed to take stderr from child process");
319
-
320
- let pool_clone1 = pool.clone();
321
- let pool_clone2 = pool.clone();
322
-
323
- tokio::spawn(stream_output_to_db(
324
- stdout,
325
- pool_clone1,
326
- attempt_id,
327
- execution_process_id,
328
- true,
329
- ));
330
- tokio::spawn(stream_output_to_db(
331
- stderr,
332
- pool_clone2,
333
- attempt_id,
334
- execution_process_id,
335
- false,
336
- ));
337
-
338
- Ok(())
339
- }
340
-
341
- /// Execute the command and stream output to database in real-time
342
- async fn execute_streaming(
343
- &self,
344
- pool: &sqlx::SqlitePool,
345
- task_id: Uuid,
346
- attempt_id: Uuid,
347
- execution_process_id: Uuid,
348
- worktree_path: &str,
349
- ) -> Result<command_group::AsyncGroupChild, ExecutorError> {
350
- let mut child = self.spawn(pool, task_id, worktree_path).await?;
351
- Self::setup_streaming(self, &mut child, pool, attempt_id, execution_process_id)?;
352
- Ok(child)
353
- }
354
-
355
- /// Execute a follow-up command and stream output to database in real-time
356
- #[allow(clippy::too_many_arguments)]
357
- async fn execute_followup_streaming(
358
- &self,
359
- pool: &sqlx::SqlitePool,
360
- task_id: Uuid,
361
- attempt_id: Uuid,
362
- execution_process_id: Uuid,
363
- session_id: &str,
364
- prompt: &str,
365
- worktree_path: &str,
366
- ) -> Result<command_group::AsyncGroupChild, ExecutorError> {
367
- let mut child = self
368
- .spawn_followup(pool, task_id, session_id, prompt, worktree_path)
369
- .await?;
370
- Self::setup_streaming(self, &mut child, pool, attempt_id, execution_process_id)?;
371
- Ok(child)
372
- }
373
- }
374
-
375
- /// Runtime executor types for internal use
376
- #[derive(Debug, Clone)]
377
- pub enum ExecutorType {
378
- SetupScript(String),
379
- CleanupScript(String),
380
- DevServer(String),
381
- CodingAgent {
382
- config: ExecutorConfig,
383
- follow_up: Option<FollowUpInfo>,
384
- },
385
- }
386
-
387
- /// Information needed to continue a previous session
388
- #[derive(Debug, Clone)]
389
- pub struct FollowUpInfo {
390
- pub session_id: String,
391
- pub prompt: String,
392
- }
393
-
394
- /// Configuration for different executor types
395
- #[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
396
- #[serde(tag = "type", rename_all = "kebab-case")]
397
- #[ts(export)]
398
- pub enum ExecutorConfig {
399
- Echo,
400
- Claude,
401
- ClaudePlan,
402
- Amp,
403
- Gemini,
404
- #[serde(alias = "setup_script")]
405
- SetupScript {
406
- script: String,
407
- },
408
- ClaudeCodeRouter,
409
- #[serde(alias = "charmopencode")]
410
- CharmOpencode,
411
- #[serde(alias = "opencode")]
412
- SstOpencode,
413
- OpencodeAi,
414
- }
415
-
416
- // Constants for frontend
417
- #[derive(Debug, Clone, Serialize, Deserialize, TS)]
418
- #[ts(export)]
419
- pub struct ExecutorConstants {
420
- pub executor_types: Vec<ExecutorConfig>,
421
- pub executor_labels: Vec<String>,
422
- }
423
-
424
- impl FromStr for ExecutorConfig {
425
- type Err = String;
426
-
427
- fn from_str(s: &str) -> Result<Self, Self::Err> {
428
- match s {
429
- "echo" => Ok(ExecutorConfig::Echo),
430
- "claude" => Ok(ExecutorConfig::Claude),
431
- "claude-plan" => Ok(ExecutorConfig::ClaudePlan),
432
- "amp" => Ok(ExecutorConfig::Amp),
433
- "gemini" => Ok(ExecutorConfig::Gemini),
434
- "charm-opencode" => Ok(ExecutorConfig::CharmOpencode),
435
- "claude-code-router" => Ok(ExecutorConfig::ClaudeCodeRouter),
436
- "sst-opencode" => Ok(ExecutorConfig::SstOpencode),
437
- "opencode-ai" => Ok(ExecutorConfig::OpencodeAi),
438
- "setup-script" => Ok(ExecutorConfig::SetupScript {
439
- script: "setup script".to_string(),
440
- }),
441
- _ => Err(format!("Unknown executor type: {}", s)),
442
- }
443
- }
444
- }
445
-
446
- impl ExecutorConfig {
447
- pub fn create_executor(&self) -> Box<dyn Executor> {
448
- match self {
449
- ExecutorConfig::Echo => Box::new(EchoExecutor),
450
- ExecutorConfig::Claude => Box::new(ClaudeExecutor::new()),
451
- ExecutorConfig::ClaudePlan => Box::new(ClaudeExecutor::new_plan_mode()),
452
- ExecutorConfig::Amp => Box::new(AmpExecutor),
453
- ExecutorConfig::Gemini => Box::new(GeminiExecutor),
454
- ExecutorConfig::ClaudeCodeRouter => Box::new(CCRExecutor::new()),
455
- ExecutorConfig::CharmOpencode => Box::new(CharmOpencodeExecutor),
456
- ExecutorConfig::SstOpencode => Box::new(SstOpencodeExecutor::new()),
457
- ExecutorConfig::OpencodeAi => Box::new(OpencodeAiExecutor),
458
- ExecutorConfig::SetupScript { script } => {
459
- Box::new(SetupScriptExecutor::new(script.clone()))
460
- }
461
- }
462
- }
463
-
464
- pub fn config_path(&self) -> Option<std::path::PathBuf> {
465
- match self {
466
- ExecutorConfig::Echo => None,
467
- ExecutorConfig::CharmOpencode => {
468
- dirs::home_dir().map(|home| home.join(".opencode.json"))
469
- }
470
- ExecutorConfig::Claude => dirs::home_dir().map(|home| home.join(".claude.json")),
471
- ExecutorConfig::ClaudePlan => dirs::home_dir().map(|home| home.join(".claude.json")),
472
- ExecutorConfig::ClaudeCodeRouter => {
473
- dirs::home_dir().map(|home| home.join(".claude.json"))
474
- }
475
- ExecutorConfig::Amp => {
476
- dirs::config_dir().map(|config| config.join("amp").join("settings.json"))
477
- }
478
- ExecutorConfig::Gemini => {
479
- dirs::home_dir().map(|home| home.join(".gemini").join("settings.json"))
480
- }
481
- ExecutorConfig::SstOpencode => {
482
- #[cfg(unix)]
483
- {
484
- xdg::BaseDirectories::with_prefix("opencode").get_config_file("opencode.json")
485
- }
486
- #[cfg(not(unix))]
487
- {
488
- dirs::config_dir().map(|config| config.join("opencode").join("opencode.json"))
489
- }
490
- }
491
- ExecutorConfig::OpencodeAi => {
492
- dirs::home_dir().map(|home| home.join(".opencode-ai.json"))
493
- }
494
- ExecutorConfig::SetupScript { .. } => None,
495
- }
496
- }
497
-
498
- /// Get the JSON attribute path for MCP servers in the config file
499
- pub fn mcp_attribute_path(&self) -> Option<Vec<&'static str>> {
500
- match self {
501
- ExecutorConfig::Echo => None, // Echo doesn't support MCP
502
- ExecutorConfig::CharmOpencode => Some(vec!["mcpServers"]),
503
- ExecutorConfig::SstOpencode => Some(vec!["mcp"]),
504
- ExecutorConfig::Claude => Some(vec!["mcpServers"]),
505
- ExecutorConfig::ClaudePlan => Some(vec!["mcpServers"]),
506
- ExecutorConfig::Amp => Some(vec!["amp", "mcpServers"]), // Nested path for Amp
507
- ExecutorConfig::Gemini => Some(vec!["mcpServers"]),
508
- ExecutorConfig::ClaudeCodeRouter => Some(vec!["mcpServers"]),
509
- ExecutorConfig::OpencodeAi => Some(vec!["mcpServers"]),
510
- ExecutorConfig::SetupScript { .. } => None, // Setup scripts don't support MCP
511
- }
512
- }
513
-
514
- /// Check if this executor supports MCP configuration
515
- pub fn supports_mcp(&self) -> bool {
516
- !matches!(
517
- self,
518
- ExecutorConfig::Echo | ExecutorConfig::SetupScript { .. }
519
- )
520
- }
521
-
522
- /// Get the display name for this executor
523
- pub fn display_name(&self) -> &'static str {
524
- match self {
525
- ExecutorConfig::Echo => "Echo (Test Mode)",
526
- ExecutorConfig::CharmOpencode => "Charm Opencode",
527
- ExecutorConfig::SstOpencode => "SST Opencode",
528
- ExecutorConfig::Claude => "Claude",
529
- ExecutorConfig::ClaudePlan => "Claude Plan",
530
- ExecutorConfig::Amp => "Amp",
531
- ExecutorConfig::Gemini => "Gemini",
532
- ExecutorConfig::ClaudeCodeRouter => "Claude Code Router",
533
- ExecutorConfig::OpencodeAi => "OpenCode AI",
534
- ExecutorConfig::SetupScript { .. } => "Setup Script",
535
- }
536
- }
537
- }
538
-
539
- impl std::fmt::Display for ExecutorConfig {
540
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541
- let s = match self {
542
- ExecutorConfig::Echo => "echo",
543
- ExecutorConfig::Claude => "claude",
544
- ExecutorConfig::ClaudePlan => "claude-plan",
545
- ExecutorConfig::Amp => "amp",
546
- ExecutorConfig::Gemini => "gemini",
547
- ExecutorConfig::SstOpencode => "sst-opencode",
548
- ExecutorConfig::CharmOpencode => "charm-opencode",
549
- ExecutorConfig::ClaudeCodeRouter => "claude-code-router",
550
- ExecutorConfig::OpencodeAi => "opencode-ai",
551
- ExecutorConfig::SetupScript { .. } => "setup-script",
552
- };
553
- write!(f, "{}", s)
554
- }
555
- }
556
-
557
- /// Stream output from a child process to the database
558
- pub async fn stream_output_to_db(
559
- output: impl tokio::io::AsyncRead + Unpin,
560
- pool: sqlx::SqlitePool,
561
- attempt_id: Uuid,
562
- execution_process_id: Uuid,
563
- is_stdout: bool,
564
- ) {
565
- if is_stdout {
566
- stream_stdout_to_db(output, pool, attempt_id, execution_process_id).await;
567
- } else {
568
- stream_stderr_to_db(output, pool, attempt_id, execution_process_id).await;
569
- }
570
- }
571
-
572
- /// Stream stdout from a child process to the database (immediate updates)
573
- async fn stream_stdout_to_db(
574
- output: impl tokio::io::AsyncRead + Unpin,
575
- pool: sqlx::SqlitePool,
576
- attempt_id: Uuid,
577
- execution_process_id: Uuid,
578
- ) {
579
- use crate::models::{execution_process::ExecutionProcess, executor_session::ExecutorSession};
580
-
581
- let mut reader = BufReader::new(output);
582
- let mut line = String::new();
583
- let mut accumulated_output = String::new();
584
- let mut update_counter = 0;
585
- let mut session_id_parsed = false;
586
-
587
- loop {
588
- line.clear();
589
- match reader.read_line(&mut line).await {
590
- Ok(0) => break, // EOF
591
- Ok(_) => {
592
- // Parse session ID from the first JSONL line
593
- if !session_id_parsed {
594
- if let Some(external_session_id) = parse_session_id_from_line(&line) {
595
- if let Err(e) = ExecutorSession::update_session_id(
596
- &pool,
597
- execution_process_id,
598
- &external_session_id,
599
- )
600
- .await
601
- {
602
- tracing::error!(
603
- "Failed to update session ID for execution process {}: {}",
604
- execution_process_id,
605
- e
606
- );
607
- } else {
608
- tracing::info!(
609
- "Updated session ID {} for execution process {}",
610
- external_session_id,
611
- execution_process_id
612
- );
613
- }
614
- session_id_parsed = true;
615
- }
616
- }
617
- accumulated_output.push_str(&line);
618
- update_counter += 1;
619
-
620
- // Update database every threshold lines or when we have a significant amount of data
621
- if update_counter >= STDOUT_UPDATE_THRESHOLD
622
- || accumulated_output.len() > BUFFER_SIZE_THRESHOLD
623
- {
624
- if let Err(e) = ExecutionProcess::append_output(
625
- &pool,
626
- execution_process_id,
627
- Some(&accumulated_output),
628
- None,
629
- )
630
- .await
631
- {
632
- tracing::error!(
633
- "Failed to update stdout for attempt {}: {}",
634
- attempt_id,
635
- e
636
- );
637
- }
638
- accumulated_output.clear();
639
- update_counter = 0;
640
- }
641
- }
642
- Err(e) => {
643
- tracing::error!("Error reading stdout for attempt {}: {}", attempt_id, e);
644
- break;
645
- }
646
- }
647
- }
648
-
649
- // Flush any remaining output
650
- if !accumulated_output.is_empty() {
651
- if let Err(e) = ExecutionProcess::append_output(
652
- &pool,
653
- execution_process_id,
654
- Some(&accumulated_output),
655
- None,
656
- )
657
- .await
658
- {
659
- tracing::error!("Failed to flush stdout for attempt {}: {}", attempt_id, e);
660
- }
661
- }
662
- }
663
-
664
- /// Stream stderr from a child process to the database (buffered with timeout)
665
- async fn stream_stderr_to_db(
666
- output: impl tokio::io::AsyncRead + Unpin,
667
- pool: sqlx::SqlitePool,
668
- attempt_id: Uuid,
669
- execution_process_id: Uuid,
670
- ) {
671
- use tokio::time::{timeout, Duration};
672
-
673
- let mut reader = BufReader::new(output);
674
- let mut line = String::new();
675
- let mut accumulated_output = String::new();
676
- const STDERR_FLUSH_TIMEOUT_MS: u64 = 100; // Fast flush for near-real-time streaming
677
- const STDERR_FLUSH_TIMEOUT: Duration = Duration::from_millis(STDERR_FLUSH_TIMEOUT_MS);
678
-
679
- loop {
680
- line.clear();
681
-
682
- // Try to read a line with a timeout
683
- let read_result = timeout(STDERR_FLUSH_TIMEOUT, reader.read_line(&mut line)).await;
684
-
685
- match read_result {
686
- Ok(Ok(0)) => {
687
- // EOF - flush remaining output and break
688
- break;
689
- }
690
- Ok(Ok(_)) => {
691
- // Successfully read a line - just accumulate it
692
- accumulated_output.push_str(&line);
693
- }
694
- Ok(Err(e)) => {
695
- tracing::error!("Error reading stderr for attempt {}: {}", attempt_id, e);
696
- break;
697
- }
698
- Err(_) => {
699
- // Timeout occurred - flush accumulated output if any
700
- if !accumulated_output.is_empty() {
701
- flush_stderr_chunk(
702
- &pool,
703
- execution_process_id,
704
- &accumulated_output,
705
- attempt_id,
706
- )
707
- .await;
708
- accumulated_output.clear();
709
- }
710
- }
711
- }
712
- }
713
-
714
- // Final flush for any remaining output
715
- if !accumulated_output.is_empty() {
716
- flush_stderr_chunk(&pool, execution_process_id, &accumulated_output, attempt_id).await;
717
- }
718
- }
719
-
720
- /// Flush a chunk of stderr output to the database
721
- async fn flush_stderr_chunk(
722
- pool: &sqlx::SqlitePool,
723
- execution_process_id: Uuid,
724
- content: &str,
725
- attempt_id: Uuid,
726
- ) {
727
- use crate::models::execution_process::ExecutionProcess;
728
-
729
- let trimmed = content.trim();
730
- if trimmed.is_empty() {
731
- return;
732
- }
733
-
734
- // Add a delimiter to separate chunks in the database
735
- let chunk_with_delimiter = format!("{}\n---STDERR_CHUNK_BOUNDARY---\n", trimmed);
736
-
737
- if let Err(e) = ExecutionProcess::append_output(
738
- pool,
739
- execution_process_id,
740
- None,
741
- Some(&chunk_with_delimiter),
742
- )
743
- .await
744
- {
745
- tracing::error!(
746
- "Failed to flush stderr chunk for attempt {}: {}",
747
- attempt_id,
748
- e
749
- );
750
- } else {
751
- tracing::debug!(
752
- "Flushed stderr chunk ({} chars) for process {}",
753
- trimmed.len(),
754
- execution_process_id
755
- );
756
- }
757
- }
758
-
759
- /// Parse assistant message from executor logs (JSONL format)
760
- pub fn parse_assistant_message_from_logs(logs: &str) -> Option<String> {
761
- use serde_json::Value;
762
-
763
- let mut last_assistant_message = None;
764
-
765
- for line in logs.lines() {
766
- let trimmed = line.trim();
767
- if trimmed.is_empty() {
768
- continue;
769
- }
770
-
771
- // Try to parse as JSON
772
- if let Ok(json) = serde_json::from_str::<Value>(trimmed) {
773
- // Check for Claude format: {"type":"assistant","message":{"content":[...]}}
774
- if let Some(msg_type) = json.get("type").and_then(|t| t.as_str()) {
775
- if msg_type == "assistant" {
776
- if let Some(message) = json.get("message") {
777
- if let Some(content) = message.get("content").and_then(|c| c.as_array()) {
778
- // Extract text content from Claude assistant message
779
- let mut text_parts = Vec::new();
780
- for content_item in content {
781
- if let Some(content_type) =
782
- content_item.get("type").and_then(|t| t.as_str())
783
- {
784
- if content_type == "text" {
785
- if let Some(text) =
786
- content_item.get("text").and_then(|t| t.as_str())
787
- {
788
- text_parts.push(text);
789
- }
790
- }
791
- }
792
- }
793
- if !text_parts.is_empty() {
794
- last_assistant_message = Some(text_parts.join("\n"));
795
- }
796
- }
797
- }
798
- continue;
799
- }
800
- }
801
-
802
- // Check for AMP format: {"type":"messages","messages":[[1,{"role":"assistant",...}]]}
803
- if let Some(messages) = json.get("messages").and_then(|m| m.as_array()) {
804
- for message_entry in messages {
805
- if let Some(message_data) = message_entry.as_array().and_then(|arr| arr.get(1))
806
- {
807
- if let Some(role) = message_data.get("role").and_then(|r| r.as_str()) {
808
- if role == "assistant" {
809
- if let Some(content) =
810
- message_data.get("content").and_then(|c| c.as_array())
811
- {
812
- // Extract text content from AMP assistant message
813
- let mut text_parts = Vec::new();
814
- for content_item in content {
815
- if let Some(content_type) =
816
- content_item.get("type").and_then(|t| t.as_str())
817
- {
818
- if content_type == "text" {
819
- if let Some(text) = content_item
820
- .get("text")
821
- .and_then(|t| t.as_str())
822
- {
823
- text_parts.push(text);
824
- }
825
- }
826
- }
827
- }
828
- if !text_parts.is_empty() {
829
- last_assistant_message = Some(text_parts.join("\n"));
830
- }
831
- }
832
- }
833
- }
834
- }
835
- }
836
- }
837
- }
838
- }
839
-
840
- last_assistant_message
841
- }
842
-
843
- /// Parse session_id from Claude or thread_id from Amp from the first JSONL line
844
- fn parse_session_id_from_line(line: &str) -> Option<String> {
845
- use serde_json::Value;
846
-
847
- let trimmed = line.trim();
848
- if trimmed.is_empty() {
849
- return None;
850
- }
851
-
852
- // Try to parse as JSON
853
- if let Ok(json) = serde_json::from_str::<Value>(trimmed) {
854
- // Check for Claude session_id
855
- if let Some(session_id) = json.get("session_id").and_then(|v| v.as_str()) {
856
- return Some(session_id.to_string());
857
- }
858
-
859
- // Check for Amp threadID
860
- if let Some(thread_id) = json.get("threadID").and_then(|v| v.as_str()) {
861
- return Some(thread_id.to_string());
862
- }
863
- }
864
-
865
- None
866
- }
867
-
868
- #[cfg(test)]
869
- mod tests {
870
- use super::*;
871
- use crate::executors::{AmpExecutor, ClaudeExecutor};
872
-
873
- #[test]
874
- fn test_parse_claude_session_id() {
875
- let claude_line = r#"{"type":"system","subtype":"init","cwd":"/private/tmp/mission-control-worktree-3abb979d-2e0e-4404-a276-c16d98a97dd5","session_id":"cc0889a2-0c59-43cc-926b-739a983888a2","tools":["Task","Bash","Glob","Grep","LS","exit_plan_mode","Read","Edit","MultiEdit","Write","NotebookRead","NotebookEdit","WebFetch","TodoRead","TodoWrite","WebSearch"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"bypassPermissions","apiKeySource":"/login managed key"}"#;
876
-
877
- assert_eq!(
878
- parse_session_id_from_line(claude_line),
879
- Some("cc0889a2-0c59-43cc-926b-739a983888a2".to_string())
880
- );
881
- }
882
-
883
- #[test]
884
- fn test_parse_amp_thread_id() {
885
- let amp_line = r#"{"type":"initial","threadID":"T-286f908a-2cd8-40cc-9490-da689b2f1560"}"#;
886
-
887
- assert_eq!(
888
- parse_session_id_from_line(amp_line),
889
- Some("T-286f908a-2cd8-40cc-9490-da689b2f1560".to_string())
890
- );
891
- }
892
-
893
- #[test]
894
- fn test_parse_invalid_json() {
895
- let invalid_line = "not json at all";
896
- assert_eq!(parse_session_id_from_line(invalid_line), None);
897
- }
898
-
899
- #[test]
900
- fn test_parse_json_without_ids() {
901
- let other_json = r#"{"type":"other","message":"hello"}"#;
902
- assert_eq!(parse_session_id_from_line(other_json), None);
903
- }
904
-
905
- #[test]
906
- fn test_parse_empty_line() {
907
- assert_eq!(parse_session_id_from_line(""), None);
908
- assert_eq!(parse_session_id_from_line(" "), None);
909
- }
910
-
911
- #[test]
912
- fn test_parse_assistant_message_from_logs() {
913
- // Test AMP format
914
- let amp_logs = r#"{"type":"initial","threadID":"T-e7af5516-e5a5-4754-8e34-810dc658716e"}
915
- {"type":"messages","messages":[[0,{"role":"user","content":[{"type":"text","text":"Task title: Test task"}],"meta":{"sentAt":1751385490573}}]],"toolResults":[]}
916
- {"type":"messages","messages":[[1,{"role":"assistant","content":[{"type":"thinking","thinking":"Testing"},{"type":"text","text":"The Pythagorean theorem states that in a right triangle, the square of the hypotenuse equals the sum of squares of the other two sides: **a² + b² = c²**."}],"state":{"type":"complete","stopReason":"end_turn"}}]],"toolResults":[]}
917
- {"type":"state","state":"idle"}
918
- {"type":"shutdown"}"#;
919
-
920
- let result = parse_assistant_message_from_logs(amp_logs);
921
- assert!(result.is_some());
922
- assert!(result.as_ref().unwrap().contains("Pythagorean theorem"));
923
- assert!(result.as_ref().unwrap().contains("a² + b² = c²"));
924
- }
925
-
926
- #[test]
927
- fn test_parse_claude_assistant_message_from_logs() {
928
- // Test Claude format
929
- let claude_logs = r#"{"type":"system","subtype":"init","cwd":"/private/tmp","session_id":"e988eeea-3712-46a1-82d4-84fbfaa69114","tools":[],"model":"claude-sonnet-4-20250514"}
930
- {"type":"assistant","message":{"id":"msg_123","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"I'll explain the Pythagorean theorem for you.\n\nThe Pythagorean theorem states that in a right triangle, the square of the hypotenuse equals the sum of the squares of the other two sides.\n\n**Formula:** a² + b² = c²"}],"stop_reason":null},"session_id":"e988eeea-3712-46a1-82d4-84fbfaa69114"}
931
- {"type":"result","subtype":"success","is_error":false,"duration_ms":6059,"result":"Final result"}"#;
932
-
933
- let result = parse_assistant_message_from_logs(claude_logs);
934
- assert!(result.is_some());
935
- assert!(result.as_ref().unwrap().contains("Pythagorean theorem"));
936
- assert!(result
937
- .as_ref()
938
- .unwrap()
939
- .contains("**Formula:** a² + b² = c²"));
940
- }
941
-
942
- #[test]
943
- fn test_amp_log_normalization() {
944
- let amp_executor = AmpExecutor;
945
- let amp_logs = r#"{"type":"initial","threadID":"T-f8f7fec0-b330-47ab-b63a-b72c42f1ef6a"}
946
- {"type":"messages","messages":[[0,{"role":"user","content":[{"type":"text","text":"Task title: Create and start should open task\nTask description: When I press 'create & start' on task creation dialog it should then open the task in the sidebar"}],"meta":{"sentAt":1751544747623}}]],"toolResults":[]}
947
- {"type":"messages","messages":[[1,{"role":"assistant","content":[{"type":"thinking","thinking":"The user wants to implement a feature where pressing \"create & start\" on the task creation dialog should open the task in the sidebar."},{"type":"text","text":"I'll help you implement the \"create & start\" functionality. Let me explore the codebase to understand the current task creation and sidebar structure."},{"type":"tool_use","id":"toolu_01FQqskzGAhZaZu8H6qSs5pV","name":"todo_write","input":{"todos":[{"id":"1","content":"Explore task creation dialog component","status":"todo","priority":"high"}]}}],"state":{"type":"complete","stopReason":"tool_use"}}]],"toolResults":[]}"#;
948
-
949
- let result = amp_executor
950
- .normalize_logs(amp_logs, "/tmp/test-worktree")
951
- .unwrap();
952
-
953
- assert_eq!(result.executor_type, "amp");
954
- assert_eq!(
955
- result.session_id,
956
- Some("T-f8f7fec0-b330-47ab-b63a-b72c42f1ef6a".to_string())
957
- );
958
- assert!(!result.entries.is_empty());
959
-
960
- // Check that we have user message, assistant message, thinking, and tool use entries
961
- let user_messages: Vec<_> = result
962
- .entries
963
- .iter()
964
- .filter(|e| matches!(e.entry_type, NormalizedEntryType::UserMessage))
965
- .collect();
966
- assert!(!user_messages.is_empty());
967
-
968
- let assistant_messages: Vec<_> = result
969
- .entries
970
- .iter()
971
- .filter(|e| matches!(e.entry_type, NormalizedEntryType::AssistantMessage))
972
- .collect();
973
- assert!(!assistant_messages.is_empty());
974
-
975
- let thinking_entries: Vec<_> = result
976
- .entries
977
- .iter()
978
- .filter(|e| matches!(e.entry_type, NormalizedEntryType::Thinking))
979
- .collect();
980
- assert!(!thinking_entries.is_empty());
981
-
982
- let tool_uses: Vec<_> = result
983
- .entries
984
- .iter()
985
- .filter(|e| matches!(e.entry_type, NormalizedEntryType::ToolUse { .. }))
986
- .collect();
987
- assert!(!tool_uses.is_empty());
988
-
989
- // Check that tool use content is concise (not the old verbose format)
990
- let todo_tool_use = tool_uses.iter().find(|e| match &e.entry_type {
991
- NormalizedEntryType::ToolUse { tool_name, .. } => tool_name == "todo_write",
992
- _ => false,
993
- });
994
- assert!(todo_tool_use.is_some());
995
- let todo_tool_use = todo_tool_use.unwrap();
996
- // Should be concise, not "Tool: todo_write with input: ..."
997
- assert_eq!(
998
- todo_tool_use.content,
999
- "TODO List:\n⏳ Explore task creation dialog component (high)"
1000
- );
1001
- }
1002
-
1003
- #[test]
1004
- fn test_claude_log_normalization() {
1005
- let claude_executor = ClaudeExecutor::new();
1006
- let claude_logs = r#"{"type":"system","subtype":"init","cwd":"/private/tmp/mission-control-worktree-8ff34214-7bb4-4a5a-9f47-bfdf79e20368","session_id":"499dcce4-04aa-4a3e-9e0c-ea0228fa87c9","tools":["Task","Bash","Glob","Grep","LS","exit_plan_mode","Read","Edit","MultiEdit","Write","NotebookRead","NotebookEdit","WebFetch","TodoRead","TodoWrite","WebSearch"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"bypassPermissions","apiKeySource":"none"}
1007
- {"type":"assistant","message":{"id":"msg_014xUHgkAhs6cRx5WVT3s7if","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"I'll help you list your projects using automagik-forge. Let me first explore the codebase to understand how automagik-forge works and find your projects."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":13497,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}},"parent_tool_use_id":null,"session_id":"499dcce4-04aa-4a3e-9e0c-ea0228fa87c9"}
1008
- {"type":"assistant","message":{"id":"msg_014xUHgkAhs6cRx5WVT3s7if","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01Br3TvXdmW6RPGpB5NihTHh","name":"Task","input":{"description":"Find automagik-forge projects","prompt":"I need to find and list projects using automagik-forge."}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":13497,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}},"parent_tool_use_id":null,"session_id":"499dcce4-04aa-4a3e-9e0c-ea0228fa87c9"}"#;
1009
-
1010
- let result = claude_executor
1011
- .normalize_logs(claude_logs, "/tmp/test-worktree")
1012
- .unwrap();
1013
-
1014
- assert_eq!(result.executor_type, "Claude Code");
1015
- assert_eq!(
1016
- result.session_id,
1017
- Some("499dcce4-04aa-4a3e-9e0c-ea0228fa87c9".to_string())
1018
- );
1019
- assert!(!result.entries.is_empty());
1020
-
1021
- // Check that we have system, assistant message, and tool use entries
1022
- let system_messages: Vec<_> = result
1023
- .entries
1024
- .iter()
1025
- .filter(|e| matches!(e.entry_type, NormalizedEntryType::SystemMessage))
1026
- .collect();
1027
- assert!(!system_messages.is_empty());
1028
-
1029
- let assistant_messages: Vec<_> = result
1030
- .entries
1031
- .iter()
1032
- .filter(|e| matches!(e.entry_type, NormalizedEntryType::AssistantMessage))
1033
- .collect();
1034
- assert!(!assistant_messages.is_empty());
1035
-
1036
- let tool_uses: Vec<_> = result
1037
- .entries
1038
- .iter()
1039
- .filter(|e| matches!(e.entry_type, NormalizedEntryType::ToolUse { .. }))
1040
- .collect();
1041
- assert!(!tool_uses.is_empty());
1042
-
1043
- // Check that tool use content is concise (not the old verbose format)
1044
- let task_tool_use = tool_uses.iter().find(|e| match &e.entry_type {
1045
- NormalizedEntryType::ToolUse { tool_name, .. } => tool_name == "Task",
1046
- _ => false,
1047
- });
1048
- assert!(task_tool_use.is_some());
1049
- let task_tool_use = task_tool_use.unwrap();
1050
- // Should be the task description, not "Tool: Task with input: ..."
1051
- assert_eq!(task_tool_use.content, "Find automagik-forge projects");
1052
- }
1053
- }