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.
- package/README.md +143 -447
- package/dist/linux-x64/automagik-forge-mcp.zip +0 -0
- package/{npx-cli/automagik-forge-0.0.55.tgz → dist/linux-x64/automagik-forge.zip} +0 -0
- package/package.json +13 -23
- package/.cargo/config.toml +0 -13
- package/.claude/commands/commit.md +0 -376
- package/.claude/commands/prompt.md +0 -871
- package/.env.example +0 -20
- package/.github/actions/setup-node/action.yml +0 -29
- package/.github/images/automagik-logo.png +0 -0
- package/.github/workflows/pre-release.yml +0 -470
- package/.github/workflows/publish.yml +0 -145
- package/.github/workflows/test.yml +0 -63
- package/.mcp.json +0 -57
- package/AGENT.md +0 -40
- package/CLAUDE.md +0 -40
- package/CODE-OF-CONDUCT.md +0 -89
- package/Cargo.toml +0 -19
- package/Dockerfile +0 -43
- package/LICENSE +0 -201
- package/Makefile +0 -97
- package/backend/.sqlx/query-01b7e2bac1261d8be3d03c03df3e5220590da6c31c77f161074fc62752d63881.json +0 -12
- package/backend/.sqlx/query-03f2b02ba6dc5ea2b3cf6b1004caea0ad6bcc10ebd63f441d321a389f026e263.json +0 -12
- package/backend/.sqlx/query-0923b77d137a29fc54d399a873ff15fc4af894490bc65a4d344a7575cb0d8643.json +0 -12
- package/backend/.sqlx/query-0f808bcdb63c5f180836e448dd64c435c51758b2fc54a52ce9e67495b1ab200e.json +0 -68
- package/backend/.sqlx/query-1268afe9ca849daa6722e3df7ca8e9e61f0d37052e782bb5452ab8e1018d9b63.json +0 -12
- package/backend/.sqlx/query-1b082630a9622f8667ee7a9aba2c2d3176019a68c6bb83d33008594821415a57.json +0 -12
- package/backend/.sqlx/query-1c7b06ba1e112abf6b945a2ff08a0b40ec23f3738c2e7399f067b558cf8d490e.json +0 -12
- package/backend/.sqlx/query-1f619f01f46859a64ded531dd0ef61abacfe62e758abe7030a6aa745140b95ca.json +0 -104
- package/backend/.sqlx/query-1fca1ce14b4b20205364cd1f1f45ebe1d2e30cd745e59e189d56487b5639dfbb.json +0 -12
- package/backend/.sqlx/query-212828320e8d871ab9d83705a040b23bcf0393dc7252177fc539a74657f578ef.json +0 -32
- package/backend/.sqlx/query-290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b.json +0 -56
- package/backend/.sqlx/query-2b471d2c2e8ffbe0cd42d2a91b814c0d79f9d09200f147e3cea33ba4ce673c8a.json +0 -68
- package/backend/.sqlx/query-354a48c705bb9bb2048c1b7f10fcb714e23f9db82b7a4ea6932486197b2ede6a.json +0 -92
- package/backend/.sqlx/query-36c9e3dd10648e94b949db5c91a774ecb1e10a899ef95da74066eccedca4d8b2.json +0 -12
- package/backend/.sqlx/query-36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94.json +0 -56
- package/backend/.sqlx/query-3a5b3c98a55ca183ab20c74708e3d7e579dda37972c059e7515c4ceee4bd8dd3.json +0 -62
- package/backend/.sqlx/query-3d0a1cabf2a52e9d90cdfd29c509ca89aeb448d0c1d2446c65cd43db40735e86.json +0 -62
- package/backend/.sqlx/query-3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031.json +0 -56
- package/backend/.sqlx/query-4049ca413b285a05aca6b25385e9c8185575f01e9069e4e8581aa45d713f612f.json +0 -32
- package/backend/.sqlx/query-412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1.json +0 -104
- package/backend/.sqlx/query-417a8b1ff4e51de82aea0159a3b97932224dc325b23476cb84153d690227fd8b.json +0 -62
- package/backend/.sqlx/query-461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2.json +0 -56
- package/backend/.sqlx/query-58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227.json +0 -92
- package/backend/.sqlx/query-5a886026d75d515c01f347cc203c8d99dd04c61dc468e2e4c5aa548436d13834.json +0 -62
- package/backend/.sqlx/query-5b902137b11022d2e1a5c4f6a9c83fec1a856c6a710aff831abd2382ede76b43.json +0 -12
- package/backend/.sqlx/query-5ed1238e52e59bb5f76c0f153fd99a14093f7ce2585bf9843585608f17ec575b.json +0 -104
- package/backend/.sqlx/query-6e8b860b14decfc2227dc57213f38442943d3fbef5c8418fd6b634c6e0f5e2ea.json +0 -104
- package/backend/.sqlx/query-6ec414276994c4ccb2433eaa5b1b342168557d17ddf5a52dac84cb1b59b9de8f.json +0 -68
- package/backend/.sqlx/query-6ecfa16d0cf825aacf233544b5baf151e9adfdca26c226ad71020d291fd802d5.json +0 -62
- package/backend/.sqlx/query-72509d252c39fce77520aa816cb2acbc1fb35dc2605e7be893610599b2427f2e.json +0 -62
- package/backend/.sqlx/query-75239b2da188f749707d77f3c1544332ca70db3d6d6743b2601dc0d167536437.json +0 -62
- package/backend/.sqlx/query-83d10e29f8478aff33434f9ac67068e013b888b953a2657e2bb72a6f619d04f2.json +0 -50
- package/backend/.sqlx/query-8610803360ea18b9b9d078a6981ea56abfbfe84e6354fc1d5ae4c622e01410ed.json +0 -68
- package/backend/.sqlx/query-86d03eb70eef39c59296416867f2ee66c9f7cd8b7f961fbda2f89fc0a1c442c2.json +0 -12
- package/backend/.sqlx/query-87d0feb5a6b442bad9c60068ea7569599cc6fc91a0e2692ecb42e93b03201b9d.json +0 -68
- package/backend/.sqlx/query-8a67b3b3337248f06a57bdf8a908f7ef23177431eaed82dc08c94c3e5944340e.json +0 -12
- package/backend/.sqlx/query-8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3.json +0 -12
- package/backend/.sqlx/query-90fd607fcb2dca72239ff25e618e21e174b195991eaa33722cbf5f76da84cfab.json +0 -62
- package/backend/.sqlx/query-92e8bdbcd80c5ff3db7a35cf79492048803ef305cbdef0d0a1fe5dc881ca8c71.json +0 -104
- package/backend/.sqlx/query-93a1605f90e9672dad29b472b6ad85fa9a55ea3ffa5abcb8724b09d61be254ca.json +0 -20
- package/backend/.sqlx/query-9472c8fb477958167f5fae40b85ac44252468c5226b2cdd7770f027332eed6d7.json +0 -104
- package/backend/.sqlx/query-96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2.json +0 -56
- package/backend/.sqlx/query-9edb2c01e91fd0f0fe7b56e988c7ae0393150f50be3f419a981e035c0121dfc7.json +0 -104
- package/backend/.sqlx/query-a157cf00616f703bfba21927f1eb1c9eec2a81c02da15f66efdba0b6c375de1b.json +0 -26
- package/backend/.sqlx/query-a31fff84f3b8e532fd1160447d89d700f06ae08821fee00c9a5b60492b05259c.json +0 -62
- package/backend/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json +0 -12
- package/backend/.sqlx/query-a6d2961718dbc3b1a925e549f49a159c561bef58c105529275f274b27e2eba5b.json +0 -104
- package/backend/.sqlx/query-a9e93d5b09b29faf66e387e4d7596a792d81e75c4d3726e83c2963e8d7c9b56f.json +0 -104
- package/backend/.sqlx/query-ac5247c8d7fb86e4650c4b0eb9420031614c831b7b085083bac20c1af314c538.json +0 -12
- package/backend/.sqlx/query-afef9467be74c411c4cb119a8b2b1aea53049877dfc30cc60b486134ba4b4c9f.json +0 -68
- package/backend/.sqlx/query-b2b2c6b4d0b1a347b5c4cb63c3a46a265d4db53be9554989a814b069d0af82f2.json +0 -62
- package/backend/.sqlx/query-c50d2ff0b12e5bcc81e371089ee2d007e233e7db93aefba4fef08e7aa68f5ab7.json +0 -20
- package/backend/.sqlx/query-c614e6056b244ca07f1b9d44e7edc9d5819225c6f8d9e077070c6e518a17f50b.json +0 -12
- package/backend/.sqlx/query-c67259be8bf4ee0cfd32167b2aa3b7fe9192809181a8171bf1c2d6df731967ae.json +0 -12
- package/backend/.sqlx/query-d2d0a1b985ebbca6a2b3e882a221a219f3199890fa640afc946ef1a792d6d8de.json +0 -12
- package/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json +0 -20
- package/backend/.sqlx/query-d3b9ea1de1576af71b312924ce7f4ea8ae5dbe2ac138ea3b4470f2d5cd734846.json +0 -12
- package/backend/.sqlx/query-ed8456646fa69ddd412441955f06ff22bfb790f29466450735e0b8bb1bc4ec94.json +0 -12
- package/backend/Cargo.toml +0 -71
- package/backend/build.rs +0 -32
- package/backend/migrations/20250617183714_init.sql +0 -44
- package/backend/migrations/20250620212427_execution_processes.sql +0 -25
- package/backend/migrations/20250620214100_remove_stdout_stderr_from_task_attempts.sql +0 -28
- package/backend/migrations/20250621120000_relate_activities_to_execution_processes.sql +0 -23
- package/backend/migrations/20250623120000_executor_sessions.sql +0 -17
- package/backend/migrations/20250623130000_add_executor_type_to_execution_processes.sql +0 -4
- package/backend/migrations/20250625000000_add_dev_script_to_projects.sql +0 -4
- package/backend/migrations/20250701000000_add_branch_to_task_attempts.sql +0 -2
- package/backend/migrations/20250701000001_add_pr_tracking_to_task_attempts.sql +0 -5
- package/backend/migrations/20250701120000_add_assistant_message_to_executor_sessions.sql +0 -2
- package/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql +0 -2
- package/backend/migrations/20250709000000_add_worktree_deleted_flag.sql +0 -2
- package/backend/migrations/20250710000000_add_setup_completion.sql +0 -3
- package/backend/migrations/20250715154859_add_task_templates.sql +0 -25
- package/backend/migrations/20250716143725_add_default_templates.sql +0 -174
- package/backend/migrations/20250716161432_update_executor_names_to_kebab_case.sql +0 -20
- package/backend/migrations/20250716170000_add_parent_task_to_tasks.sql +0 -7
- package/backend/migrations/20250717000000_drop_task_attempt_activities.sql +0 -9
- package/backend/migrations/20250719000000_add_cleanup_script_to_projects.sql +0 -2
- package/backend/migrations/20250720000000_add_cleanupscript_to_process_type_constraint.sql +0 -25
- package/backend/migrations/20250723000000_add_wish_to_tasks.sql +0 -7
- package/backend/migrations/20250724000000_remove_unique_wish_constraint.sql +0 -5
- package/backend/scripts/toast-notification.ps1 +0 -23
- package/backend/sounds/abstract-sound1.wav +0 -0
- package/backend/sounds/abstract-sound2.wav +0 -0
- package/backend/sounds/abstract-sound3.wav +0 -0
- package/backend/sounds/abstract-sound4.wav +0 -0
- package/backend/sounds/cow-mooing.wav +0 -0
- package/backend/sounds/phone-vibration.wav +0 -0
- package/backend/sounds/rooster.wav +0 -0
- package/backend/src/app_state.rs +0 -218
- package/backend/src/bin/generate_types.rs +0 -189
- package/backend/src/bin/mcp_task_server.rs +0 -191
- package/backend/src/execution_monitor.rs +0 -1193
- package/backend/src/executor.rs +0 -1053
- package/backend/src/executors/amp.rs +0 -697
- package/backend/src/executors/ccr.rs +0 -91
- package/backend/src/executors/charm_opencode.rs +0 -113
- package/backend/src/executors/claude.rs +0 -887
- package/backend/src/executors/cleanup_script.rs +0 -124
- package/backend/src/executors/dev_server.rs +0 -53
- package/backend/src/executors/echo.rs +0 -79
- package/backend/src/executors/gemini/config.rs +0 -67
- package/backend/src/executors/gemini/streaming.rs +0 -363
- package/backend/src/executors/gemini.rs +0 -765
- package/backend/src/executors/mod.rs +0 -23
- package/backend/src/executors/opencode_ai.rs +0 -113
- package/backend/src/executors/setup_script.rs +0 -130
- package/backend/src/executors/sst_opencode/filter.rs +0 -184
- package/backend/src/executors/sst_opencode/tools.rs +0 -139
- package/backend/src/executors/sst_opencode.rs +0 -756
- package/backend/src/lib.rs +0 -45
- package/backend/src/main.rs +0 -324
- package/backend/src/mcp/mod.rs +0 -1
- package/backend/src/mcp/task_server.rs +0 -850
- package/backend/src/middleware/mod.rs +0 -3
- package/backend/src/middleware/model_loaders.rs +0 -242
- package/backend/src/models/api_response.rs +0 -36
- package/backend/src/models/config.rs +0 -375
- package/backend/src/models/execution_process.rs +0 -430
- package/backend/src/models/executor_session.rs +0 -225
- package/backend/src/models/mod.rs +0 -12
- package/backend/src/models/project.rs +0 -356
- package/backend/src/models/task.rs +0 -345
- package/backend/src/models/task_attempt.rs +0 -1214
- package/backend/src/models/task_template.rs +0 -146
- package/backend/src/openapi.rs +0 -93
- package/backend/src/routes/auth.rs +0 -297
- package/backend/src/routes/config.rs +0 -385
- package/backend/src/routes/filesystem.rs +0 -228
- package/backend/src/routes/health.rs +0 -16
- package/backend/src/routes/mod.rs +0 -9
- package/backend/src/routes/projects.rs +0 -562
- package/backend/src/routes/stream.rs +0 -244
- package/backend/src/routes/task_attempts.rs +0 -1172
- package/backend/src/routes/task_templates.rs +0 -229
- package/backend/src/routes/tasks.rs +0 -353
- package/backend/src/services/analytics.rs +0 -216
- package/backend/src/services/git_service.rs +0 -1321
- package/backend/src/services/github_service.rs +0 -307
- package/backend/src/services/mod.rs +0 -13
- package/backend/src/services/notification_service.rs +0 -263
- package/backend/src/services/pr_monitor.rs +0 -214
- package/backend/src/services/process_service.rs +0 -940
- package/backend/src/utils/path.rs +0 -96
- package/backend/src/utils/shell.rs +0 -19
- package/backend/src/utils/text.rs +0 -24
- package/backend/src/utils/worktree_manager.rs +0 -578
- package/backend/src/utils.rs +0 -125
- package/backend/test.db +0 -0
- package/build-npm-package.sh +0 -61
- package/dev_assets_seed/config.json +0 -19
- package/frontend/.eslintrc.json +0 -25
- package/frontend/.prettierrc.json +0 -8
- package/frontend/components.json +0 -17
- package/frontend/index.html +0 -19
- package/frontend/package-lock.json +0 -7321
- package/frontend/package.json +0 -61
- package/frontend/postcss.config.js +0 -6
- package/frontend/public/android-chrome-192x192.png +0 -0
- package/frontend/public/android-chrome-512x512.png +0 -0
- package/frontend/public/apple-touch-icon.png +0 -0
- package/frontend/public/automagik-forge-logo-dark.svg +0 -3
- package/frontend/public/automagik-forge-logo.svg +0 -3
- package/frontend/public/automagik-forge-screenshot-overview.png +0 -0
- package/frontend/public/favicon-16x16.png +0 -0
- package/frontend/public/favicon-32x32.png +0 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/public/site.webmanifest +0 -1
- package/frontend/public/viba-kanban-favicon.png +0 -0
- package/frontend/src/App.tsx +0 -157
- package/frontend/src/components/DisclaimerDialog.tsx +0 -106
- package/frontend/src/components/GitHubLoginDialog.tsx +0 -314
- package/frontend/src/components/OnboardingDialog.tsx +0 -185
- package/frontend/src/components/PrivacyOptInDialog.tsx +0 -130
- package/frontend/src/components/ProvidePatDialog.tsx +0 -98
- package/frontend/src/components/TaskTemplateManager.tsx +0 -336
- package/frontend/src/components/config-provider.tsx +0 -119
- package/frontend/src/components/context/TaskDetailsContextProvider.tsx +0 -470
- package/frontend/src/components/context/taskDetailsContext.ts +0 -125
- package/frontend/src/components/keyboard-shortcuts-demo.tsx +0 -35
- package/frontend/src/components/layout/navbar.tsx +0 -86
- package/frontend/src/components/logo.tsx +0 -44
- package/frontend/src/components/projects/ProjectCard.tsx +0 -155
- package/frontend/src/components/projects/project-detail.tsx +0 -251
- package/frontend/src/components/projects/project-form-fields.tsx +0 -238
- package/frontend/src/components/projects/project-form.tsx +0 -301
- package/frontend/src/components/projects/project-list.tsx +0 -200
- package/frontend/src/components/projects/projects-page.tsx +0 -20
- package/frontend/src/components/tasks/BranchSelector.tsx +0 -169
- package/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx +0 -94
- package/frontend/src/components/tasks/EditorSelectionDialog.tsx +0 -119
- package/frontend/src/components/tasks/TaskCard.tsx +0 -154
- package/frontend/src/components/tasks/TaskDetails/CollapsibleToolbar.tsx +0 -33
- package/frontend/src/components/tasks/TaskDetails/DiffCard.tsx +0 -109
- package/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx +0 -135
- package/frontend/src/components/tasks/TaskDetails/DiffFile.tsx +0 -296
- package/frontend/src/components/tasks/TaskDetails/DiffTab.tsx +0 -32
- package/frontend/src/components/tasks/TaskDetails/DisplayConversationEntry.tsx +0 -392
- package/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx +0 -256
- package/frontend/src/components/tasks/TaskDetails/LogsTab/ConversationEntry.tsx +0 -56
- package/frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx +0 -92
- package/frontend/src/components/tasks/TaskDetails/LogsTab/Prompt.tsx +0 -22
- package/frontend/src/components/tasks/TaskDetails/LogsTab/SetupScriptRunning.tsx +0 -49
- package/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +0 -186
- package/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +0 -288
- package/frontend/src/components/tasks/TaskDetails/RelatedTasksTab.tsx +0 -216
- package/frontend/src/components/tasks/TaskDetails/TabNavigation.tsx +0 -93
- package/frontend/src/components/tasks/TaskDetailsHeader.tsx +0 -169
- package/frontend/src/components/tasks/TaskDetailsPanel.tsx +0 -126
- package/frontend/src/components/tasks/TaskDetailsToolbar.tsx +0 -302
- package/frontend/src/components/tasks/TaskFollowUpSection.tsx +0 -130
- package/frontend/src/components/tasks/TaskFormDialog.tsx +0 -400
- package/frontend/src/components/tasks/TaskKanbanBoard.tsx +0 -180
- package/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx +0 -259
- package/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx +0 -243
- package/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx +0 -899
- package/frontend/src/components/tasks/index.ts +0 -2
- package/frontend/src/components/theme-provider.tsx +0 -82
- package/frontend/src/components/theme-toggle.tsx +0 -36
- package/frontend/src/components/ui/alert.tsx +0 -59
- package/frontend/src/components/ui/auto-expanding-textarea.tsx +0 -70
- package/frontend/src/components/ui/badge.tsx +0 -36
- package/frontend/src/components/ui/button.tsx +0 -56
- package/frontend/src/components/ui/card.tsx +0 -86
- package/frontend/src/components/ui/checkbox.tsx +0 -44
- package/frontend/src/components/ui/chip.tsx +0 -25
- package/frontend/src/components/ui/dialog.tsx +0 -124
- package/frontend/src/components/ui/dropdown-menu.tsx +0 -198
- package/frontend/src/components/ui/file-search-textarea.tsx +0 -292
- package/frontend/src/components/ui/folder-picker.tsx +0 -279
- package/frontend/src/components/ui/input.tsx +0 -25
- package/frontend/src/components/ui/label.tsx +0 -24
- package/frontend/src/components/ui/loader.tsx +0 -26
- package/frontend/src/components/ui/markdown-renderer.tsx +0 -75
- package/frontend/src/components/ui/select.tsx +0 -160
- package/frontend/src/components/ui/separator.tsx +0 -31
- package/frontend/src/components/ui/shadcn-io/kanban/index.tsx +0 -185
- package/frontend/src/components/ui/table.tsx +0 -117
- package/frontend/src/components/ui/tabs.tsx +0 -53
- package/frontend/src/components/ui/textarea.tsx +0 -22
- package/frontend/src/components/ui/tooltip.tsx +0 -28
- package/frontend/src/hooks/useNormalizedConversation.ts +0 -440
- package/frontend/src/index.css +0 -225
- package/frontend/src/lib/api.ts +0 -630
- package/frontend/src/lib/keyboard-shortcuts.ts +0 -266
- package/frontend/src/lib/responsive-config.ts +0 -70
- package/frontend/src/lib/types.ts +0 -39
- package/frontend/src/lib/utils.ts +0 -10
- package/frontend/src/main.tsx +0 -50
- package/frontend/src/pages/McpServers.tsx +0 -418
- package/frontend/src/pages/Settings.tsx +0 -610
- package/frontend/src/pages/project-tasks.tsx +0 -575
- package/frontend/src/pages/projects.tsx +0 -18
- package/frontend/src/vite-env.d.ts +0 -1
- package/frontend/tailwind.config.js +0 -125
- package/frontend/tsconfig.json +0 -26
- package/frontend/tsconfig.node.json +0 -10
- package/frontend/vite.config.ts +0 -33
- package/npx-cli/README.md +0 -159
- package/npx-cli/automagik-forge-0.1.0.tgz +0 -0
- package/npx-cli/automagik-forge-0.1.10.tgz +0 -0
- package/npx-cli/package.json +0 -17
- package/npx-cli/vibe-kanban-0.0.55.tgz +0 -0
- package/pnpm-workspace.yaml +0 -2
- package/rust-toolchain.toml +0 -11
- package/rustfmt.toml +0 -3
- package/scripts/load-env.js +0 -43
- package/scripts/mcp_test.js +0 -374
- package/scripts/prepare-db.js +0 -45
- package/scripts/setup-dev-environment.js +0 -274
- package/scripts/start-mcp-sse.js +0 -70
- package/scripts/test-debug.js +0 -32
- package/scripts/test-mcp-sse.js +0 -138
- package/scripts/test-simple.js +0 -44
- package/scripts/test-wish-final.js +0 -179
- package/scripts/test-wish-system.js +0 -221
- package/shared/types.ts +0 -182
- package/test-npm-package.sh +0 -42
- /package/{npx-cli/bin → bin}/cli.js +0 -0
@@ -1,1214 +0,0 @@
|
|
1
|
-
use std::path::Path;
|
2
|
-
|
3
|
-
use chrono::{DateTime, Utc};
|
4
|
-
use git2::{BranchType, Error as GitError, Repository};
|
5
|
-
use serde::{Deserialize, Serialize};
|
6
|
-
use sqlx::{FromRow, SqlitePool, Type};
|
7
|
-
use tracing::info;
|
8
|
-
use ts_rs::TS;
|
9
|
-
use utoipa::ToSchema;
|
10
|
-
use uuid::Uuid;
|
11
|
-
|
12
|
-
use super::{project::Project, task::Task};
|
13
|
-
use crate::services::{
|
14
|
-
CreatePrRequest, GitHubRepoInfo, GitHubService, GitHubServiceError, GitService,
|
15
|
-
GitServiceError, ProcessService,
|
16
|
-
};
|
17
|
-
|
18
|
-
// Constants for git diff operations
|
19
|
-
const GIT_DIFF_CONTEXT_LINES: u32 = 3;
|
20
|
-
const GIT_DIFF_INTERHUNK_LINES: u32 = 0;
|
21
|
-
|
22
|
-
#[derive(Debug)]
|
23
|
-
pub enum TaskAttemptError {
|
24
|
-
Database(sqlx::Error),
|
25
|
-
Git(GitError),
|
26
|
-
GitService(GitServiceError),
|
27
|
-
GitHubService(GitHubServiceError),
|
28
|
-
TaskNotFound,
|
29
|
-
ProjectNotFound,
|
30
|
-
ValidationError(String),
|
31
|
-
BranchNotFound(String),
|
32
|
-
}
|
33
|
-
|
34
|
-
impl std::fmt::Display for TaskAttemptError {
|
35
|
-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
36
|
-
match self {
|
37
|
-
TaskAttemptError::Database(e) => write!(f, "Database error: {}", e),
|
38
|
-
TaskAttemptError::Git(e) => write!(f, "Git error: {}", e),
|
39
|
-
TaskAttemptError::GitService(e) => write!(f, "Git service error: {}", e),
|
40
|
-
TaskAttemptError::GitHubService(e) => write!(f, "GitHub service error: {}", e),
|
41
|
-
TaskAttemptError::TaskNotFound => write!(f, "Task not found"),
|
42
|
-
TaskAttemptError::ProjectNotFound => write!(f, "Project not found"),
|
43
|
-
TaskAttemptError::ValidationError(e) => write!(f, "Validation error: {}", e),
|
44
|
-
TaskAttemptError::BranchNotFound(branch) => write!(f, "Branch '{}' not found", branch),
|
45
|
-
}
|
46
|
-
}
|
47
|
-
}
|
48
|
-
|
49
|
-
impl std::error::Error for TaskAttemptError {}
|
50
|
-
|
51
|
-
impl From<sqlx::Error> for TaskAttemptError {
|
52
|
-
fn from(err: sqlx::Error) -> Self {
|
53
|
-
TaskAttemptError::Database(err)
|
54
|
-
}
|
55
|
-
}
|
56
|
-
|
57
|
-
impl From<GitError> for TaskAttemptError {
|
58
|
-
fn from(err: GitError) -> Self {
|
59
|
-
TaskAttemptError::Git(err)
|
60
|
-
}
|
61
|
-
}
|
62
|
-
|
63
|
-
impl From<GitServiceError> for TaskAttemptError {
|
64
|
-
fn from(err: GitServiceError) -> Self {
|
65
|
-
TaskAttemptError::GitService(err)
|
66
|
-
}
|
67
|
-
}
|
68
|
-
|
69
|
-
impl From<GitHubServiceError> for TaskAttemptError {
|
70
|
-
fn from(err: GitHubServiceError) -> Self {
|
71
|
-
TaskAttemptError::GitHubService(err)
|
72
|
-
}
|
73
|
-
}
|
74
|
-
|
75
|
-
#[derive(Debug, Clone, Type, Serialize, Deserialize, PartialEq, TS, ToSchema)]
|
76
|
-
#[sqlx(type_name = "task_attempt_status", rename_all = "lowercase")]
|
77
|
-
#[serde(rename_all = "lowercase")]
|
78
|
-
#[ts(export)]
|
79
|
-
pub enum TaskAttemptStatus {
|
80
|
-
SetupRunning,
|
81
|
-
SetupComplete,
|
82
|
-
SetupFailed,
|
83
|
-
ExecutorRunning,
|
84
|
-
ExecutorComplete,
|
85
|
-
ExecutorFailed,
|
86
|
-
}
|
87
|
-
|
88
|
-
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS, ToSchema)]
|
89
|
-
#[ts(export)]
|
90
|
-
pub struct TaskAttempt {
|
91
|
-
pub id: Uuid,
|
92
|
-
pub task_id: Uuid, // Foreign key to Task
|
93
|
-
pub worktree_path: String,
|
94
|
-
pub branch: String, // Git branch name for this task attempt
|
95
|
-
pub base_branch: String, // Base branch this attempt is based on
|
96
|
-
pub merge_commit: Option<String>,
|
97
|
-
pub executor: Option<String>, // Name of the executor to use
|
98
|
-
pub pr_url: Option<String>, // GitHub PR URL
|
99
|
-
pub pr_number: Option<i64>, // GitHub PR number
|
100
|
-
pub pr_status: Option<String>, // open, closed, merged
|
101
|
-
pub pr_merged_at: Option<DateTime<Utc>>, // When PR was merged
|
102
|
-
pub worktree_deleted: bool, // Flag indicating if worktree has been cleaned up
|
103
|
-
pub setup_completed_at: Option<DateTime<Utc>>, // When setup script was last completed
|
104
|
-
pub created_at: DateTime<Utc>,
|
105
|
-
pub updated_at: DateTime<Utc>,
|
106
|
-
}
|
107
|
-
|
108
|
-
#[derive(Debug, Deserialize, TS, ToSchema)]
|
109
|
-
#[ts(export)]
|
110
|
-
pub struct CreateTaskAttempt {
|
111
|
-
pub executor: Option<String>, // Optional executor name (defaults to "echo")
|
112
|
-
pub base_branch: Option<String>, // Optional base branch to checkout (defaults to current HEAD)
|
113
|
-
}
|
114
|
-
|
115
|
-
#[derive(Debug, Deserialize, TS, ToSchema)]
|
116
|
-
#[ts(export)]
|
117
|
-
pub struct UpdateTaskAttempt {
|
118
|
-
// Currently no updateable fields, but keeping struct for API compatibility
|
119
|
-
}
|
120
|
-
|
121
|
-
/// GitHub PR creation parameters
|
122
|
-
pub struct CreatePrParams<'a> {
|
123
|
-
pub attempt_id: Uuid,
|
124
|
-
pub task_id: Uuid,
|
125
|
-
pub project_id: Uuid,
|
126
|
-
pub github_token: &'a str,
|
127
|
-
pub title: &'a str,
|
128
|
-
pub body: Option<&'a str>,
|
129
|
-
pub base_branch: Option<&'a str>,
|
130
|
-
}
|
131
|
-
|
132
|
-
#[derive(Debug, Deserialize, TS, ToSchema)]
|
133
|
-
#[ts(export)]
|
134
|
-
pub struct CreateFollowUpAttempt {
|
135
|
-
pub prompt: String,
|
136
|
-
}
|
137
|
-
|
138
|
-
#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
|
139
|
-
#[ts(export)]
|
140
|
-
pub enum DiffChunkType {
|
141
|
-
Equal,
|
142
|
-
Insert,
|
143
|
-
Delete,
|
144
|
-
}
|
145
|
-
|
146
|
-
#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
|
147
|
-
#[ts(export)]
|
148
|
-
pub struct DiffChunk {
|
149
|
-
pub chunk_type: DiffChunkType,
|
150
|
-
pub content: String,
|
151
|
-
}
|
152
|
-
|
153
|
-
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
154
|
-
#[ts(export)]
|
155
|
-
pub struct FileDiff {
|
156
|
-
pub path: String,
|
157
|
-
pub chunks: Vec<DiffChunk>,
|
158
|
-
}
|
159
|
-
|
160
|
-
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
161
|
-
#[ts(export)]
|
162
|
-
pub struct WorktreeDiff {
|
163
|
-
pub files: Vec<FileDiff>,
|
164
|
-
}
|
165
|
-
|
166
|
-
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
167
|
-
#[ts(export)]
|
168
|
-
pub struct BranchStatus {
|
169
|
-
pub is_behind: bool,
|
170
|
-
pub commits_behind: usize,
|
171
|
-
pub commits_ahead: usize,
|
172
|
-
pub up_to_date: bool,
|
173
|
-
pub merged: bool,
|
174
|
-
pub has_uncommitted_changes: bool,
|
175
|
-
pub base_branch_name: String,
|
176
|
-
}
|
177
|
-
|
178
|
-
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
179
|
-
#[ts(export)]
|
180
|
-
pub enum ExecutionState {
|
181
|
-
NotStarted,
|
182
|
-
SetupRunning,
|
183
|
-
SetupComplete,
|
184
|
-
SetupFailed,
|
185
|
-
SetupStopped,
|
186
|
-
CodingAgentRunning,
|
187
|
-
CodingAgentComplete,
|
188
|
-
CodingAgentFailed,
|
189
|
-
CodingAgentStopped,
|
190
|
-
Complete,
|
191
|
-
}
|
192
|
-
|
193
|
-
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
194
|
-
#[ts(export)]
|
195
|
-
pub struct TaskAttemptState {
|
196
|
-
pub execution_state: ExecutionState,
|
197
|
-
pub has_changes: bool,
|
198
|
-
pub has_setup_script: bool,
|
199
|
-
pub setup_process_id: Option<String>,
|
200
|
-
pub coding_agent_process_id: Option<String>,
|
201
|
-
}
|
202
|
-
|
203
|
-
/// Context data for resume operations (simplified)
|
204
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
205
|
-
pub struct AttemptResumeContext {
|
206
|
-
pub execution_history: String,
|
207
|
-
pub cumulative_diffs: String,
|
208
|
-
}
|
209
|
-
|
210
|
-
#[derive(Debug)]
|
211
|
-
pub struct TaskAttemptContext {
|
212
|
-
pub task_attempt: TaskAttempt,
|
213
|
-
pub task: Task,
|
214
|
-
pub project: Project,
|
215
|
-
}
|
216
|
-
|
217
|
-
impl TaskAttempt {
|
218
|
-
/// Load task attempt with full validation - ensures task_attempt belongs to task and task belongs to project
|
219
|
-
pub async fn load_context(
|
220
|
-
pool: &SqlitePool,
|
221
|
-
attempt_id: Uuid,
|
222
|
-
task_id: Uuid,
|
223
|
-
project_id: Uuid,
|
224
|
-
) -> Result<TaskAttemptContext, TaskAttemptError> {
|
225
|
-
// Single query with JOIN validation to ensure proper relationships
|
226
|
-
let task_attempt = sqlx::query_as!(
|
227
|
-
TaskAttempt,
|
228
|
-
r#"SELECT ta.id AS "id!: Uuid",
|
229
|
-
ta.task_id AS "task_id!: Uuid",
|
230
|
-
ta.worktree_path,
|
231
|
-
ta.branch,
|
232
|
-
ta.base_branch,
|
233
|
-
ta.merge_commit,
|
234
|
-
ta.executor,
|
235
|
-
ta.pr_url,
|
236
|
-
ta.pr_number,
|
237
|
-
ta.pr_status,
|
238
|
-
ta.pr_merged_at AS "pr_merged_at: DateTime<Utc>",
|
239
|
-
ta.worktree_deleted AS "worktree_deleted!: bool",
|
240
|
-
ta.setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
241
|
-
ta.created_at AS "created_at!: DateTime<Utc>",
|
242
|
-
ta.updated_at AS "updated_at!: DateTime<Utc>"
|
243
|
-
FROM task_attempts ta
|
244
|
-
JOIN tasks t ON ta.task_id = t.id
|
245
|
-
JOIN projects p ON t.project_id = p.id
|
246
|
-
WHERE ta.id = $1 AND t.id = $2 AND p.id = $3"#,
|
247
|
-
attempt_id,
|
248
|
-
task_id,
|
249
|
-
project_id
|
250
|
-
)
|
251
|
-
.fetch_optional(pool)
|
252
|
-
.await?
|
253
|
-
.ok_or(TaskAttemptError::TaskNotFound)?;
|
254
|
-
|
255
|
-
// Load task and project (we know they exist due to JOIN validation)
|
256
|
-
let task = Task::find_by_id(pool, task_id)
|
257
|
-
.await?
|
258
|
-
.ok_or(TaskAttemptError::TaskNotFound)?;
|
259
|
-
|
260
|
-
let project = Project::find_by_id(pool, project_id)
|
261
|
-
.await?
|
262
|
-
.ok_or(TaskAttemptError::ProjectNotFound)?;
|
263
|
-
|
264
|
-
Ok(TaskAttemptContext {
|
265
|
-
task_attempt,
|
266
|
-
task,
|
267
|
-
project,
|
268
|
-
})
|
269
|
-
}
|
270
|
-
|
271
|
-
/// Helper function to mark a worktree as deleted in the database
|
272
|
-
pub async fn mark_worktree_deleted(
|
273
|
-
pool: &SqlitePool,
|
274
|
-
attempt_id: Uuid,
|
275
|
-
) -> Result<(), sqlx::Error> {
|
276
|
-
sqlx::query!(
|
277
|
-
"UPDATE task_attempts SET worktree_deleted = TRUE, updated_at = datetime('now') WHERE id = ?",
|
278
|
-
attempt_id
|
279
|
-
)
|
280
|
-
.execute(pool)
|
281
|
-
.await?;
|
282
|
-
Ok(())
|
283
|
-
}
|
284
|
-
|
285
|
-
/// Get the base directory for automagik-forge worktrees
|
286
|
-
pub fn get_worktree_base_dir() -> std::path::PathBuf {
|
287
|
-
let dir_name = if cfg!(debug_assertions) {
|
288
|
-
"automagik-forge-dev"
|
289
|
-
} else {
|
290
|
-
"automagik-forge"
|
291
|
-
};
|
292
|
-
|
293
|
-
if cfg!(target_os = "macos") {
|
294
|
-
// macOS already uses /var/folders/... which is persistent storage
|
295
|
-
std::env::temp_dir().join(dir_name)
|
296
|
-
} else if cfg!(target_os = "linux") {
|
297
|
-
// Linux: use /var/tmp instead of /tmp to avoid RAM usage
|
298
|
-
std::path::PathBuf::from("/var/tmp").join(dir_name)
|
299
|
-
} else {
|
300
|
-
// Windows and other platforms: use temp dir with automagik-forge subdirectory
|
301
|
-
std::env::temp_dir().join(dir_name)
|
302
|
-
}
|
303
|
-
}
|
304
|
-
|
305
|
-
pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
|
306
|
-
sqlx::query_as!(
|
307
|
-
TaskAttempt,
|
308
|
-
r#"SELECT id AS "id!: Uuid",
|
309
|
-
task_id AS "task_id!: Uuid",
|
310
|
-
worktree_path,
|
311
|
-
branch,
|
312
|
-
merge_commit,
|
313
|
-
base_branch,
|
314
|
-
executor,
|
315
|
-
pr_url,
|
316
|
-
pr_number,
|
317
|
-
pr_status,
|
318
|
-
pr_merged_at AS "pr_merged_at: DateTime<Utc>",
|
319
|
-
worktree_deleted AS "worktree_deleted!: bool",
|
320
|
-
setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
321
|
-
created_at AS "created_at!: DateTime<Utc>",
|
322
|
-
updated_at AS "updated_at!: DateTime<Utc>"
|
323
|
-
FROM task_attempts
|
324
|
-
WHERE id = $1"#,
|
325
|
-
id
|
326
|
-
)
|
327
|
-
.fetch_optional(pool)
|
328
|
-
.await
|
329
|
-
}
|
330
|
-
|
331
|
-
pub async fn find_by_task_id(
|
332
|
-
pool: &SqlitePool,
|
333
|
-
task_id: Uuid,
|
334
|
-
) -> Result<Vec<Self>, sqlx::Error> {
|
335
|
-
sqlx::query_as!(
|
336
|
-
TaskAttempt,
|
337
|
-
r#"SELECT id AS "id!: Uuid",
|
338
|
-
task_id AS "task_id!: Uuid",
|
339
|
-
worktree_path,
|
340
|
-
branch,
|
341
|
-
base_branch,
|
342
|
-
merge_commit,
|
343
|
-
executor,
|
344
|
-
pr_url,
|
345
|
-
pr_number,
|
346
|
-
pr_status,
|
347
|
-
pr_merged_at AS "pr_merged_at: DateTime<Utc>",
|
348
|
-
worktree_deleted AS "worktree_deleted!: bool",
|
349
|
-
setup_completed_at AS "setup_completed_at: DateTime<Utc>",
|
350
|
-
created_at AS "created_at!: DateTime<Utc>",
|
351
|
-
updated_at AS "updated_at!: DateTime<Utc>"
|
352
|
-
FROM task_attempts
|
353
|
-
WHERE task_id = $1
|
354
|
-
ORDER BY created_at DESC"#,
|
355
|
-
task_id
|
356
|
-
)
|
357
|
-
.fetch_all(pool)
|
358
|
-
.await
|
359
|
-
}
|
360
|
-
|
361
|
-
/// Find task attempts by task_id with project git repo path for cleanup operations
|
362
|
-
pub async fn find_by_task_id_with_project(
|
363
|
-
pool: &SqlitePool,
|
364
|
-
task_id: Uuid,
|
365
|
-
) -> Result<Vec<(Uuid, String, String)>, sqlx::Error> {
|
366
|
-
let records = sqlx::query!(
|
367
|
-
r#"
|
368
|
-
SELECT ta.id as "attempt_id!: Uuid", ta.worktree_path, p.git_repo_path as "git_repo_path!"
|
369
|
-
FROM task_attempts ta
|
370
|
-
JOIN tasks t ON ta.task_id = t.id
|
371
|
-
JOIN projects p ON t.project_id = p.id
|
372
|
-
WHERE ta.task_id = $1
|
373
|
-
"#,
|
374
|
-
task_id
|
375
|
-
)
|
376
|
-
.fetch_all(pool)
|
377
|
-
.await?;
|
378
|
-
|
379
|
-
Ok(records
|
380
|
-
.into_iter()
|
381
|
-
.map(|r| (r.attempt_id, r.worktree_path, r.git_repo_path))
|
382
|
-
.collect())
|
383
|
-
}
|
384
|
-
|
385
|
-
/// Find task attempts that are expired (24+ hours since last activity) and eligible for worktree cleanup
|
386
|
-
/// Activity includes: execution completion, task attempt updates (including worktree recreation),
|
387
|
-
/// and any attempts that are currently in progress
|
388
|
-
pub async fn find_expired_for_cleanup(
|
389
|
-
pool: &SqlitePool,
|
390
|
-
) -> Result<Vec<(Uuid, String, String)>, sqlx::Error> {
|
391
|
-
let records = sqlx::query!(
|
392
|
-
r#"
|
393
|
-
SELECT ta.id as "attempt_id!: Uuid", ta.worktree_path, p.git_repo_path as "git_repo_path!"
|
394
|
-
FROM task_attempts ta
|
395
|
-
LEFT JOIN execution_processes ep ON ta.id = ep.task_attempt_id AND ep.completed_at IS NOT NULL
|
396
|
-
JOIN tasks t ON ta.task_id = t.id
|
397
|
-
JOIN projects p ON t.project_id = p.id
|
398
|
-
WHERE ta.worktree_deleted = FALSE
|
399
|
-
-- Exclude attempts with any running processes (in progress)
|
400
|
-
AND ta.id NOT IN (
|
401
|
-
SELECT DISTINCT ep2.task_attempt_id
|
402
|
-
FROM execution_processes ep2
|
403
|
-
WHERE ep2.completed_at IS NULL
|
404
|
-
)
|
405
|
-
GROUP BY ta.id, ta.worktree_path, p.git_repo_path, ta.updated_at
|
406
|
-
HAVING datetime('now', '-24 hours') > datetime(
|
407
|
-
MAX(
|
408
|
-
CASE
|
409
|
-
WHEN ep.completed_at IS NOT NULL THEN ep.completed_at
|
410
|
-
ELSE ta.updated_at
|
411
|
-
END
|
412
|
-
)
|
413
|
-
)
|
414
|
-
ORDER BY MAX(
|
415
|
-
CASE
|
416
|
-
WHEN ep.completed_at IS NOT NULL THEN ep.completed_at
|
417
|
-
ELSE ta.updated_at
|
418
|
-
END
|
419
|
-
) ASC
|
420
|
-
"#
|
421
|
-
)
|
422
|
-
.fetch_all(pool)
|
423
|
-
.await?;
|
424
|
-
|
425
|
-
Ok(records
|
426
|
-
.into_iter()
|
427
|
-
.filter_map(|r| {
|
428
|
-
r.worktree_path
|
429
|
-
.map(|path| (r.attempt_id, path, r.git_repo_path))
|
430
|
-
})
|
431
|
-
.collect())
|
432
|
-
}
|
433
|
-
|
434
|
-
pub async fn create(
|
435
|
-
pool: &SqlitePool,
|
436
|
-
data: &CreateTaskAttempt,
|
437
|
-
task_id: Uuid,
|
438
|
-
) -> Result<Self, TaskAttemptError> {
|
439
|
-
let attempt_id = Uuid::new_v4();
|
440
|
-
// let prefixed_id = format!("automagik-forge-{}", attempt_id);
|
441
|
-
|
442
|
-
// First, get the task to get the project_id
|
443
|
-
let task = Task::find_by_id(pool, task_id)
|
444
|
-
.await?
|
445
|
-
.ok_or(TaskAttemptError::TaskNotFound)?;
|
446
|
-
|
447
|
-
// Create a unique and helpful branch name
|
448
|
-
let task_title_id = crate::utils::text::git_branch_id(&task.title);
|
449
|
-
let task_attempt_branch = format!(
|
450
|
-
"vk-{}-{}",
|
451
|
-
crate::utils::text::short_uuid(&attempt_id),
|
452
|
-
task_title_id
|
453
|
-
);
|
454
|
-
|
455
|
-
// Generate worktree path using automagik-forge specific directory
|
456
|
-
let worktree_path = Self::get_worktree_base_dir().join(&task_attempt_branch);
|
457
|
-
let worktree_path_str = worktree_path.to_string_lossy().to_string();
|
458
|
-
|
459
|
-
// Then get the project using the project_id
|
460
|
-
let project = Project::find_by_id(pool, task.project_id)
|
461
|
-
.await?
|
462
|
-
.ok_or(TaskAttemptError::ProjectNotFound)?;
|
463
|
-
|
464
|
-
// Create GitService instance
|
465
|
-
let git_service = GitService::new(&project.git_repo_path)?;
|
466
|
-
|
467
|
-
// Determine the resolved base branch name first
|
468
|
-
let resolved_base_branch = if let Some(ref base_branch) = data.base_branch {
|
469
|
-
base_branch.clone()
|
470
|
-
} else {
|
471
|
-
// Default to current HEAD branch name or "main"
|
472
|
-
git_service.get_default_branch_name()?
|
473
|
-
};
|
474
|
-
|
475
|
-
// Create the worktree using GitService
|
476
|
-
git_service.create_worktree(
|
477
|
-
&task_attempt_branch,
|
478
|
-
&worktree_path,
|
479
|
-
data.base_branch.as_deref(),
|
480
|
-
)?;
|
481
|
-
|
482
|
-
// Insert the record into the database
|
483
|
-
Ok(sqlx::query_as!(
|
484
|
-
TaskAttempt,
|
485
|
-
r#"INSERT INTO task_attempts (id, task_id, worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at, worktree_deleted, setup_completed_at)
|
486
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
487
|
-
RETURNING id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as "pr_merged_at: DateTime<Utc>", worktree_deleted as "worktree_deleted!: bool", setup_completed_at as "setup_completed_at: DateTime<Utc>", created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
|
488
|
-
attempt_id,
|
489
|
-
task_id,
|
490
|
-
worktree_path_str,
|
491
|
-
task_attempt_branch,
|
492
|
-
resolved_base_branch,
|
493
|
-
Option::<String>::None, // merge_commit is always None during creation
|
494
|
-
data.executor,
|
495
|
-
Option::<String>::None, // pr_url is None during creation
|
496
|
-
Option::<i64>::None, // pr_number is None during creation
|
497
|
-
Option::<String>::None, // pr_status is None during creation
|
498
|
-
Option::<DateTime<Utc>>::None, // pr_merged_at is None during creation
|
499
|
-
false, // worktree_deleted is false during creation
|
500
|
-
Option::<DateTime<Utc>>::None // setup_completed_at is None during creation
|
501
|
-
)
|
502
|
-
.fetch_one(pool)
|
503
|
-
.await?)
|
504
|
-
}
|
505
|
-
|
506
|
-
/// Perform the actual merge operation using GitService
|
507
|
-
fn perform_merge_operation(
|
508
|
-
worktree_path: &str,
|
509
|
-
main_repo_path: &str,
|
510
|
-
branch_name: &str,
|
511
|
-
base_branch: &str,
|
512
|
-
task_title: &str,
|
513
|
-
task_description: &Option<String>,
|
514
|
-
task_id: Uuid,
|
515
|
-
) -> Result<String, TaskAttemptError> {
|
516
|
-
let git_service = GitService::new(main_repo_path)?;
|
517
|
-
let worktree_path = Path::new(worktree_path);
|
518
|
-
|
519
|
-
// Extract first section of UUID (before first hyphen)
|
520
|
-
let task_uuid_str = task_id.to_string();
|
521
|
-
let first_uuid_section = task_uuid_str.split('-').next().unwrap_or(&task_uuid_str);
|
522
|
-
|
523
|
-
// Create commit message with task title and description
|
524
|
-
let mut commit_message = format!("{} (automagik-forge {})", task_title, first_uuid_section);
|
525
|
-
|
526
|
-
// Add description on next line if it exists
|
527
|
-
if let Some(description) = task_description {
|
528
|
-
if !description.trim().is_empty() {
|
529
|
-
commit_message.push_str("\n\n");
|
530
|
-
commit_message.push_str(description);
|
531
|
-
}
|
532
|
-
}
|
533
|
-
|
534
|
-
git_service
|
535
|
-
.merge_changes(worktree_path, branch_name, base_branch, &commit_message)
|
536
|
-
.map_err(TaskAttemptError::from)
|
537
|
-
}
|
538
|
-
|
539
|
-
/// Perform the actual git rebase operations using GitService
|
540
|
-
fn perform_rebase_operation(
|
541
|
-
worktree_path: &str,
|
542
|
-
main_repo_path: &str,
|
543
|
-
new_base_branch: Option<String>,
|
544
|
-
old_base_branch: String,
|
545
|
-
) -> Result<String, TaskAttemptError> {
|
546
|
-
let git_service = GitService::new(main_repo_path)?;
|
547
|
-
let worktree_path = Path::new(worktree_path);
|
548
|
-
|
549
|
-
git_service
|
550
|
-
.rebase_branch(worktree_path, new_base_branch.as_deref(), &old_base_branch)
|
551
|
-
.map_err(TaskAttemptError::from)
|
552
|
-
}
|
553
|
-
|
554
|
-
/// Merge the worktree changes back to the main repository
|
555
|
-
pub async fn merge_changes(
|
556
|
-
pool: &SqlitePool,
|
557
|
-
attempt_id: Uuid,
|
558
|
-
task_id: Uuid,
|
559
|
-
project_id: Uuid,
|
560
|
-
) -> Result<String, TaskAttemptError> {
|
561
|
-
// Load context with full validation
|
562
|
-
let ctx = TaskAttempt::load_context(pool, attempt_id, task_id, project_id).await?;
|
563
|
-
|
564
|
-
// Ensure worktree exists (recreate if needed for cold task support)
|
565
|
-
let worktree_path =
|
566
|
-
Self::ensure_worktree_exists(pool, attempt_id, project_id, "merge").await?;
|
567
|
-
|
568
|
-
// Perform the actual merge operation
|
569
|
-
let merge_commit_id = Self::perform_merge_operation(
|
570
|
-
&worktree_path,
|
571
|
-
&ctx.project.git_repo_path,
|
572
|
-
&ctx.task_attempt.branch,
|
573
|
-
&ctx.task_attempt.base_branch,
|
574
|
-
&ctx.task.title,
|
575
|
-
&ctx.task.description,
|
576
|
-
ctx.task.id,
|
577
|
-
)?;
|
578
|
-
|
579
|
-
// Update the task attempt with the merge commit
|
580
|
-
sqlx::query!(
|
581
|
-
"UPDATE task_attempts SET merge_commit = $1, updated_at = datetime('now') WHERE id = $2",
|
582
|
-
merge_commit_id,
|
583
|
-
attempt_id
|
584
|
-
)
|
585
|
-
.execute(pool)
|
586
|
-
.await?;
|
587
|
-
|
588
|
-
Ok(merge_commit_id)
|
589
|
-
}
|
590
|
-
|
591
|
-
/// Start the execution flow for a task attempt (setup script + executor)
|
592
|
-
pub async fn start_execution(
|
593
|
-
pool: &SqlitePool,
|
594
|
-
app_state: &crate::app_state::AppState,
|
595
|
-
attempt_id: Uuid,
|
596
|
-
task_id: Uuid,
|
597
|
-
project_id: Uuid,
|
598
|
-
) -> Result<(), TaskAttemptError> {
|
599
|
-
ProcessService::start_execution(pool, app_state, attempt_id, task_id, project_id).await
|
600
|
-
}
|
601
|
-
|
602
|
-
/// Start a dev server for this task attempt
|
603
|
-
pub async fn start_dev_server(
|
604
|
-
pool: &SqlitePool,
|
605
|
-
app_state: &crate::app_state::AppState,
|
606
|
-
attempt_id: Uuid,
|
607
|
-
task_id: Uuid,
|
608
|
-
project_id: Uuid,
|
609
|
-
) -> Result<(), TaskAttemptError> {
|
610
|
-
ProcessService::start_dev_server(pool, app_state, attempt_id, task_id, project_id).await
|
611
|
-
}
|
612
|
-
|
613
|
-
/// Start a follow-up execution using the same executor type as the first process
|
614
|
-
/// Returns the attempt_id that was actually used (always the original attempt_id for session continuity)
|
615
|
-
pub async fn start_followup_execution(
|
616
|
-
pool: &SqlitePool,
|
617
|
-
app_state: &crate::app_state::AppState,
|
618
|
-
attempt_id: Uuid,
|
619
|
-
task_id: Uuid,
|
620
|
-
project_id: Uuid,
|
621
|
-
prompt: &str,
|
622
|
-
) -> Result<Uuid, TaskAttemptError> {
|
623
|
-
ProcessService::start_followup_execution(
|
624
|
-
pool, app_state, attempt_id, task_id, project_id, prompt,
|
625
|
-
)
|
626
|
-
.await
|
627
|
-
}
|
628
|
-
|
629
|
-
/// Ensure worktree exists, recreating from branch if needed (cold task support)
|
630
|
-
pub async fn ensure_worktree_exists(
|
631
|
-
pool: &SqlitePool,
|
632
|
-
attempt_id: Uuid,
|
633
|
-
project_id: Uuid,
|
634
|
-
context: &str,
|
635
|
-
) -> Result<String, TaskAttemptError> {
|
636
|
-
let task_attempt = TaskAttempt::find_by_id(pool, attempt_id)
|
637
|
-
.await?
|
638
|
-
.ok_or(TaskAttemptError::TaskNotFound)?;
|
639
|
-
|
640
|
-
// Return existing path if worktree still exists
|
641
|
-
if std::path::Path::new(&task_attempt.worktree_path).exists() {
|
642
|
-
return Ok(task_attempt.worktree_path);
|
643
|
-
}
|
644
|
-
|
645
|
-
// Recreate worktree from branch
|
646
|
-
info!(
|
647
|
-
"Worktree {} no longer exists, recreating from branch {} for {}",
|
648
|
-
task_attempt.worktree_path, task_attempt.branch, context
|
649
|
-
);
|
650
|
-
|
651
|
-
let new_worktree_path =
|
652
|
-
Self::recreate_worktree_from_branch(pool, &task_attempt, project_id).await?;
|
653
|
-
|
654
|
-
// Update database with new path, reset worktree_deleted flag, and clear setup completion
|
655
|
-
sqlx::query!(
|
656
|
-
"UPDATE task_attempts SET worktree_path = $1, worktree_deleted = FALSE, setup_completed_at = NULL, updated_at = datetime('now') WHERE id = $2",
|
657
|
-
new_worktree_path,
|
658
|
-
attempt_id
|
659
|
-
)
|
660
|
-
.execute(pool)
|
661
|
-
.await?;
|
662
|
-
|
663
|
-
Ok(new_worktree_path)
|
664
|
-
}
|
665
|
-
|
666
|
-
/// Recreate a worktree from an existing branch (for cold task support)
|
667
|
-
pub async fn recreate_worktree_from_branch(
|
668
|
-
pool: &SqlitePool,
|
669
|
-
task_attempt: &TaskAttempt,
|
670
|
-
project_id: Uuid,
|
671
|
-
) -> Result<String, TaskAttemptError> {
|
672
|
-
let project = Project::find_by_id(pool, project_id)
|
673
|
-
.await?
|
674
|
-
.ok_or(TaskAttemptError::ProjectNotFound)?;
|
675
|
-
|
676
|
-
// Create GitService instance
|
677
|
-
let git_service = GitService::new(&project.git_repo_path)?;
|
678
|
-
|
679
|
-
// Use the stored worktree path from database - this ensures we recreate in the exact same location
|
680
|
-
// where Claude originally created its session, maintaining session continuity
|
681
|
-
let stored_worktree_path = std::path::PathBuf::from(&task_attempt.worktree_path);
|
682
|
-
|
683
|
-
let result_path = git_service
|
684
|
-
.recreate_worktree_from_branch(&task_attempt.branch, &stored_worktree_path)
|
685
|
-
.await?;
|
686
|
-
|
687
|
-
Ok(result_path.to_string_lossy().to_string())
|
688
|
-
}
|
689
|
-
|
690
|
-
/// Get the git diff between the base commit and the current committed worktree state
|
691
|
-
pub async fn get_diff(
|
692
|
-
pool: &SqlitePool,
|
693
|
-
attempt_id: Uuid,
|
694
|
-
task_id: Uuid,
|
695
|
-
project_id: Uuid,
|
696
|
-
) -> Result<WorktreeDiff, TaskAttemptError> {
|
697
|
-
// Load context with full validation
|
698
|
-
let ctx = TaskAttempt::load_context(pool, attempt_id, task_id, project_id).await?;
|
699
|
-
|
700
|
-
// Create GitService instance
|
701
|
-
let git_service = GitService::new(&ctx.project.git_repo_path)?;
|
702
|
-
|
703
|
-
if let Some(merge_commit_id) = &ctx.task_attempt.merge_commit {
|
704
|
-
// Task attempt has been merged - show the diff from the merge commit
|
705
|
-
git_service
|
706
|
-
.get_enhanced_diff(
|
707
|
-
Path::new(""),
|
708
|
-
Some(merge_commit_id),
|
709
|
-
&ctx.task_attempt.base_branch,
|
710
|
-
)
|
711
|
-
.map_err(TaskAttemptError::from)
|
712
|
-
} else {
|
713
|
-
// Task attempt not yet merged - get worktree diff
|
714
|
-
// Ensure worktree exists (recreate if needed for cold task support)
|
715
|
-
let worktree_path =
|
716
|
-
Self::ensure_worktree_exists(pool, attempt_id, project_id, "diff").await?;
|
717
|
-
|
718
|
-
git_service
|
719
|
-
.get_enhanced_diff(
|
720
|
-
Path::new(&worktree_path),
|
721
|
-
None,
|
722
|
-
&ctx.task_attempt.base_branch,
|
723
|
-
)
|
724
|
-
.map_err(TaskAttemptError::from)
|
725
|
-
}
|
726
|
-
}
|
727
|
-
|
728
|
-
/// Get the branch status for this task attempt
|
729
|
-
pub async fn get_branch_status(
|
730
|
-
pool: &SqlitePool,
|
731
|
-
attempt_id: Uuid,
|
732
|
-
task_id: Uuid,
|
733
|
-
project_id: Uuid,
|
734
|
-
) -> Result<BranchStatus, TaskAttemptError> {
|
735
|
-
// Load context with full validation
|
736
|
-
let ctx = TaskAttempt::load_context(pool, attempt_id, task_id, project_id).await?;
|
737
|
-
|
738
|
-
use git2::{Status, StatusOptions};
|
739
|
-
|
740
|
-
// Ensure worktree exists (recreate if needed for cold task support)
|
741
|
-
let main_repo = Repository::open(&ctx.project.git_repo_path)?;
|
742
|
-
let attempt_branch = ctx.task_attempt.branch.clone();
|
743
|
-
|
744
|
-
// ── locate the commit pointed to by the attempt branch ───────────────────────
|
745
|
-
let attempt_ref = main_repo
|
746
|
-
// try "refs/heads/<name>" first, then raw name
|
747
|
-
.find_reference(&format!("refs/heads/{}", attempt_branch))
|
748
|
-
.or_else(|_| main_repo.find_reference(&attempt_branch))?;
|
749
|
-
let attempt_oid = attempt_ref.target().unwrap();
|
750
|
-
|
751
|
-
// ── determine the base branch & ahead/behind counts ─────────────────────────
|
752
|
-
let base_branch_name = ctx.task_attempt.base_branch.clone();
|
753
|
-
|
754
|
-
// 1. prefer the branch’s configured upstream, if any
|
755
|
-
if let Ok(local_branch) = main_repo.find_branch(&attempt_branch, BranchType::Local) {
|
756
|
-
if let Ok(upstream) = local_branch.upstream() {
|
757
|
-
if let Some(_name) = upstream.name()? {
|
758
|
-
if let Some(base_oid) = upstream.get().target() {
|
759
|
-
let (_ahead, _behind) =
|
760
|
-
main_repo.graph_ahead_behind(attempt_oid, base_oid)?;
|
761
|
-
// Ignore upstream since we use stored base branch
|
762
|
-
}
|
763
|
-
}
|
764
|
-
}
|
765
|
-
}
|
766
|
-
|
767
|
-
// Calculate ahead/behind counts using the stored base branch
|
768
|
-
let (commits_ahead, commits_behind) =
|
769
|
-
if let Ok(base_branch) = main_repo.find_branch(&base_branch_name, BranchType::Local) {
|
770
|
-
if let Some(base_oid) = base_branch.get().target() {
|
771
|
-
main_repo.graph_ahead_behind(attempt_oid, base_oid)?
|
772
|
-
} else {
|
773
|
-
(0, 0) // Base branch has no commits
|
774
|
-
}
|
775
|
-
} else {
|
776
|
-
// Base branch doesn't exist, assume no relationship
|
777
|
-
(0, 0)
|
778
|
-
};
|
779
|
-
|
780
|
-
// ── detect any uncommitted / untracked changes ───────────────────────────────
|
781
|
-
let repo_for_status = Repository::open(&ctx.project.git_repo_path)?;
|
782
|
-
|
783
|
-
let mut status_opts = StatusOptions::new();
|
784
|
-
status_opts
|
785
|
-
.include_untracked(true)
|
786
|
-
.recurse_untracked_dirs(true)
|
787
|
-
.include_ignored(false);
|
788
|
-
|
789
|
-
let has_uncommitted_changes = repo_for_status
|
790
|
-
.statuses(Some(&mut status_opts))?
|
791
|
-
.iter()
|
792
|
-
.any(|e| e.status() != Status::CURRENT);
|
793
|
-
|
794
|
-
// ── assemble & return ────────────────────────────────────────────────────────
|
795
|
-
Ok(BranchStatus {
|
796
|
-
is_behind: commits_behind > 0,
|
797
|
-
commits_behind,
|
798
|
-
commits_ahead,
|
799
|
-
up_to_date: commits_behind == 0 && commits_ahead == 0,
|
800
|
-
merged: ctx.task_attempt.merge_commit.is_some(),
|
801
|
-
has_uncommitted_changes,
|
802
|
-
base_branch_name,
|
803
|
-
})
|
804
|
-
}
|
805
|
-
|
806
|
-
/// Rebase the worktree branch onto specified base branch (or current HEAD if none specified)
|
807
|
-
pub async fn rebase_attempt(
|
808
|
-
pool: &SqlitePool,
|
809
|
-
attempt_id: Uuid,
|
810
|
-
task_id: Uuid,
|
811
|
-
project_id: Uuid,
|
812
|
-
new_base_branch: Option<String>,
|
813
|
-
) -> Result<String, TaskAttemptError> {
|
814
|
-
// Load context with full validation
|
815
|
-
let ctx = TaskAttempt::load_context(pool, attempt_id, task_id, project_id).await?;
|
816
|
-
|
817
|
-
// Use the stored base branch if no new base branch is provided
|
818
|
-
let effective_base_branch =
|
819
|
-
new_base_branch.or_else(|| Some(ctx.task_attempt.base_branch.clone()));
|
820
|
-
|
821
|
-
// Ensure worktree exists (recreate if needed for cold task support)
|
822
|
-
let worktree_path =
|
823
|
-
Self::ensure_worktree_exists(pool, attempt_id, project_id, "rebase").await?;
|
824
|
-
|
825
|
-
let new_base_commit = Self::perform_rebase_operation(
|
826
|
-
&worktree_path,
|
827
|
-
&ctx.project.git_repo_path,
|
828
|
-
effective_base_branch.clone(),
|
829
|
-
ctx.task_attempt.base_branch.clone(),
|
830
|
-
)?;
|
831
|
-
|
832
|
-
// Update the database with the new base branch if it was changed
|
833
|
-
if let Some(new_base_branch) = &effective_base_branch {
|
834
|
-
if new_base_branch != &ctx.task_attempt.base_branch {
|
835
|
-
// For remote branches, store the local branch name in the database
|
836
|
-
let db_branch_name = if new_base_branch.starts_with("origin/") {
|
837
|
-
new_base_branch.strip_prefix("origin/").unwrap()
|
838
|
-
} else {
|
839
|
-
new_base_branch
|
840
|
-
};
|
841
|
-
|
842
|
-
sqlx::query!(
|
843
|
-
"UPDATE task_attempts SET base_branch = $1, updated_at = datetime('now') WHERE id = $2",
|
844
|
-
db_branch_name,
|
845
|
-
attempt_id
|
846
|
-
)
|
847
|
-
.execute(pool)
|
848
|
-
.await?;
|
849
|
-
}
|
850
|
-
}
|
851
|
-
|
852
|
-
Ok(new_base_commit)
|
853
|
-
}
|
854
|
-
|
855
|
-
/// Delete a file from the worktree and commit the change
|
856
|
-
pub async fn delete_file(
|
857
|
-
pool: &SqlitePool,
|
858
|
-
attempt_id: Uuid,
|
859
|
-
task_id: Uuid,
|
860
|
-
project_id: Uuid,
|
861
|
-
file_path: &str,
|
862
|
-
) -> Result<String, TaskAttemptError> {
|
863
|
-
// Load context with full validation
|
864
|
-
let ctx = TaskAttempt::load_context(pool, attempt_id, task_id, project_id).await?;
|
865
|
-
|
866
|
-
// Ensure worktree exists (recreate if needed for cold task support)
|
867
|
-
let worktree_path_str =
|
868
|
-
Self::ensure_worktree_exists(pool, attempt_id, project_id, "delete file").await?;
|
869
|
-
|
870
|
-
// Create GitService instance
|
871
|
-
let git_service = GitService::new(&ctx.project.git_repo_path)?;
|
872
|
-
|
873
|
-
// Use GitService to delete file and commit
|
874
|
-
let commit_id =
|
875
|
-
git_service.delete_file_and_commit(Path::new(&worktree_path_str), file_path)?;
|
876
|
-
|
877
|
-
Ok(commit_id)
|
878
|
-
}
|
879
|
-
|
880
|
-
/// Create a GitHub PR for this task attempt
|
881
|
-
pub async fn create_github_pr(
|
882
|
-
pool: &SqlitePool,
|
883
|
-
params: CreatePrParams<'_>,
|
884
|
-
) -> Result<String, TaskAttemptError> {
|
885
|
-
// Load context with full validation
|
886
|
-
let ctx =
|
887
|
-
TaskAttempt::load_context(pool, params.attempt_id, params.task_id, params.project_id)
|
888
|
-
.await?;
|
889
|
-
|
890
|
-
// Ensure worktree exists (recreate if needed for cold task support)
|
891
|
-
let worktree_path =
|
892
|
-
Self::ensure_worktree_exists(pool, params.attempt_id, params.project_id, "GitHub PR")
|
893
|
-
.await?;
|
894
|
-
|
895
|
-
// Create GitHub service instance
|
896
|
-
let github_service = GitHubService::new(params.github_token)?;
|
897
|
-
|
898
|
-
// Use GitService to get the remote URL, then create GitHubRepoInfo
|
899
|
-
let git_service = GitService::new(&ctx.project.git_repo_path)?;
|
900
|
-
let (owner, repo_name) = git_service
|
901
|
-
.get_github_repo_info()
|
902
|
-
.map_err(|e| TaskAttemptError::ValidationError(e.to_string()))?;
|
903
|
-
let repo_info = GitHubRepoInfo { owner, repo_name };
|
904
|
-
|
905
|
-
// Push the branch to GitHub first
|
906
|
-
Self::push_branch_to_github(
|
907
|
-
&ctx.project.git_repo_path,
|
908
|
-
&worktree_path,
|
909
|
-
&ctx.task_attempt.branch,
|
910
|
-
params.github_token,
|
911
|
-
)?;
|
912
|
-
|
913
|
-
// Create the PR using GitHub service
|
914
|
-
let pr_request = CreatePrRequest {
|
915
|
-
title: params.title.to_string(),
|
916
|
-
body: params.body.map(|s| s.to_string()),
|
917
|
-
head_branch: ctx.task_attempt.branch.clone(),
|
918
|
-
base_branch: params.base_branch.unwrap_or("main").to_string(),
|
919
|
-
};
|
920
|
-
|
921
|
-
let pr_info = github_service.create_pr(&repo_info, &pr_request).await?;
|
922
|
-
|
923
|
-
// Update the task attempt with PR information
|
924
|
-
sqlx::query!(
|
925
|
-
"UPDATE task_attempts SET pr_url = $1, pr_number = $2, pr_status = $3, updated_at = datetime('now') WHERE id = $4",
|
926
|
-
pr_info.url,
|
927
|
-
pr_info.number,
|
928
|
-
pr_info.status,
|
929
|
-
params.attempt_id
|
930
|
-
)
|
931
|
-
.execute(pool)
|
932
|
-
.await?;
|
933
|
-
|
934
|
-
Ok(pr_info.url)
|
935
|
-
}
|
936
|
-
|
937
|
-
/// Push the branch to GitHub remote
|
938
|
-
fn push_branch_to_github(
|
939
|
-
git_repo_path: &str,
|
940
|
-
worktree_path: &str,
|
941
|
-
branch_name: &str,
|
942
|
-
github_token: &str,
|
943
|
-
) -> Result<(), TaskAttemptError> {
|
944
|
-
// Use GitService to push to GitHub
|
945
|
-
let git_service = GitService::new(git_repo_path)?;
|
946
|
-
git_service
|
947
|
-
.push_to_github(Path::new(worktree_path), branch_name, github_token)
|
948
|
-
.map_err(TaskAttemptError::from)
|
949
|
-
}
|
950
|
-
|
951
|
-
/// Update PR status and merge commit
|
952
|
-
pub async fn update_pr_status(
|
953
|
-
pool: &SqlitePool,
|
954
|
-
attempt_id: Uuid,
|
955
|
-
status: &str,
|
956
|
-
merged_at: Option<DateTime<Utc>>,
|
957
|
-
merge_commit_sha: Option<&str>,
|
958
|
-
) -> Result<(), sqlx::Error> {
|
959
|
-
sqlx::query!(
|
960
|
-
"UPDATE task_attempts SET pr_status = $1, pr_merged_at = $2, merge_commit = $3, updated_at = datetime('now') WHERE id = $4",
|
961
|
-
status,
|
962
|
-
merged_at,
|
963
|
-
merge_commit_sha,
|
964
|
-
attempt_id
|
965
|
-
)
|
966
|
-
.execute(pool)
|
967
|
-
.await?;
|
968
|
-
|
969
|
-
Ok(())
|
970
|
-
}
|
971
|
-
|
972
|
-
/// Get the current execution state for a task attempt
|
973
|
-
pub async fn get_execution_state(
|
974
|
-
pool: &SqlitePool,
|
975
|
-
attempt_id: Uuid,
|
976
|
-
task_id: Uuid,
|
977
|
-
project_id: Uuid,
|
978
|
-
) -> Result<TaskAttemptState, TaskAttemptError> {
|
979
|
-
// Load context with full validation
|
980
|
-
let ctx = TaskAttempt::load_context(pool, attempt_id, task_id, project_id).await?;
|
981
|
-
|
982
|
-
let has_setup_script = ctx
|
983
|
-
.project
|
984
|
-
.setup_script
|
985
|
-
.as_ref()
|
986
|
-
.map(|script| !script.trim().is_empty())
|
987
|
-
.unwrap_or(false);
|
988
|
-
|
989
|
-
// Get all execution processes for this attempt, ordered by created_at
|
990
|
-
let processes =
|
991
|
-
crate::models::execution_process::ExecutionProcess::find_by_task_attempt_id(
|
992
|
-
pool, attempt_id,
|
993
|
-
)
|
994
|
-
.await?;
|
995
|
-
|
996
|
-
// Find setup and coding agent processes
|
997
|
-
let setup_process = processes.iter().find(|p| {
|
998
|
-
matches!(
|
999
|
-
p.process_type,
|
1000
|
-
crate::models::execution_process::ExecutionProcessType::SetupScript
|
1001
|
-
)
|
1002
|
-
});
|
1003
|
-
|
1004
|
-
let coding_agent_process = processes.iter().find(|p| {
|
1005
|
-
matches!(
|
1006
|
-
p.process_type,
|
1007
|
-
crate::models::execution_process::ExecutionProcessType::CodingAgent
|
1008
|
-
)
|
1009
|
-
});
|
1010
|
-
|
1011
|
-
// Determine execution state based on processes
|
1012
|
-
let execution_state = if let Some(setup) = setup_process {
|
1013
|
-
match setup.status {
|
1014
|
-
crate::models::execution_process::ExecutionProcessStatus::Running => {
|
1015
|
-
ExecutionState::SetupRunning
|
1016
|
-
}
|
1017
|
-
crate::models::execution_process::ExecutionProcessStatus::Completed => {
|
1018
|
-
if let Some(agent) = coding_agent_process {
|
1019
|
-
match agent.status {
|
1020
|
-
crate::models::execution_process::ExecutionProcessStatus::Running => {
|
1021
|
-
ExecutionState::CodingAgentRunning
|
1022
|
-
}
|
1023
|
-
crate::models::execution_process::ExecutionProcessStatus::Completed => {
|
1024
|
-
ExecutionState::CodingAgentComplete
|
1025
|
-
}
|
1026
|
-
crate::models::execution_process::ExecutionProcessStatus::Failed => {
|
1027
|
-
ExecutionState::CodingAgentFailed
|
1028
|
-
}
|
1029
|
-
crate::models::execution_process::ExecutionProcessStatus::Killed => {
|
1030
|
-
ExecutionState::CodingAgentStopped
|
1031
|
-
}
|
1032
|
-
}
|
1033
|
-
} else {
|
1034
|
-
ExecutionState::SetupComplete
|
1035
|
-
}
|
1036
|
-
}
|
1037
|
-
crate::models::execution_process::ExecutionProcessStatus::Failed => {
|
1038
|
-
ExecutionState::SetupFailed
|
1039
|
-
}
|
1040
|
-
crate::models::execution_process::ExecutionProcessStatus::Killed => {
|
1041
|
-
ExecutionState::SetupStopped
|
1042
|
-
}
|
1043
|
-
}
|
1044
|
-
} else if let Some(agent) = coding_agent_process {
|
1045
|
-
// No setup script, only coding agent
|
1046
|
-
match agent.status {
|
1047
|
-
crate::models::execution_process::ExecutionProcessStatus::Running => {
|
1048
|
-
ExecutionState::CodingAgentRunning
|
1049
|
-
}
|
1050
|
-
crate::models::execution_process::ExecutionProcessStatus::Completed => {
|
1051
|
-
ExecutionState::CodingAgentComplete
|
1052
|
-
}
|
1053
|
-
crate::models::execution_process::ExecutionProcessStatus::Failed => {
|
1054
|
-
ExecutionState::CodingAgentFailed
|
1055
|
-
}
|
1056
|
-
crate::models::execution_process::ExecutionProcessStatus::Killed => {
|
1057
|
-
ExecutionState::CodingAgentStopped
|
1058
|
-
}
|
1059
|
-
}
|
1060
|
-
} else {
|
1061
|
-
// No processes started yet
|
1062
|
-
ExecutionState::NotStarted
|
1063
|
-
};
|
1064
|
-
|
1065
|
-
// Check if there are any changes (quick diff check)
|
1066
|
-
let has_changes = match Self::get_diff(pool, attempt_id, task_id, project_id).await {
|
1067
|
-
Ok(diff) => !diff.files.is_empty(),
|
1068
|
-
Err(_) => false, // If diff fails, assume no changes
|
1069
|
-
};
|
1070
|
-
|
1071
|
-
Ok(TaskAttemptState {
|
1072
|
-
execution_state,
|
1073
|
-
has_changes,
|
1074
|
-
has_setup_script,
|
1075
|
-
setup_process_id: setup_process.map(|p| p.id.to_string()),
|
1076
|
-
coding_agent_process_id: coding_agent_process.map(|p| p.id.to_string()),
|
1077
|
-
})
|
1078
|
-
}
|
1079
|
-
|
1080
|
-
/// Check if setup script has been completed for this worktree
|
1081
|
-
pub async fn is_setup_completed(
|
1082
|
-
pool: &SqlitePool,
|
1083
|
-
attempt_id: Uuid,
|
1084
|
-
) -> Result<bool, TaskAttemptError> {
|
1085
|
-
let task_attempt = Self::find_by_id(pool, attempt_id)
|
1086
|
-
.await?
|
1087
|
-
.ok_or(TaskAttemptError::TaskNotFound)?;
|
1088
|
-
|
1089
|
-
Ok(task_attempt.setup_completed_at.is_some())
|
1090
|
-
}
|
1091
|
-
|
1092
|
-
/// Mark setup script as completed for this worktree
|
1093
|
-
pub async fn mark_setup_completed(
|
1094
|
-
pool: &SqlitePool,
|
1095
|
-
attempt_id: Uuid,
|
1096
|
-
) -> Result<(), TaskAttemptError> {
|
1097
|
-
sqlx::query!(
|
1098
|
-
"UPDATE task_attempts SET setup_completed_at = datetime('now'), updated_at = datetime('now') WHERE id = ?",
|
1099
|
-
attempt_id
|
1100
|
-
)
|
1101
|
-
.execute(pool)
|
1102
|
-
.await?;
|
1103
|
-
|
1104
|
-
Ok(())
|
1105
|
-
}
|
1106
|
-
|
1107
|
-
/// Get execution history from current attempt only (simplified)
|
1108
|
-
pub async fn get_attempt_execution_history(
|
1109
|
-
pool: &SqlitePool,
|
1110
|
-
attempt_id: Uuid,
|
1111
|
-
) -> Result<String, TaskAttemptError> {
|
1112
|
-
// Get all coding agent processes for this attempt
|
1113
|
-
let processes =
|
1114
|
-
crate::models::execution_process::ExecutionProcess::find_by_task_attempt_id(
|
1115
|
-
pool, attempt_id,
|
1116
|
-
)
|
1117
|
-
.await?;
|
1118
|
-
|
1119
|
-
// Filter to coding agent processes only and aggregate stdout
|
1120
|
-
let coding_processes: Vec<_> = processes
|
1121
|
-
.into_iter()
|
1122
|
-
.filter(|p| {
|
1123
|
-
matches!(
|
1124
|
-
p.process_type,
|
1125
|
-
crate::models::execution_process::ExecutionProcessType::CodingAgent
|
1126
|
-
)
|
1127
|
-
})
|
1128
|
-
.collect();
|
1129
|
-
|
1130
|
-
let mut history = String::new();
|
1131
|
-
for process in coding_processes {
|
1132
|
-
if let Some(stdout) = process.stdout {
|
1133
|
-
if !stdout.trim().is_empty() {
|
1134
|
-
history.push_str(&stdout);
|
1135
|
-
history.push('\n');
|
1136
|
-
}
|
1137
|
-
}
|
1138
|
-
}
|
1139
|
-
|
1140
|
-
Ok(history)
|
1141
|
-
}
|
1142
|
-
|
1143
|
-
/// Get diff between base_branch and current attempt (simplified)
|
1144
|
-
pub async fn get_attempt_diff(
|
1145
|
-
pool: &SqlitePool,
|
1146
|
-
attempt_id: Uuid,
|
1147
|
-
project_id: Uuid,
|
1148
|
-
) -> Result<String, TaskAttemptError> {
|
1149
|
-
// Get the task attempt with base_branch
|
1150
|
-
let attempt = Self::find_by_id(pool, attempt_id)
|
1151
|
-
.await?
|
1152
|
-
.ok_or(TaskAttemptError::TaskNotFound)?;
|
1153
|
-
|
1154
|
-
// Get the project
|
1155
|
-
let project = Project::find_by_id(pool, project_id)
|
1156
|
-
.await?
|
1157
|
-
.ok_or(TaskAttemptError::ProjectNotFound)?;
|
1158
|
-
|
1159
|
-
// Open the main repository
|
1160
|
-
let repo = Repository::open(&project.git_repo_path)?;
|
1161
|
-
|
1162
|
-
// Get base branch commit
|
1163
|
-
let base_branch = repo
|
1164
|
-
.find_branch(&attempt.base_branch, git2::BranchType::Local)
|
1165
|
-
.map_err(|_| TaskAttemptError::BranchNotFound(attempt.base_branch.clone()))?;
|
1166
|
-
let base_commit = base_branch.get().peel_to_commit()?;
|
1167
|
-
|
1168
|
-
// Get current branch commit
|
1169
|
-
let current_branch = repo
|
1170
|
-
.find_branch(&attempt.branch, git2::BranchType::Local)
|
1171
|
-
.map_err(|_| TaskAttemptError::BranchNotFound(attempt.branch.clone()))?;
|
1172
|
-
let current_commit = current_branch.get().peel_to_commit()?;
|
1173
|
-
|
1174
|
-
// Create diff between base and current
|
1175
|
-
let base_tree = base_commit.tree()?;
|
1176
|
-
let current_tree = current_commit.tree()?;
|
1177
|
-
|
1178
|
-
let mut diff_opts = git2::DiffOptions::new();
|
1179
|
-
diff_opts.context_lines(GIT_DIFF_CONTEXT_LINES);
|
1180
|
-
diff_opts.interhunk_lines(GIT_DIFF_INTERHUNK_LINES);
|
1181
|
-
|
1182
|
-
let diff =
|
1183
|
-
repo.diff_tree_to_tree(Some(&base_tree), Some(¤t_tree), Some(&mut diff_opts))?;
|
1184
|
-
|
1185
|
-
// Convert to text format
|
1186
|
-
let mut diff_text = String::new();
|
1187
|
-
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
|
1188
|
-
let content = std::str::from_utf8(line.content()).unwrap_or("");
|
1189
|
-
diff_text.push_str(&format!("{}{}", line.origin(), content));
|
1190
|
-
true
|
1191
|
-
})?;
|
1192
|
-
|
1193
|
-
Ok(diff_text)
|
1194
|
-
}
|
1195
|
-
|
1196
|
-
/// Get comprehensive resume context for Gemini followup execution (simplified)
|
1197
|
-
pub async fn get_attempt_resume_context(
|
1198
|
-
pool: &SqlitePool,
|
1199
|
-
attempt_id: Uuid,
|
1200
|
-
_task_id: Uuid,
|
1201
|
-
project_id: Uuid,
|
1202
|
-
) -> Result<AttemptResumeContext, TaskAttemptError> {
|
1203
|
-
// Get execution history from current attempt only
|
1204
|
-
let execution_history = Self::get_attempt_execution_history(pool, attempt_id).await?;
|
1205
|
-
|
1206
|
-
// Get diff between base_branch and current attempt
|
1207
|
-
let cumulative_diffs = Self::get_attempt_diff(pool, attempt_id, project_id).await?;
|
1208
|
-
|
1209
|
-
Ok(AttemptResumeContext {
|
1210
|
-
execution_history,
|
1211
|
-
cumulative_diffs,
|
1212
|
-
})
|
1213
|
-
}
|
1214
|
-
}
|