alysis-code 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,926 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, Protocol
7
+
8
+ from . import workspace_isolation as _workspace_isolation
9
+ from .execution_shared import copy_workspace_snapshot, sync_snapshot_changed_files
10
+ from .forge import RunPaths
11
+ from .git_ops import GitOpsError, ensure_runtime_artifact_excludes
12
+ from .runtime_artifacts import is_runtime_artifact_path
13
+
14
+ _cleanup_workspace_path = _workspace_isolation._cleanup_workspace_path
15
+ _inspect_existing_git_workspace = _workspace_isolation._inspect_existing_git_workspace
16
+ _reset_git_workspace_to_target = _workspace_isolation._reset_git_workspace_to_target
17
+ _retry_readonly_removal = _workspace_isolation._retry_readonly_removal
18
+ _run_git_checked = _workspace_isolation._run_git_checked
19
+ _sanitize_existing_git_workspace = _workspace_isolation._sanitize_existing_git_workspace
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class SwarmBackendStartupResult:
24
+ warnings: list[str]
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class PreparedTaskWorkspace:
29
+ backend_name: str
30
+ task_id: str
31
+ branch: str
32
+ base_branch: str
33
+ worktree_path: Path
34
+ control_root: Path
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class PreparedBatchCandidateWorkspace:
39
+ backend_name: str
40
+ batch_label: str
41
+ branch: str
42
+ base_branch: str
43
+ worktree_path: Path
44
+ control_root: Path
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class SwarmApplyResult:
49
+ backend_name: str
50
+ task_id: str
51
+ branch: str
52
+ merge_commit_hash: str
53
+ action: str = "merged"
54
+
55
+
56
+ class SwarmBackend(Protocol):
57
+ name: str
58
+ capabilities: frozenset[str]
59
+ requires_head_commit: bool
60
+ supports_remote_sync: bool
61
+
62
+ def prepare_startup(self, root: Path) -> SwarmBackendStartupResult: ...
63
+
64
+ def prune(self, root: Path) -> None: ...
65
+
66
+ def has_head_commit(self, root: Path) -> bool: ...
67
+
68
+ def default_base_branch(self, root: Path) -> str: ...
69
+
70
+ def branch_exists(self, root: Path, branch: str) -> bool: ...
71
+
72
+ def task_workspace_path(self, *, run_dir: Path, task_id: str) -> Path: ...
73
+
74
+ def candidate_workspace_path(self, *, run_dir: Path, batch_label: str) -> Path: ...
75
+
76
+ def prepare_task_workspace(
77
+ self,
78
+ *,
79
+ root: Path,
80
+ run_dir: Path,
81
+ task_id: str,
82
+ branch: str,
83
+ base_branch: str,
84
+ ) -> PreparedTaskWorkspace: ...
85
+
86
+ def prepare_candidate_workspace(
87
+ self,
88
+ *,
89
+ root: Path,
90
+ run_dir: Path,
91
+ batch_label: str,
92
+ branch: str,
93
+ base_branch: str,
94
+ ) -> PreparedBatchCandidateWorkspace: ...
95
+
96
+ def load_task_workspace(
97
+ self,
98
+ *,
99
+ root: Path,
100
+ run_dir: Path,
101
+ task_id: str,
102
+ branch: str,
103
+ base_branch: str,
104
+ ) -> PreparedTaskWorkspace: ...
105
+
106
+ def apply_task_success(
107
+ self,
108
+ *,
109
+ root: Path,
110
+ prepared_workspace: PreparedTaskWorkspace,
111
+ message: str,
112
+ changed_files: list[str] | None = None,
113
+ ) -> SwarmApplyResult: ...
114
+
115
+ def apply_task_to_candidate(
116
+ self,
117
+ *,
118
+ root: Path,
119
+ candidate_workspace: PreparedBatchCandidateWorkspace,
120
+ prepared_workspace: PreparedTaskWorkspace,
121
+ message: str,
122
+ changed_files: list[str] | None = None,
123
+ ) -> None: ...
124
+
125
+ def cleanup_task_workspace(
126
+ self,
127
+ *,
128
+ root: Path,
129
+ prepared_workspace: PreparedTaskWorkspace,
130
+ keep_worktrees: bool,
131
+ ) -> list[str]: ...
132
+
133
+ def cleanup_candidate_workspace(
134
+ self,
135
+ *,
136
+ root: Path,
137
+ candidate_workspace: PreparedBatchCandidateWorkspace,
138
+ keep_worktrees: bool,
139
+ ) -> list[str]: ...
140
+
141
+ def cleanup_failed_task_workspace(
142
+ self,
143
+ *,
144
+ root: Path,
145
+ prepared_workspace: PreparedTaskWorkspace,
146
+ keep_worktrees: bool,
147
+ ) -> list[str]: ...
148
+
149
+
150
+ def _require_materialized_candidate_workspace(
151
+ *,
152
+ worktree_path: Path,
153
+ expected_branch: str,
154
+ ) -> None:
155
+ if not worktree_path.exists():
156
+ raise GitOpsError(
157
+ "candidate workspace was not materialized by ensure_worktree_fn; "
158
+ f"expected a git worktree at {worktree_path} on branch {expected_branch}"
159
+ )
160
+ valid, _clean, reason = _inspect_existing_git_workspace(
161
+ worktree_path=worktree_path,
162
+ expected_branch=expected_branch,
163
+ )
164
+ if valid:
165
+ return
166
+ raise GitOpsError(
167
+ "candidate workspace was not materialized correctly by ensure_worktree_fn: "
168
+ f"{reason}. Expected a git worktree at {worktree_path} on branch {expected_branch}"
169
+ )
170
+
171
+
172
+ def _tracked_runtime_artifact_paths(root: Path) -> list[str]:
173
+ cp = _run_git_checked(
174
+ root,
175
+ ["ls-files", "-z"],
176
+ error_message="failed to inspect tracked files",
177
+ )
178
+ return sorted(
179
+ path
180
+ for path in cp.stdout.split("\0")
181
+ if path.strip() and is_runtime_artifact_path(path, root=root)
182
+ )
183
+
184
+
185
+ def _failed_cleanup_marker_path(*, worktree_path: Path) -> Path:
186
+ return worktree_path.parent / "failed_cleanup.json"
187
+
188
+
189
+ @dataclass(frozen=True)
190
+ class GitWorktreeSwarmBackend:
191
+ ensure_worktree_fn: Any
192
+ merge_runner: Any
193
+ remove_worktree_fn: Any
194
+ delete_branch_fn: Any
195
+ prune_fn: Any
196
+ branch_exists_fn: Any
197
+ current_branch_fn: Any
198
+ has_head_commit_fn: Any
199
+
200
+ name: str = "git_worktree"
201
+ capabilities: frozenset[str] = frozenset({"git", "merge", "worktree"})
202
+ requires_head_commit: bool = True
203
+ supports_remote_sync: bool = True
204
+
205
+ def _clear_failed_cleanup_marker(self, *, worktree_path: Path) -> None:
206
+ marker_path = _failed_cleanup_marker_path(worktree_path=worktree_path)
207
+ if not marker_path.exists():
208
+ return
209
+ try:
210
+ marker_path.unlink()
211
+ except OSError as e:
212
+ raise GitOpsError(f"failed to remove cleanup marker {marker_path}: {e}") from e
213
+
214
+ def _write_failed_cleanup_marker(
215
+ self,
216
+ *,
217
+ worktree_path: Path,
218
+ branch: str,
219
+ cleanup_errors: list[str],
220
+ unresolved_state: list[str],
221
+ ) -> None:
222
+ marker_path = _failed_cleanup_marker_path(worktree_path=worktree_path)
223
+ marker_path.parent.mkdir(parents=True, exist_ok=True)
224
+ payload = {
225
+ "branch": branch,
226
+ "cleanup_errors": cleanup_errors,
227
+ "unresolved_state": unresolved_state,
228
+ }
229
+ try:
230
+ marker_path.write_text(
231
+ json.dumps(payload, indent=2, sort_keys=True) + "\n",
232
+ encoding="utf-8",
233
+ )
234
+ except OSError as e:
235
+ raise GitOpsError(f"failed to write cleanup marker {marker_path}: {e}") from e
236
+
237
+ def _record_failed_cleanup_state(
238
+ self,
239
+ *,
240
+ root: Path,
241
+ worktree_path: Path,
242
+ branch: str,
243
+ cleanup_errors: list[str],
244
+ ) -> list[str]:
245
+ unresolved_state: list[str] = []
246
+ if worktree_path.exists():
247
+ unresolved_state.append(f"worktree path still exists: {worktree_path}")
248
+ branch_name = str(branch or "").strip()
249
+ if branch_name:
250
+ try:
251
+ if self.branch_exists(root, branch_name):
252
+ unresolved_state.append(f"task branch still exists: {branch_name}")
253
+ except Exception as e: # noqa: BLE001
254
+ cleanup_errors.append(f"branch existence check failed: {e}")
255
+ if cleanup_errors or unresolved_state:
256
+ self._write_failed_cleanup_marker(
257
+ worktree_path=worktree_path,
258
+ branch=branch_name,
259
+ cleanup_errors=list(cleanup_errors),
260
+ unresolved_state=unresolved_state,
261
+ )
262
+ return unresolved_state
263
+ self._clear_failed_cleanup_marker(worktree_path=worktree_path)
264
+ return []
265
+
266
+ def _cleanup_failed_branch(self, *, root: Path, branch: str) -> None:
267
+ branch_name = str(branch or "").strip()
268
+ if not branch_name or not self.branch_exists(root, branch_name):
269
+ return
270
+ try:
271
+ self.delete_branch_fn(root, branch_name)
272
+ except Exception: # noqa: BLE001
273
+ if not self.branch_exists(root, branch_name):
274
+ return
275
+ _run_git_checked(
276
+ root,
277
+ ["branch", "-D", branch_name],
278
+ error_message=f"failed to force delete rejected task branch {branch_name}",
279
+ )
280
+ return
281
+ if self.branch_exists(root, branch_name):
282
+ _run_git_checked(
283
+ root,
284
+ ["branch", "-D", branch_name],
285
+ error_message=f"failed to force delete rejected task branch {branch_name}",
286
+ )
287
+
288
+ def _retry_incomplete_failed_cleanup(
289
+ self,
290
+ *,
291
+ root: Path,
292
+ worktree_path: Path,
293
+ branch: str,
294
+ ) -> None:
295
+ marker_path = _failed_cleanup_marker_path(worktree_path=worktree_path)
296
+ if not marker_path.exists():
297
+ return
298
+ try:
299
+ original_marker_payload = json.loads(marker_path.read_text(encoding="utf-8"))
300
+ except Exception: # noqa: BLE001
301
+ original_marker_payload = None
302
+
303
+ retry_errors: list[str] = []
304
+ if worktree_path.exists():
305
+ try:
306
+ self.remove_worktree_fn(
307
+ root=root,
308
+ worktree_repo_path=worktree_path,
309
+ force=True,
310
+ )
311
+ except Exception as e: # noqa: BLE001
312
+ retry_errors.append(f"worktree cleanup failed: {e}")
313
+ if branch:
314
+ try:
315
+ self._cleanup_failed_branch(root=root, branch=branch)
316
+ except Exception as e: # noqa: BLE001
317
+ retry_errors.append(f"branch cleanup failed: {e}")
318
+
319
+ unresolved_state = self._record_failed_cleanup_state(
320
+ root=root,
321
+ worktree_path=worktree_path,
322
+ branch=branch,
323
+ cleanup_errors=retry_errors,
324
+ )
325
+ if retry_errors or unresolved_state:
326
+ marker_note = f" See {marker_path} for details."
327
+ previous_note = ""
328
+ if isinstance(original_marker_payload, dict):
329
+ previous_errors = original_marker_payload.get("cleanup_errors")
330
+ if isinstance(previous_errors, list) and previous_errors:
331
+ previous_note = f" Previous cleanup errors: {'; '.join(str(item) for item in previous_errors)}."
332
+ unresolved_note = ""
333
+ if unresolved_state:
334
+ unresolved_note = f" Unresolved state: {'; '.join(unresolved_state)}."
335
+ retry_note = ""
336
+ if retry_errors:
337
+ retry_note = f" Retry cleanup errors: {'; '.join(retry_errors)}."
338
+ raise GitOpsError(
339
+ "previous failed task cleanup left unresolved git-worktree state; rerun blocked "
340
+ f"until cleanup succeeds.{previous_note}{retry_note}{unresolved_note}{marker_note}"
341
+ )
342
+
343
+ def prepare_startup(self, root: Path) -> SwarmBackendStartupResult:
344
+ warnings: list[str] = []
345
+ try:
346
+ self.prune(root)
347
+ except Exception as e: # noqa: BLE001
348
+ warnings.append(f"git worktree prune failed: {e}")
349
+ return SwarmBackendStartupResult(warnings=warnings)
350
+
351
+ def prune(self, root: Path) -> None:
352
+ self.prune_fn(root)
353
+
354
+ def has_head_commit(self, root: Path) -> bool:
355
+ return bool(self.has_head_commit_fn(root))
356
+
357
+ def default_base_branch(self, root: Path) -> str:
358
+ return str(self.current_branch_fn(root))
359
+
360
+ def branch_exists(self, root: Path, branch: str) -> bool:
361
+ return bool(self.branch_exists_fn(root, branch))
362
+
363
+ def task_workspace_path(self, *, run_dir: Path, task_id: str) -> Path:
364
+ return run_dir / "worktrees" / task_id / "repo"
365
+
366
+ def candidate_workspace_path(self, *, run_dir: Path, batch_label: str) -> Path:
367
+ return run_dir / "worktrees" / "_batch_candidates" / batch_label / "repo"
368
+
369
+ def prepare_task_workspace(
370
+ self,
371
+ *,
372
+ root: Path,
373
+ run_dir: Path,
374
+ task_id: str,
375
+ branch: str,
376
+ base_branch: str,
377
+ ) -> PreparedTaskWorkspace:
378
+ worktree_path = self.task_workspace_path(run_dir=run_dir, task_id=task_id)
379
+ self._retry_incomplete_failed_cleanup(
380
+ root=root,
381
+ worktree_path=worktree_path,
382
+ branch=branch,
383
+ )
384
+ self.ensure_worktree_fn(
385
+ root=root,
386
+ worktree_repo_path=worktree_path,
387
+ branch=branch,
388
+ base_branch=base_branch,
389
+ )
390
+ return PreparedTaskWorkspace(
391
+ backend_name=self.name,
392
+ task_id=task_id,
393
+ branch=branch,
394
+ base_branch=base_branch,
395
+ worktree_path=worktree_path,
396
+ control_root=root,
397
+ )
398
+
399
+ def prepare_candidate_workspace(
400
+ self,
401
+ *,
402
+ root: Path,
403
+ run_dir: Path,
404
+ batch_label: str,
405
+ branch: str,
406
+ base_branch: str,
407
+ ) -> PreparedBatchCandidateWorkspace:
408
+ worktree_path = self.candidate_workspace_path(run_dir=run_dir, batch_label=batch_label)
409
+ self._retry_incomplete_failed_cleanup(
410
+ root=root,
411
+ worktree_path=worktree_path,
412
+ branch=branch,
413
+ )
414
+ self.ensure_worktree_fn(
415
+ root=root,
416
+ worktree_repo_path=worktree_path,
417
+ branch=branch,
418
+ base_branch=base_branch,
419
+ )
420
+ # Candidate verification runs real git operations inside the candidate repo.
421
+ # Custom ensure_worktree_fn implementations must materialize the repo path
422
+ # and check out the requested candidate branch before returning.
423
+ _require_materialized_candidate_workspace(
424
+ worktree_path=worktree_path,
425
+ expected_branch=branch,
426
+ )
427
+ _reset_git_workspace_to_target(worktree_path=worktree_path, target=base_branch)
428
+ return PreparedBatchCandidateWorkspace(
429
+ backend_name=self.name,
430
+ batch_label=batch_label,
431
+ branch=branch,
432
+ base_branch=base_branch,
433
+ worktree_path=worktree_path,
434
+ control_root=worktree_path,
435
+ )
436
+
437
+ def load_task_workspace(
438
+ self,
439
+ *,
440
+ root: Path,
441
+ run_dir: Path,
442
+ task_id: str,
443
+ branch: str,
444
+ base_branch: str,
445
+ ) -> PreparedTaskWorkspace:
446
+ return PreparedTaskWorkspace(
447
+ backend_name=self.name,
448
+ task_id=task_id,
449
+ branch=branch,
450
+ base_branch=base_branch,
451
+ worktree_path=self.task_workspace_path(run_dir=run_dir, task_id=task_id),
452
+ control_root=root,
453
+ )
454
+
455
+ def apply_task_success(
456
+ self,
457
+ *,
458
+ root: Path,
459
+ prepared_workspace: PreparedTaskWorkspace,
460
+ message: str,
461
+ changed_files: list[str] | None = None,
462
+ ) -> SwarmApplyResult:
463
+ _ = changed_files
464
+ merge_commit_hash = self.merge_runner(
465
+ root,
466
+ base_branch=prepared_workspace.base_branch,
467
+ task_branch=prepared_workspace.branch,
468
+ message=message,
469
+ )
470
+ return SwarmApplyResult(
471
+ backend_name=self.name,
472
+ task_id=prepared_workspace.task_id,
473
+ branch=prepared_workspace.branch,
474
+ merge_commit_hash=merge_commit_hash,
475
+ action="merged",
476
+ )
477
+
478
+ def apply_task_to_candidate(
479
+ self,
480
+ *,
481
+ root: Path,
482
+ candidate_workspace: PreparedBatchCandidateWorkspace,
483
+ prepared_workspace: PreparedTaskWorkspace,
484
+ message: str,
485
+ changed_files: list[str] | None = None,
486
+ ) -> None:
487
+ _ = root
488
+ _ = changed_files
489
+ self.merge_runner(
490
+ candidate_workspace.control_root,
491
+ base_branch=candidate_workspace.branch,
492
+ task_branch=prepared_workspace.branch,
493
+ message=message,
494
+ )
495
+
496
+ def cleanup_task_workspace(
497
+ self,
498
+ *,
499
+ root: Path,
500
+ prepared_workspace: PreparedTaskWorkspace,
501
+ keep_worktrees: bool,
502
+ ) -> list[str]:
503
+ if keep_worktrees:
504
+ return []
505
+
506
+ cleanup_errors: list[str] = []
507
+ if prepared_workspace.worktree_path.exists():
508
+ try:
509
+ self.remove_worktree_fn(
510
+ root=root,
511
+ worktree_repo_path=prepared_workspace.worktree_path,
512
+ force=True,
513
+ )
514
+ except Exception as e: # noqa: BLE001
515
+ cleanup_errors.append(f"worktree cleanup failed: {e}")
516
+ if prepared_workspace.branch:
517
+ try:
518
+ self.delete_branch_fn(root, prepared_workspace.branch)
519
+ except Exception as e: # noqa: BLE001
520
+ cleanup_errors.append(f"branch cleanup failed: {e}")
521
+ return cleanup_errors
522
+
523
+ def cleanup_candidate_workspace(
524
+ self,
525
+ *,
526
+ root: Path,
527
+ candidate_workspace: PreparedBatchCandidateWorkspace,
528
+ keep_worktrees: bool,
529
+ ) -> list[str]:
530
+ if keep_worktrees:
531
+ return []
532
+
533
+ cleanup_errors: list[str] = []
534
+ if candidate_workspace.worktree_path.exists():
535
+ try:
536
+ self.remove_worktree_fn(
537
+ root=root,
538
+ worktree_repo_path=candidate_workspace.worktree_path,
539
+ force=True,
540
+ )
541
+ except Exception as e: # noqa: BLE001
542
+ cleanup_errors.append(f"candidate worktree cleanup failed: {e}")
543
+ if candidate_workspace.branch:
544
+ try:
545
+ self.delete_branch_fn(root, candidate_workspace.branch)
546
+ except Exception as e: # noqa: BLE001
547
+ cleanup_errors.append(f"candidate branch cleanup failed: {e}")
548
+ try:
549
+ unresolved_state = self._record_failed_cleanup_state(
550
+ root=root,
551
+ worktree_path=candidate_workspace.worktree_path,
552
+ branch=candidate_workspace.branch,
553
+ cleanup_errors=cleanup_errors,
554
+ )
555
+ if unresolved_state and not cleanup_errors:
556
+ cleanup_errors.append(f"cleanup incomplete: {'; '.join(unresolved_state)}")
557
+ except Exception as e: # noqa: BLE001
558
+ cleanup_errors.append(f"candidate cleanup marker failed: {e}")
559
+ return cleanup_errors
560
+
561
+ def cleanup_failed_task_workspace(
562
+ self,
563
+ *,
564
+ root: Path,
565
+ prepared_workspace: PreparedTaskWorkspace,
566
+ keep_worktrees: bool,
567
+ ) -> list[str]:
568
+ if keep_worktrees:
569
+ return []
570
+
571
+ cleanup_errors: list[str] = []
572
+ if prepared_workspace.worktree_path.exists():
573
+ try:
574
+ self.remove_worktree_fn(
575
+ root=root,
576
+ worktree_repo_path=prepared_workspace.worktree_path,
577
+ force=True,
578
+ )
579
+ except Exception as e: # noqa: BLE001
580
+ cleanup_errors.append(f"worktree cleanup failed: {e}")
581
+ if prepared_workspace.branch:
582
+ try:
583
+ self._cleanup_failed_branch(root=root, branch=prepared_workspace.branch)
584
+ except Exception as e: # noqa: BLE001
585
+ cleanup_errors.append(f"branch cleanup failed: {e}")
586
+ try:
587
+ unresolved_state = self._record_failed_cleanup_state(
588
+ root=root,
589
+ worktree_path=prepared_workspace.worktree_path,
590
+ branch=prepared_workspace.branch,
591
+ cleanup_errors=cleanup_errors,
592
+ )
593
+ if unresolved_state and not cleanup_errors:
594
+ cleanup_errors.append(f"cleanup incomplete: {'; '.join(unresolved_state)}")
595
+ except Exception as e: # noqa: BLE001
596
+ cleanup_errors.append(f"cleanup marker failed: {e}")
597
+ return cleanup_errors
598
+
599
+
600
+ @dataclass(frozen=True)
601
+ class SnapshotSwarmBackend:
602
+ merge_runner: Any
603
+ branch_exists_fn: Any
604
+ current_branch_fn: Any
605
+ snapshot_base_branch: str = "snapshot-base"
606
+ name: str = "snapshot_workspace"
607
+ capabilities: frozenset[str] = frozenset({"git", "snapshot", "sync"})
608
+ requires_head_commit: bool = False
609
+ supports_remote_sync: bool = False
610
+
611
+ def prepare_startup(self, root: Path) -> SwarmBackendStartupResult:
612
+ _ = root
613
+ return SwarmBackendStartupResult(warnings=[])
614
+
615
+ def prune(self, root: Path) -> None:
616
+ _ = root
617
+
618
+ def has_head_commit(self, root: Path) -> bool:
619
+ _ = root
620
+ return False
621
+
622
+ def default_base_branch(self, root: Path) -> str:
623
+ try:
624
+ branch = str(self.current_branch_fn(root) or "").strip()
625
+ except Exception: # noqa: BLE001
626
+ branch = ""
627
+ return branch or self.snapshot_base_branch
628
+
629
+ def branch_exists(self, root: Path, branch: str) -> bool:
630
+ return bool(self.branch_exists_fn(root, branch))
631
+
632
+ def task_workspace_path(self, *, run_dir: Path, task_id: str) -> Path:
633
+ return run_dir / "worktrees" / task_id / "repo"
634
+
635
+ def candidate_workspace_path(self, *, run_dir: Path, batch_label: str) -> Path:
636
+ return run_dir / "worktrees" / "_batch_candidates" / batch_label / "repo"
637
+
638
+ def prepare_task_workspace(
639
+ self,
640
+ *,
641
+ root: Path,
642
+ run_dir: Path,
643
+ task_id: str,
644
+ branch: str,
645
+ base_branch: str,
646
+ ) -> PreparedTaskWorkspace:
647
+ worktree_path = self.task_workspace_path(run_dir=run_dir, task_id=task_id)
648
+ if worktree_path.exists():
649
+ valid, clean, _reason = _inspect_existing_git_workspace(
650
+ worktree_path=worktree_path,
651
+ expected_branch=branch,
652
+ )
653
+ if valid:
654
+ if not clean:
655
+ _sanitize_existing_git_workspace(worktree_path=worktree_path)
656
+ if not _tracked_runtime_artifact_paths(worktree_path):
657
+ return PreparedTaskWorkspace(
658
+ backend_name=self.name,
659
+ task_id=task_id,
660
+ branch=branch,
661
+ base_branch=base_branch,
662
+ worktree_path=worktree_path,
663
+ control_root=worktree_path,
664
+ )
665
+ _cleanup_workspace_path(worktree_path)
666
+ copy_workspace_snapshot(src_root=root, dest_root=worktree_path)
667
+ self._init_snapshot_repo(
668
+ worktree_path=worktree_path,
669
+ base_branch=base_branch,
670
+ task_branch=branch,
671
+ task_id=task_id,
672
+ )
673
+ return PreparedTaskWorkspace(
674
+ backend_name=self.name,
675
+ task_id=task_id,
676
+ branch=branch,
677
+ base_branch=base_branch,
678
+ worktree_path=worktree_path,
679
+ control_root=worktree_path,
680
+ )
681
+
682
+ def prepare_candidate_workspace(
683
+ self,
684
+ *,
685
+ root: Path,
686
+ run_dir: Path,
687
+ batch_label: str,
688
+ branch: str,
689
+ base_branch: str,
690
+ ) -> PreparedBatchCandidateWorkspace:
691
+ worktree_path = self.candidate_workspace_path(run_dir=run_dir, batch_label=batch_label)
692
+ if worktree_path.exists():
693
+ _cleanup_workspace_path(worktree_path)
694
+ copy_workspace_snapshot(src_root=root, dest_root=worktree_path)
695
+ self._init_snapshot_repo(
696
+ worktree_path=worktree_path,
697
+ base_branch=base_branch,
698
+ task_branch=branch,
699
+ task_id=batch_label,
700
+ )
701
+ return PreparedBatchCandidateWorkspace(
702
+ backend_name=self.name,
703
+ batch_label=batch_label,
704
+ branch=branch,
705
+ base_branch=base_branch,
706
+ worktree_path=worktree_path,
707
+ control_root=worktree_path,
708
+ )
709
+
710
+ def load_task_workspace(
711
+ self,
712
+ *,
713
+ root: Path,
714
+ run_dir: Path,
715
+ task_id: str,
716
+ branch: str,
717
+ base_branch: str,
718
+ ) -> PreparedTaskWorkspace:
719
+ _ = root
720
+ worktree_path = self.task_workspace_path(run_dir=run_dir, task_id=task_id)
721
+ if not worktree_path.exists():
722
+ raise GitOpsError(
723
+ "snapshot workspace is missing for ready_for_merge task; rerun the task first"
724
+ )
725
+ return PreparedTaskWorkspace(
726
+ backend_name=self.name,
727
+ task_id=task_id,
728
+ branch=branch,
729
+ base_branch=base_branch,
730
+ worktree_path=worktree_path,
731
+ control_root=worktree_path,
732
+ )
733
+
734
+ def apply_task_success(
735
+ self,
736
+ *,
737
+ root: Path,
738
+ prepared_workspace: PreparedTaskWorkspace,
739
+ message: str,
740
+ changed_files: list[str] | None = None,
741
+ ) -> SwarmApplyResult:
742
+ merge_commit_hash = self.merge_runner(
743
+ prepared_workspace.control_root,
744
+ base_branch=prepared_workspace.base_branch,
745
+ task_branch=prepared_workspace.branch,
746
+ message=message,
747
+ )
748
+ effective_changed_files = changed_files or self._changed_files_since_base(
749
+ worktree_path=prepared_workspace.control_root,
750
+ base_branch=prepared_workspace.base_branch,
751
+ )
752
+ sync_snapshot_changed_files(
753
+ snapshot_root=prepared_workspace.worktree_path,
754
+ workspace_root=root,
755
+ changed_files=effective_changed_files,
756
+ )
757
+ return SwarmApplyResult(
758
+ backend_name=self.name,
759
+ task_id=prepared_workspace.task_id,
760
+ branch=prepared_workspace.branch,
761
+ merge_commit_hash=merge_commit_hash,
762
+ action="applied",
763
+ )
764
+
765
+ def apply_task_to_candidate(
766
+ self,
767
+ *,
768
+ root: Path,
769
+ candidate_workspace: PreparedBatchCandidateWorkspace,
770
+ prepared_workspace: PreparedTaskWorkspace,
771
+ message: str,
772
+ changed_files: list[str] | None = None,
773
+ ) -> None:
774
+ _ = root
775
+ _ = message
776
+ effective_changed_files = changed_files or self._changed_files_since_base(
777
+ worktree_path=prepared_workspace.control_root,
778
+ base_branch=prepared_workspace.base_branch,
779
+ )
780
+ sync_snapshot_changed_files(
781
+ snapshot_root=prepared_workspace.worktree_path,
782
+ workspace_root=candidate_workspace.worktree_path,
783
+ changed_files=effective_changed_files,
784
+ )
785
+
786
+ def cleanup_task_workspace(
787
+ self,
788
+ *,
789
+ root: Path,
790
+ prepared_workspace: PreparedTaskWorkspace,
791
+ keep_worktrees: bool,
792
+ ) -> list[str]:
793
+ _ = root
794
+ if keep_worktrees:
795
+ return []
796
+ if not prepared_workspace.worktree_path.exists():
797
+ return []
798
+ try:
799
+ _cleanup_workspace_path(prepared_workspace.worktree_path)
800
+ except (GitOpsError, OSError) as e:
801
+ return [f"snapshot cleanup failed: {e}"]
802
+ return []
803
+
804
+ def cleanup_candidate_workspace(
805
+ self,
806
+ *,
807
+ root: Path,
808
+ candidate_workspace: PreparedBatchCandidateWorkspace,
809
+ keep_worktrees: bool,
810
+ ) -> list[str]:
811
+ _ = root
812
+ if keep_worktrees:
813
+ return []
814
+ if not candidate_workspace.worktree_path.exists():
815
+ return []
816
+ try:
817
+ _cleanup_workspace_path(candidate_workspace.worktree_path)
818
+ except (GitOpsError, OSError) as e:
819
+ return [f"snapshot candidate cleanup failed: {e}"]
820
+ return []
821
+
822
+ def cleanup_failed_task_workspace(
823
+ self,
824
+ *,
825
+ root: Path,
826
+ prepared_workspace: PreparedTaskWorkspace,
827
+ keep_worktrees: bool,
828
+ ) -> list[str]:
829
+ _ = root
830
+ if keep_worktrees:
831
+ return []
832
+ if not prepared_workspace.worktree_path.exists():
833
+ return []
834
+ try:
835
+ _sanitize_existing_git_workspace(worktree_path=prepared_workspace.worktree_path)
836
+ except Exception as e: # noqa: BLE001
837
+ try:
838
+ _cleanup_workspace_path(prepared_workspace.worktree_path)
839
+ except GitOpsError as cleanup_error:
840
+ return [
841
+ "snapshot failure cleanup failed: "
842
+ f"{e}; fallback removal also failed: {cleanup_error}"
843
+ ]
844
+ return [f"snapshot failure cleanup reset failed; removed workspace instead: {e}"]
845
+ return []
846
+
847
+ def _init_snapshot_repo(
848
+ self,
849
+ *,
850
+ worktree_path: Path,
851
+ base_branch: str,
852
+ task_branch: str,
853
+ task_id: str,
854
+ ) -> None:
855
+ worktree_path.mkdir(parents=True, exist_ok=True)
856
+ _run_git_checked(
857
+ worktree_path, ["init", "-q"], error_message="failed to init snapshot repo"
858
+ )
859
+ _run_git_checked(
860
+ worktree_path,
861
+ ["config", "user.name", "alysis-agent"],
862
+ error_message="failed to configure snapshot git user.name",
863
+ )
864
+ _run_git_checked(
865
+ worktree_path,
866
+ ["config", "user.email", "alysis-agent@local"],
867
+ error_message="failed to configure snapshot git user.email",
868
+ )
869
+ ensure_runtime_artifact_excludes(worktree_path)
870
+ _run_git_checked(
871
+ worktree_path,
872
+ ["checkout", "-b", base_branch],
873
+ error_message=f"failed to create snapshot base branch {base_branch}",
874
+ )
875
+ _run_git_checked(
876
+ worktree_path, ["add", "-A"], error_message="failed to stage snapshot files"
877
+ )
878
+ _run_git_checked(
879
+ worktree_path,
880
+ ["commit", "--allow-empty", "-m", f"Snapshot baseline for {task_id}"],
881
+ error_message="failed to create snapshot baseline commit",
882
+ extra_config={"user.name": "alysis-agent", "user.email": "alysis-agent@local"},
883
+ )
884
+ _run_git_checked(
885
+ worktree_path,
886
+ ["checkout", "-b", task_branch],
887
+ error_message=f"failed to create snapshot task branch {task_branch}",
888
+ )
889
+
890
+ def _changed_files_since_base(self, *, worktree_path: Path, base_branch: str) -> list[str]:
891
+ cp = _run_git_checked(
892
+ worktree_path,
893
+ ["diff", "--name-only", f"{base_branch}..HEAD"],
894
+ error_message=f"failed to list snapshot changes from {base_branch}",
895
+ )
896
+ return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
897
+
898
+
899
+ def select_swarm_backend(
900
+ *,
901
+ paths: RunPaths,
902
+ ensure_worktree_fn: Any,
903
+ merge_runner: Any,
904
+ remove_worktree_fn: Any,
905
+ delete_branch_fn: Any,
906
+ prune_fn: Any,
907
+ branch_exists_fn: Any,
908
+ current_branch_fn: Any,
909
+ has_head_commit_fn: Any,
910
+ ) -> SwarmBackend:
911
+ if paths.workspace_kind in {"plain_dir", "git_repo_no_head"} or not paths.has_head_commit:
912
+ return SnapshotSwarmBackend(
913
+ merge_runner=merge_runner,
914
+ branch_exists_fn=branch_exists_fn,
915
+ current_branch_fn=current_branch_fn,
916
+ )
917
+ return GitWorktreeSwarmBackend(
918
+ ensure_worktree_fn=ensure_worktree_fn,
919
+ merge_runner=merge_runner,
920
+ remove_worktree_fn=remove_worktree_fn,
921
+ delete_branch_fn=delete_branch_fn,
922
+ prune_fn=prune_fn,
923
+ branch_exists_fn=branch_exists_fn,
924
+ current_branch_fn=current_branch_fn,
925
+ has_head_commit_fn=has_head_commit_fn,
926
+ )