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
package/backend/src/executor.rs
DELETED
@@ -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
|
-
}
|