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,765 +0,0 @@
|
|
1
|
-
//! Gemini executor implementation
|
2
|
-
//!
|
3
|
-
//! This module provides Gemini CLI-based task execution with streaming support.
|
4
|
-
|
5
|
-
mod config;
|
6
|
-
mod streaming;
|
7
|
-
|
8
|
-
use std::{process::Stdio, time::Instant};
|
9
|
-
|
10
|
-
use async_trait::async_trait;
|
11
|
-
use command_group::{AsyncCommandGroup, AsyncGroupChild};
|
12
|
-
use config::{
|
13
|
-
max_chunk_size, max_display_size, max_latency_ms, max_message_size, GeminiStreamConfig,
|
14
|
-
};
|
15
|
-
// Re-export for external use
|
16
|
-
use serde_json::Value;
|
17
|
-
pub use streaming::GeminiPatchBatch;
|
18
|
-
use streaming::GeminiStreaming;
|
19
|
-
use tokio::{io::AsyncWriteExt, process::Command};
|
20
|
-
use uuid::Uuid;
|
21
|
-
|
22
|
-
use crate::{
|
23
|
-
executor::{
|
24
|
-
Executor, ExecutorError, NormalizedConversation, NormalizedEntry, NormalizedEntryType,
|
25
|
-
},
|
26
|
-
models::task::Task,
|
27
|
-
utils::shell::get_shell_command,
|
28
|
-
};
|
29
|
-
|
30
|
-
/// An executor that uses Gemini CLI to process tasks
|
31
|
-
pub struct GeminiExecutor;
|
32
|
-
|
33
|
-
#[async_trait]
|
34
|
-
impl Executor for GeminiExecutor {
|
35
|
-
async fn spawn(
|
36
|
-
&self,
|
37
|
-
pool: &sqlx::SqlitePool,
|
38
|
-
task_id: Uuid,
|
39
|
-
worktree_path: &str,
|
40
|
-
) -> Result<AsyncGroupChild, ExecutorError> {
|
41
|
-
// Get the task to fetch its description
|
42
|
-
let task = Task::find_by_id(pool, task_id)
|
43
|
-
.await?
|
44
|
-
.ok_or(ExecutorError::TaskNotFound)?;
|
45
|
-
|
46
|
-
let prompt = if let Some(task_description) = task.description {
|
47
|
-
format!(
|
48
|
-
r#"project_id: {}
|
49
|
-
|
50
|
-
Task title: {}
|
51
|
-
Task description: {}"#,
|
52
|
-
task.project_id, task.title, task_description
|
53
|
-
)
|
54
|
-
} else {
|
55
|
-
format!(
|
56
|
-
r#"project_id: {}
|
57
|
-
|
58
|
-
Task title: {}"#,
|
59
|
-
task.project_id, task.title
|
60
|
-
)
|
61
|
-
};
|
62
|
-
|
63
|
-
let mut command = Self::create_gemini_command(worktree_path);
|
64
|
-
|
65
|
-
let mut child = command
|
66
|
-
.group_spawn() // Create new process group so we can kill entire tree
|
67
|
-
.map_err(|e| {
|
68
|
-
crate::executor::SpawnContext::from_command(&command, "Gemini")
|
69
|
-
.with_task(task_id, Some(task.title.clone()))
|
70
|
-
.with_context("Gemini CLI execution for new task")
|
71
|
-
.spawn_error(e)
|
72
|
-
})?;
|
73
|
-
|
74
|
-
// Write prompt to stdin
|
75
|
-
if let Some(mut stdin) = child.inner().stdin.take() {
|
76
|
-
tracing::debug!(
|
77
|
-
"Writing prompt to Gemini stdin for task {}: {:?}",
|
78
|
-
task_id,
|
79
|
-
prompt
|
80
|
-
);
|
81
|
-
stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
|
82
|
-
let context = crate::executor::SpawnContext::from_command(&command, "Gemini")
|
83
|
-
.with_task(task_id, Some(task.title.clone()))
|
84
|
-
.with_context("Failed to write prompt to Gemini CLI stdin");
|
85
|
-
ExecutorError::spawn_failed(e, context)
|
86
|
-
})?;
|
87
|
-
stdin.shutdown().await.map_err(|e| {
|
88
|
-
let context = crate::executor::SpawnContext::from_command(&command, "Gemini")
|
89
|
-
.with_task(task_id, Some(task.title.clone()))
|
90
|
-
.with_context("Failed to close Gemini CLI stdin");
|
91
|
-
ExecutorError::spawn_failed(e, context)
|
92
|
-
})?;
|
93
|
-
tracing::info!(
|
94
|
-
"Successfully sent prompt to Gemini stdin for task {}",
|
95
|
-
task_id
|
96
|
-
);
|
97
|
-
}
|
98
|
-
|
99
|
-
Ok(child)
|
100
|
-
}
|
101
|
-
|
102
|
-
async fn execute_streaming(
|
103
|
-
&self,
|
104
|
-
pool: &sqlx::SqlitePool,
|
105
|
-
task_id: Uuid,
|
106
|
-
attempt_id: Uuid,
|
107
|
-
execution_process_id: Uuid,
|
108
|
-
worktree_path: &str,
|
109
|
-
) -> Result<AsyncGroupChild, ExecutorError> {
|
110
|
-
tracing::info!(
|
111
|
-
"Starting Gemini execution for task {} attempt {}",
|
112
|
-
task_id,
|
113
|
-
attempt_id
|
114
|
-
);
|
115
|
-
|
116
|
-
Self::update_session_id(pool, execution_process_id, &attempt_id.to_string()).await;
|
117
|
-
|
118
|
-
let mut child = self.spawn(pool, task_id, worktree_path).await?;
|
119
|
-
|
120
|
-
tracing::info!(
|
121
|
-
"Gemini process spawned successfully for attempt {}, PID: {:?}",
|
122
|
-
attempt_id,
|
123
|
-
child.inner().id()
|
124
|
-
);
|
125
|
-
|
126
|
-
Self::setup_streaming(pool, &mut child, attempt_id, execution_process_id);
|
127
|
-
|
128
|
-
Ok(child)
|
129
|
-
}
|
130
|
-
|
131
|
-
async fn spawn_followup(
|
132
|
-
&self,
|
133
|
-
pool: &sqlx::SqlitePool,
|
134
|
-
task_id: Uuid,
|
135
|
-
session_id: &str,
|
136
|
-
prompt: &str,
|
137
|
-
worktree_path: &str,
|
138
|
-
) -> Result<AsyncGroupChild, ExecutorError> {
|
139
|
-
// For Gemini, session_id is the attempt_id
|
140
|
-
let attempt_id = Uuid::parse_str(session_id)
|
141
|
-
.map_err(|_| ExecutorError::InvalidSessionId(session_id.to_string()))?;
|
142
|
-
|
143
|
-
let task = self.load_task(pool, task_id).await?;
|
144
|
-
let resume_context = self.collect_resume_context(pool, &task, attempt_id).await?;
|
145
|
-
let comprehensive_prompt = self.build_comprehensive_prompt(&task, &resume_context, prompt);
|
146
|
-
self.spawn_process(worktree_path, &comprehensive_prompt, attempt_id)
|
147
|
-
.await
|
148
|
-
}
|
149
|
-
|
150
|
-
async fn execute_followup_streaming(
|
151
|
-
&self,
|
152
|
-
pool: &sqlx::SqlitePool,
|
153
|
-
task_id: Uuid,
|
154
|
-
attempt_id: Uuid,
|
155
|
-
execution_process_id: Uuid,
|
156
|
-
session_id: &str,
|
157
|
-
prompt: &str,
|
158
|
-
worktree_path: &str,
|
159
|
-
) -> Result<AsyncGroupChild, ExecutorError> {
|
160
|
-
tracing::info!(
|
161
|
-
"Starting Gemini follow-up execution for attempt {} (session {})",
|
162
|
-
attempt_id,
|
163
|
-
session_id
|
164
|
-
);
|
165
|
-
|
166
|
-
// For Gemini, session_id is the attempt_id - update it in the database
|
167
|
-
Self::update_session_id(pool, execution_process_id, session_id).await;
|
168
|
-
|
169
|
-
let mut child = self
|
170
|
-
.spawn_followup(pool, task_id, session_id, prompt, worktree_path)
|
171
|
-
.await?;
|
172
|
-
|
173
|
-
tracing::info!(
|
174
|
-
"Gemini follow-up process spawned successfully for attempt {}, PID: {:?}",
|
175
|
-
attempt_id,
|
176
|
-
child.inner().id()
|
177
|
-
);
|
178
|
-
|
179
|
-
Self::setup_streaming(pool, &mut child, attempt_id, execution_process_id);
|
180
|
-
|
181
|
-
Ok(child)
|
182
|
-
}
|
183
|
-
|
184
|
-
fn normalize_logs(
|
185
|
-
&self,
|
186
|
-
logs: &str,
|
187
|
-
_worktree_path: &str,
|
188
|
-
) -> Result<NormalizedConversation, String> {
|
189
|
-
let mut entries: Vec<NormalizedEntry> = Vec::new();
|
190
|
-
let mut parse_errors = Vec::new();
|
191
|
-
|
192
|
-
for (line_num, line) in logs.lines().enumerate() {
|
193
|
-
let trimmed = line.trim();
|
194
|
-
if trimmed.is_empty() {
|
195
|
-
continue;
|
196
|
-
}
|
197
|
-
|
198
|
-
// Try to parse as JSON first (for NormalizedEntry format)
|
199
|
-
if trimmed.starts_with('{') {
|
200
|
-
match serde_json::from_str::<NormalizedEntry>(trimmed) {
|
201
|
-
Ok(entry) => {
|
202
|
-
entries.push(entry);
|
203
|
-
}
|
204
|
-
Err(e) => {
|
205
|
-
tracing::warn!(
|
206
|
-
"Failed to parse JSONL line {} in Gemini logs: {} - Line: {}",
|
207
|
-
line_num + 1,
|
208
|
-
e,
|
209
|
-
trimmed
|
210
|
-
);
|
211
|
-
parse_errors.push(format!("Line {}: {}", line_num + 1, e));
|
212
|
-
|
213
|
-
// Create a fallback entry for unrecognized JSON
|
214
|
-
let fallback_entry = NormalizedEntry {
|
215
|
-
timestamp: Some(chrono::Utc::now().to_rfc3339()),
|
216
|
-
entry_type: NormalizedEntryType::SystemMessage,
|
217
|
-
content: format!("Raw output: {}", trimmed),
|
218
|
-
metadata: None,
|
219
|
-
};
|
220
|
-
entries.push(fallback_entry);
|
221
|
-
}
|
222
|
-
}
|
223
|
-
} else {
|
224
|
-
// For non-JSON lines, treat as plain text content
|
225
|
-
let text_entry = NormalizedEntry {
|
226
|
-
timestamp: Some(chrono::Utc::now().to_rfc3339()),
|
227
|
-
entry_type: NormalizedEntryType::AssistantMessage,
|
228
|
-
content: trimmed.to_string(),
|
229
|
-
metadata: None,
|
230
|
-
};
|
231
|
-
entries.push(text_entry);
|
232
|
-
}
|
233
|
-
}
|
234
|
-
|
235
|
-
if !parse_errors.is_empty() {
|
236
|
-
tracing::warn!(
|
237
|
-
"Gemini normalize_logs encountered {} parse errors: {}",
|
238
|
-
parse_errors.len(),
|
239
|
-
parse_errors.join("; ")
|
240
|
-
);
|
241
|
-
}
|
242
|
-
|
243
|
-
tracing::debug!(
|
244
|
-
"Gemini normalize_logs processed {} lines, created {} entries",
|
245
|
-
logs.lines().count(),
|
246
|
-
entries.len()
|
247
|
-
);
|
248
|
-
|
249
|
-
Ok(NormalizedConversation {
|
250
|
-
entries,
|
251
|
-
session_id: None, // Session ID is managed directly via database, not extracted from logs
|
252
|
-
executor_type: "gemini".to_string(),
|
253
|
-
prompt: None,
|
254
|
-
summary: None,
|
255
|
-
})
|
256
|
-
}
|
257
|
-
|
258
|
-
// Note: Gemini streaming is handled by the Gemini-specific WAL system.
|
259
|
-
// See emit_content_batch() method which calls GeminiExecutor::push_patch().
|
260
|
-
}
|
261
|
-
|
262
|
-
impl GeminiExecutor {
|
263
|
-
/// Create a standardized Gemini CLI command
|
264
|
-
fn create_gemini_command(worktree_path: &str) -> Command {
|
265
|
-
let (shell_cmd, shell_arg) = get_shell_command();
|
266
|
-
let gemini_command = "npx @google/gemini-cli@latest --yolo";
|
267
|
-
|
268
|
-
let mut command = Command::new(shell_cmd);
|
269
|
-
command
|
270
|
-
.kill_on_drop(true)
|
271
|
-
.stdin(Stdio::piped())
|
272
|
-
.stdout(Stdio::piped())
|
273
|
-
.stderr(Stdio::piped())
|
274
|
-
.current_dir(worktree_path)
|
275
|
-
.arg(shell_arg)
|
276
|
-
.arg(gemini_command)
|
277
|
-
.env("NODE_NO_WARNINGS", "1");
|
278
|
-
command
|
279
|
-
}
|
280
|
-
|
281
|
-
/// Update executor session ID with error handling
|
282
|
-
async fn update_session_id(
|
283
|
-
pool: &sqlx::SqlitePool,
|
284
|
-
execution_process_id: Uuid,
|
285
|
-
session_id: &str,
|
286
|
-
) {
|
287
|
-
if let Err(e) = crate::models::executor_session::ExecutorSession::update_session_id(
|
288
|
-
pool,
|
289
|
-
execution_process_id,
|
290
|
-
session_id,
|
291
|
-
)
|
292
|
-
.await
|
293
|
-
{
|
294
|
-
tracing::error!(
|
295
|
-
"Failed to update session ID for Gemini execution process {}: {}",
|
296
|
-
execution_process_id,
|
297
|
-
e
|
298
|
-
);
|
299
|
-
} else {
|
300
|
-
tracing::info!(
|
301
|
-
"Updated session ID {} for Gemini execution process {}",
|
302
|
-
session_id,
|
303
|
-
execution_process_id
|
304
|
-
);
|
305
|
-
}
|
306
|
-
}
|
307
|
-
|
308
|
-
/// Setup streaming for both stdout and stderr
|
309
|
-
fn setup_streaming(
|
310
|
-
pool: &sqlx::SqlitePool,
|
311
|
-
child: &mut AsyncGroupChild,
|
312
|
-
attempt_id: Uuid,
|
313
|
-
execution_process_id: Uuid,
|
314
|
-
) {
|
315
|
-
// Take stdout and stderr pipes for streaming
|
316
|
-
let stdout = child
|
317
|
-
.inner()
|
318
|
-
.stdout
|
319
|
-
.take()
|
320
|
-
.expect("Failed to take stdout from child process");
|
321
|
-
let stderr = child
|
322
|
-
.inner()
|
323
|
-
.stderr
|
324
|
-
.take()
|
325
|
-
.expect("Failed to take stderr from child process");
|
326
|
-
|
327
|
-
// Start streaming tasks with Gemini-specific line-based message updates
|
328
|
-
let pool_clone1 = pool.clone();
|
329
|
-
let pool_clone2 = pool.clone();
|
330
|
-
|
331
|
-
tokio::spawn(Self::stream_gemini_chunked(
|
332
|
-
stdout,
|
333
|
-
pool_clone1,
|
334
|
-
attempt_id,
|
335
|
-
execution_process_id,
|
336
|
-
));
|
337
|
-
// Use default stderr streaming (no custom parsing)
|
338
|
-
tokio::spawn(crate::executor::stream_output_to_db(
|
339
|
-
stderr,
|
340
|
-
pool_clone2,
|
341
|
-
attempt_id,
|
342
|
-
execution_process_id,
|
343
|
-
false,
|
344
|
-
));
|
345
|
-
}
|
346
|
-
|
347
|
-
/// Push patches to the Gemini WAL system
|
348
|
-
pub fn push_patch(execution_process_id: Uuid, patches: Vec<Value>, content_length: usize) {
|
349
|
-
GeminiStreaming::push_patch(execution_process_id, patches, content_length);
|
350
|
-
}
|
351
|
-
|
352
|
-
/// Get WAL batches for an execution process, optionally filtering by cursor
|
353
|
-
pub fn get_wal_batches(
|
354
|
-
execution_process_id: Uuid,
|
355
|
-
after_batch_id: Option<u64>,
|
356
|
-
) -> Option<Vec<GeminiPatchBatch>> {
|
357
|
-
GeminiStreaming::get_wal_batches(execution_process_id, after_batch_id)
|
358
|
-
}
|
359
|
-
|
360
|
-
/// Clean up WAL when execution process finishes
|
361
|
-
pub async fn finalize_execution(
|
362
|
-
pool: &sqlx::SqlitePool,
|
363
|
-
execution_process_id: Uuid,
|
364
|
-
final_buffer: &str,
|
365
|
-
) {
|
366
|
-
GeminiStreaming::finalize_execution(pool, execution_process_id, final_buffer).await;
|
367
|
-
}
|
368
|
-
|
369
|
-
/// Find the best boundary to split a chunk (newline preferred, sentence fallback)
|
370
|
-
pub fn find_chunk_boundary(buffer: &str, max_size: usize) -> usize {
|
371
|
-
GeminiStreaming::find_chunk_boundary(buffer, max_size)
|
372
|
-
}
|
373
|
-
|
374
|
-
/// Conditionally flush accumulated content to database in chunks
|
375
|
-
pub async fn maybe_flush_chunk(
|
376
|
-
pool: &sqlx::SqlitePool,
|
377
|
-
execution_process_id: Uuid,
|
378
|
-
buffer: &mut String,
|
379
|
-
config: &GeminiStreamConfig,
|
380
|
-
) {
|
381
|
-
GeminiStreaming::maybe_flush_chunk(pool, execution_process_id, buffer, config).await;
|
382
|
-
}
|
383
|
-
|
384
|
-
/// Emit JSON patch for current message state - either "replace" for growing message or "add" for new message.
|
385
|
-
fn emit_message_patch(
|
386
|
-
execution_process_id: Uuid,
|
387
|
-
current_message: &str,
|
388
|
-
entry_count: &mut usize,
|
389
|
-
force_new_message: bool,
|
390
|
-
) {
|
391
|
-
if current_message.is_empty() {
|
392
|
-
return;
|
393
|
-
}
|
394
|
-
|
395
|
-
if force_new_message && *entry_count > 0 {
|
396
|
-
// Start new message: add new entry to array
|
397
|
-
*entry_count += 1;
|
398
|
-
let patch_vec = vec![serde_json::json!({
|
399
|
-
"op": "add",
|
400
|
-
"path": format!("/entries/{}", *entry_count - 1),
|
401
|
-
"value": {
|
402
|
-
"timestamp": chrono::Utc::now().to_rfc3339(),
|
403
|
-
"entry_type": {"type": "assistant_message"},
|
404
|
-
"content": current_message,
|
405
|
-
"metadata": null,
|
406
|
-
}
|
407
|
-
})];
|
408
|
-
|
409
|
-
Self::push_patch(execution_process_id, patch_vec, current_message.len());
|
410
|
-
} else {
|
411
|
-
// Growing message: replace current entry
|
412
|
-
if *entry_count == 0 {
|
413
|
-
*entry_count = 1; // Initialize first message
|
414
|
-
}
|
415
|
-
|
416
|
-
let patch_vec = vec![serde_json::json!({
|
417
|
-
"op": "replace",
|
418
|
-
"path": format!("/entries/{}", *entry_count - 1),
|
419
|
-
"value": {
|
420
|
-
"timestamp": chrono::Utc::now().to_rfc3339(),
|
421
|
-
"entry_type": {"type": "assistant_message"},
|
422
|
-
"content": current_message,
|
423
|
-
"metadata": null,
|
424
|
-
}
|
425
|
-
})];
|
426
|
-
|
427
|
-
Self::push_patch(execution_process_id, patch_vec, current_message.len());
|
428
|
-
}
|
429
|
-
}
|
430
|
-
|
431
|
-
/// Emit final content when stream ends
|
432
|
-
async fn emit_final_content(
|
433
|
-
execution_process_id: Uuid,
|
434
|
-
remaining_content: &str,
|
435
|
-
entry_count: &mut usize,
|
436
|
-
) {
|
437
|
-
if !remaining_content.trim().is_empty() {
|
438
|
-
Self::emit_message_patch(
|
439
|
-
execution_process_id,
|
440
|
-
remaining_content,
|
441
|
-
entry_count,
|
442
|
-
false, // Don't force new message for final content
|
443
|
-
);
|
444
|
-
}
|
445
|
-
}
|
446
|
-
|
447
|
-
async fn load_task(
|
448
|
-
&self,
|
449
|
-
pool: &sqlx::SqlitePool,
|
450
|
-
task_id: Uuid,
|
451
|
-
) -> Result<Task, ExecutorError> {
|
452
|
-
Task::find_by_id(pool, task_id)
|
453
|
-
.await?
|
454
|
-
.ok_or(ExecutorError::TaskNotFound)
|
455
|
-
}
|
456
|
-
|
457
|
-
async fn collect_resume_context(
|
458
|
-
&self,
|
459
|
-
pool: &sqlx::SqlitePool,
|
460
|
-
task: &Task,
|
461
|
-
attempt_id: Uuid,
|
462
|
-
) -> Result<crate::models::task_attempt::AttemptResumeContext, ExecutorError> {
|
463
|
-
crate::models::task_attempt::TaskAttempt::get_attempt_resume_context(
|
464
|
-
pool,
|
465
|
-
attempt_id,
|
466
|
-
task.id,
|
467
|
-
task.project_id,
|
468
|
-
)
|
469
|
-
.await
|
470
|
-
.map_err(ExecutorError::from)
|
471
|
-
}
|
472
|
-
|
473
|
-
fn build_comprehensive_prompt(
|
474
|
-
&self,
|
475
|
-
task: &Task,
|
476
|
-
resume_context: &crate::models::task_attempt::AttemptResumeContext,
|
477
|
-
prompt: &str,
|
478
|
-
) -> String {
|
479
|
-
format!(
|
480
|
-
r#"RESUME CONTEXT FOR CONTINUING TASK
|
481
|
-
=== TASK INFORMATION ===
|
482
|
-
Project ID: {}
|
483
|
-
Task ID: {}
|
484
|
-
Task Title: {}
|
485
|
-
Task Description: {}
|
486
|
-
=== EXECUTION HISTORY ===
|
487
|
-
The following is the execution history from this task attempt:
|
488
|
-
{}
|
489
|
-
=== CURRENT CHANGES ===
|
490
|
-
The following git diff shows changes made from the base branch to the current state:
|
491
|
-
```diff
|
492
|
-
{}
|
493
|
-
```
|
494
|
-
=== CURRENT REQUEST ===
|
495
|
-
{}
|
496
|
-
=== INSTRUCTIONS ===
|
497
|
-
You are continuing work on the above task. The execution history shows what has been done previously, and the git diff shows the current state of all changes. Please continue from where the previous execution left off, taking into account all the context provided above.
|
498
|
-
"#,
|
499
|
-
task.project_id,
|
500
|
-
task.id,
|
501
|
-
task.title,
|
502
|
-
task.description
|
503
|
-
.as_deref()
|
504
|
-
.unwrap_or("No description provided"),
|
505
|
-
if resume_context.execution_history.trim().is_empty() {
|
506
|
-
"(No previous execution history)"
|
507
|
-
} else {
|
508
|
-
&resume_context.execution_history
|
509
|
-
},
|
510
|
-
if resume_context.cumulative_diffs.trim().is_empty() {
|
511
|
-
"(No changes detected)"
|
512
|
-
} else {
|
513
|
-
&resume_context.cumulative_diffs
|
514
|
-
},
|
515
|
-
prompt
|
516
|
-
)
|
517
|
-
}
|
518
|
-
|
519
|
-
async fn spawn_process(
|
520
|
-
&self,
|
521
|
-
worktree_path: &str,
|
522
|
-
comprehensive_prompt: &str,
|
523
|
-
attempt_id: Uuid,
|
524
|
-
) -> Result<AsyncGroupChild, ExecutorError> {
|
525
|
-
tracing::info!(
|
526
|
-
"Spawning Gemini followup execution for attempt {} with resume context ({} chars)",
|
527
|
-
attempt_id,
|
528
|
-
comprehensive_prompt.len()
|
529
|
-
);
|
530
|
-
|
531
|
-
let mut command = GeminiExecutor::create_gemini_command(worktree_path);
|
532
|
-
|
533
|
-
let mut child = command.group_spawn().map_err(|e| {
|
534
|
-
crate::executor::SpawnContext::from_command(&command, "Gemini")
|
535
|
-
.with_context(format!(
|
536
|
-
"Gemini CLI followup execution with context for attempt {}",
|
537
|
-
attempt_id
|
538
|
-
))
|
539
|
-
.spawn_error(e)
|
540
|
-
})?;
|
541
|
-
|
542
|
-
self.send_prompt_to_stdin(&mut child, &command, comprehensive_prompt, attempt_id)
|
543
|
-
.await?;
|
544
|
-
Ok(child)
|
545
|
-
}
|
546
|
-
|
547
|
-
async fn send_prompt_to_stdin(
|
548
|
-
&self,
|
549
|
-
child: &mut AsyncGroupChild,
|
550
|
-
command: &Command,
|
551
|
-
comprehensive_prompt: &str,
|
552
|
-
attempt_id: Uuid,
|
553
|
-
) -> Result<(), ExecutorError> {
|
554
|
-
if let Some(mut stdin) = child.inner().stdin.take() {
|
555
|
-
tracing::debug!(
|
556
|
-
"Sending resume context to Gemini for attempt {}: {} characters",
|
557
|
-
attempt_id,
|
558
|
-
comprehensive_prompt.len()
|
559
|
-
);
|
560
|
-
|
561
|
-
stdin
|
562
|
-
.write_all(comprehensive_prompt.as_bytes())
|
563
|
-
.await
|
564
|
-
.map_err(|e| {
|
565
|
-
let context = crate::executor::SpawnContext::from_command(command, "Gemini")
|
566
|
-
.with_context(format!(
|
567
|
-
"Failed to write resume prompt to Gemini CLI stdin for attempt {}",
|
568
|
-
attempt_id
|
569
|
-
));
|
570
|
-
ExecutorError::spawn_failed(e, context)
|
571
|
-
})?;
|
572
|
-
|
573
|
-
stdin.shutdown().await.map_err(|e| {
|
574
|
-
let context = crate::executor::SpawnContext::from_command(command, "Gemini")
|
575
|
-
.with_context(format!(
|
576
|
-
"Failed to close Gemini CLI stdin for attempt {}",
|
577
|
-
attempt_id
|
578
|
-
));
|
579
|
-
ExecutorError::spawn_failed(e, context)
|
580
|
-
})?;
|
581
|
-
|
582
|
-
tracing::info!(
|
583
|
-
"Successfully sent resume context to Gemini for attempt {}",
|
584
|
-
attempt_id
|
585
|
-
);
|
586
|
-
}
|
587
|
-
|
588
|
-
Ok(())
|
589
|
-
}
|
590
|
-
|
591
|
-
/// Format Gemini CLI output by inserting line breaks where periods are directly
|
592
|
-
/// followed by capital letters (common Gemini CLI formatting issue).
|
593
|
-
/// Handles both intra-chunk and cross-chunk period-to-capital transitions.
|
594
|
-
fn format_gemini_output(content: &str, accumulated_message: &str) -> String {
|
595
|
-
let mut result = String::with_capacity(content.len() + 100); // Reserve some extra space for potential newlines
|
596
|
-
let chars: Vec<char> = content.chars().collect();
|
597
|
-
|
598
|
-
// Check for cross-chunk boundary: previous chunk ended with period, current starts with capital
|
599
|
-
if !accumulated_message.is_empty() && !content.is_empty() {
|
600
|
-
let ends_with_period = accumulated_message.ends_with('.');
|
601
|
-
let starts_with_capital = chars
|
602
|
-
.first()
|
603
|
-
.map(|&c| c.is_uppercase() && c.is_alphabetic())
|
604
|
-
.unwrap_or(false);
|
605
|
-
|
606
|
-
if ends_with_period && starts_with_capital {
|
607
|
-
result.push('\n');
|
608
|
-
}
|
609
|
-
}
|
610
|
-
|
611
|
-
// Handle intra-chunk period-to-capital transitions
|
612
|
-
for i in 0..chars.len() {
|
613
|
-
result.push(chars[i]);
|
614
|
-
|
615
|
-
// Check if current char is '.' and next char is uppercase letter (no space between)
|
616
|
-
if chars[i] == '.' && i + 1 < chars.len() {
|
617
|
-
let next_char = chars[i + 1];
|
618
|
-
if next_char.is_uppercase() && next_char.is_alphabetic() {
|
619
|
-
result.push('\n');
|
620
|
-
}
|
621
|
-
}
|
622
|
-
}
|
623
|
-
|
624
|
-
result
|
625
|
-
}
|
626
|
-
|
627
|
-
/// Stream Gemini output with dual-buffer approach: chunks for UI updates, messages for storage.
|
628
|
-
///
|
629
|
-
/// **Chunks** (~2KB): Frequent UI updates using "replace" patches for smooth streaming
|
630
|
-
/// **Messages** (~8KB): Logical boundaries using "add" patches for new entries
|
631
|
-
/// **Consistent WAL/DB**: Both systems see same message structure via JSON patches
|
632
|
-
pub async fn stream_gemini_chunked(
|
633
|
-
mut output: impl tokio::io::AsyncRead + Unpin,
|
634
|
-
pool: sqlx::SqlitePool,
|
635
|
-
attempt_id: Uuid,
|
636
|
-
execution_process_id: Uuid,
|
637
|
-
) {
|
638
|
-
use tokio::io::{AsyncReadExt, BufReader};
|
639
|
-
|
640
|
-
let chunk_limit = max_chunk_size();
|
641
|
-
let display_chunk_size = max_display_size(); // ~2KB for UI updates
|
642
|
-
let message_boundary_size = max_message_size(); // ~8KB for new message boundaries
|
643
|
-
let max_latency = std::time::Duration::from_millis(max_latency_ms());
|
644
|
-
|
645
|
-
let mut reader = BufReader::new(&mut output);
|
646
|
-
|
647
|
-
// Dual buffers: chunk buffer for UI, message buffer for DB
|
648
|
-
let mut current_message = String::new(); // Current assistant message content
|
649
|
-
let mut db_buffer = String::new(); // Buffer for database storage (using ChunkStore)
|
650
|
-
let mut entry_count = 0usize; // Track assistant message entries
|
651
|
-
|
652
|
-
let mut read_buf = vec![0u8; chunk_limit.min(max_chunk_size())]; // Use configurable chunk limit, capped for memory efficiency
|
653
|
-
let mut last_chunk_emit = Instant::now();
|
654
|
-
|
655
|
-
// Configuration for WAL and DB management
|
656
|
-
let config = GeminiStreamConfig::default();
|
657
|
-
|
658
|
-
tracing::info!(
|
659
|
-
"Starting dual-buffer Gemini streaming for attempt {} (chunks: {}B, messages: {}B)",
|
660
|
-
attempt_id,
|
661
|
-
display_chunk_size,
|
662
|
-
message_boundary_size
|
663
|
-
);
|
664
|
-
|
665
|
-
loop {
|
666
|
-
match reader.read(&mut read_buf).await {
|
667
|
-
Ok(0) => {
|
668
|
-
// EOF: emit final content and flush to database
|
669
|
-
Self::emit_final_content(
|
670
|
-
execution_process_id,
|
671
|
-
¤t_message,
|
672
|
-
&mut entry_count,
|
673
|
-
)
|
674
|
-
.await;
|
675
|
-
|
676
|
-
// Flush any remaining database buffer
|
677
|
-
Self::finalize_execution(&pool, execution_process_id, &db_buffer).await;
|
678
|
-
break;
|
679
|
-
}
|
680
|
-
Ok(n) => {
|
681
|
-
// Convert bytes to string and apply Gemini-specific formatting
|
682
|
-
let raw_chunk = String::from_utf8_lossy(&read_buf[..n]);
|
683
|
-
let formatted_chunk = Self::format_gemini_output(&raw_chunk, ¤t_message);
|
684
|
-
|
685
|
-
// Add to both buffers
|
686
|
-
current_message.push_str(&formatted_chunk);
|
687
|
-
db_buffer.push_str(&formatted_chunk);
|
688
|
-
|
689
|
-
// 1. Check for chunk emission (frequent UI updates ~2KB)
|
690
|
-
let should_emit_chunk = current_message.len() >= display_chunk_size
|
691
|
-
|| (last_chunk_emit.elapsed() >= max_latency
|
692
|
-
&& !current_message.is_empty());
|
693
|
-
|
694
|
-
if should_emit_chunk {
|
695
|
-
// Emit "replace" patch for growing message (smooth UI)
|
696
|
-
Self::emit_message_patch(
|
697
|
-
execution_process_id,
|
698
|
-
¤t_message,
|
699
|
-
&mut entry_count,
|
700
|
-
false, // Not forcing new message
|
701
|
-
);
|
702
|
-
last_chunk_emit = Instant::now();
|
703
|
-
}
|
704
|
-
|
705
|
-
// 2. Check for message boundary (new assistant message ~8KB)
|
706
|
-
let should_start_new_message = current_message.len() >= message_boundary_size;
|
707
|
-
|
708
|
-
if should_start_new_message {
|
709
|
-
// Find optimal boundary for new message
|
710
|
-
let boundary =
|
711
|
-
Self::find_chunk_boundary(¤t_message, message_boundary_size);
|
712
|
-
|
713
|
-
if boundary > 0 && boundary < current_message.len() {
|
714
|
-
// Split at boundary: complete current message, start new one
|
715
|
-
let completed_message = current_message[..boundary].to_string();
|
716
|
-
let remaining_content = current_message[boundary..].to_string();
|
717
|
-
|
718
|
-
// CRITICAL FIX: Only emit "replace" patch to complete current message
|
719
|
-
// Do NOT emit "add" patch as it shifts existing database entries
|
720
|
-
Self::emit_message_patch(
|
721
|
-
execution_process_id,
|
722
|
-
&completed_message,
|
723
|
-
&mut entry_count,
|
724
|
-
false, // Complete current message
|
725
|
-
);
|
726
|
-
|
727
|
-
// Store the completed message to database
|
728
|
-
// This ensures the database gets the completed content at the boundary
|
729
|
-
Self::maybe_flush_chunk(
|
730
|
-
&pool,
|
731
|
-
execution_process_id,
|
732
|
-
&mut db_buffer,
|
733
|
-
&config,
|
734
|
-
)
|
735
|
-
.await;
|
736
|
-
|
737
|
-
// Start fresh message with remaining content (no WAL patch yet)
|
738
|
-
// Next chunk emission will create "replace" patch for entry_count + 1
|
739
|
-
current_message = remaining_content;
|
740
|
-
entry_count += 1; // Move to next entry index for future patches
|
741
|
-
}
|
742
|
-
}
|
743
|
-
|
744
|
-
// 3. Flush to database (same boundary detection)
|
745
|
-
Self::maybe_flush_chunk(&pool, execution_process_id, &mut db_buffer, &config)
|
746
|
-
.await;
|
747
|
-
}
|
748
|
-
Err(e) => {
|
749
|
-
tracing::error!(
|
750
|
-
"Error reading stdout for Gemini attempt {}: {}",
|
751
|
-
attempt_id,
|
752
|
-
e
|
753
|
-
);
|
754
|
-
break;
|
755
|
-
}
|
756
|
-
}
|
757
|
-
}
|
758
|
-
|
759
|
-
tracing::info!(
|
760
|
-
"Dual-buffer Gemini streaming completed for attempt {} ({} messages)",
|
761
|
-
attempt_id,
|
762
|
-
entry_count
|
763
|
-
);
|
764
|
-
}
|
765
|
-
}
|