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,666 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from dataclasses import dataclass, replace
6
+ from pathlib import Path
7
+ from threading import RLock
8
+ from typing import Any, Literal
9
+
10
+ from ..atomic_io import atomic_write_text
11
+ from ..git_evidence import CandidateGitState, capture_candidate_git_state
12
+ from ..session_store import SessionStore
13
+ from ..workspace_isolation import GitOpsError, _cleanup_workspace_path, _run_git_checked
14
+
15
+ WorkspaceState = Literal["prepared", "captured", "applied", "discarded"]
16
+ WorkspaceReleaseAction = Literal["applied", "discarded"]
17
+
18
+ _SAFE_RUN_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
19
+ _PATCH_FAILED_PATH_RE = re.compile(r"patch failed: (.+?):\d+(?:\s|$)")
20
+ _PATCH_DOES_NOT_APPLY_PATH_RE = re.compile(r"error: (.+?): patch does not apply")
21
+
22
+
23
+ @dataclass
24
+ class SubagentWorkspaceRecord:
25
+ run_id: str
26
+ worktree_path: Path
27
+ base_commit: str
28
+ parent_dirty_paths: tuple[str, ...]
29
+ state: WorkspaceState = "prepared"
30
+ evidence: CandidateGitState | None = None
31
+ patch_artifact: str = ""
32
+ pinned_by_run_ids: tuple[str, ...] = ()
33
+ no_changes: bool = False
34
+ cleanup_pending: bool = False
35
+ cleanup_error: str = ""
36
+
37
+
38
+ class SubagentWorkspaceProvider:
39
+ """Own isolated Git worktrees for one parent agent session."""
40
+
41
+ def __init__(self, *, root: Path, store: SessionStore) -> None:
42
+ self.root = root.resolve()
43
+ self.store = store
44
+ self.worktrees_root = store.session_artifact_root / "subagent_worktrees"
45
+ self._records: dict[str, SubagentWorkspaceRecord] = {}
46
+ self._lock = RLock()
47
+
48
+ def _event(self, run_id: str, action: str, **fields: Any) -> None:
49
+ self.store.append(
50
+ "subagent_workspace",
51
+ {"run_id": run_id, "action": action, **fields},
52
+ )
53
+
54
+ @staticmethod
55
+ def _error(*, run_id: str, error_code: str, message: str) -> dict[str, Any]:
56
+ return {
57
+ "ok": False,
58
+ "run_id": run_id,
59
+ "error": message,
60
+ "error_code": error_code,
61
+ }
62
+
63
+ @classmethod
64
+ def _release_locked_error(cls, record: SubagentWorkspaceRecord) -> dict[str, Any]:
65
+ return {
66
+ **cls._error(
67
+ run_id=record.run_id,
68
+ error_code="workspace_release_locked",
69
+ message=(
70
+ f"Isolated workspace {record.run_id} is pinned by a running child: "
71
+ f"{', '.join(record.pinned_by_run_ids)}."
72
+ ),
73
+ ),
74
+ "pinned_by_run_ids": list(record.pinned_by_run_ids),
75
+ }
76
+
77
+ @staticmethod
78
+ def _validate_run_id(run_id: str) -> str:
79
+ normalized = str(run_id or "").strip()
80
+ if not _SAFE_RUN_ID_RE.fullmatch(normalized):
81
+ raise ValueError("invalid subagent workspace run id")
82
+ return normalized
83
+
84
+ def _parent_git_context(self) -> tuple[str, tuple[str, ...]]:
85
+ inside = _run_git_checked(
86
+ self.root,
87
+ ["rev-parse", "--is-inside-work-tree"],
88
+ error_message="workspace_view=isolated requires a git repository",
89
+ )
90
+ if inside.stdout.strip().lower() != "true":
91
+ raise GitOpsError("workspace_view=isolated requires a git repository")
92
+ base_commit = _run_git_checked(
93
+ self.root,
94
+ ["rev-parse", "HEAD"],
95
+ error_message="failed to resolve parent HEAD",
96
+ ).stdout.strip()
97
+ if not base_commit:
98
+ raise GitOpsError("failed to resolve parent HEAD")
99
+ status = _run_git_checked(
100
+ self.root,
101
+ ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
102
+ error_message="failed to inspect parent workspace",
103
+ ).stdout
104
+ return base_commit, _porcelain_paths(status)
105
+
106
+ def prepare(self, run_id: str) -> dict[str, Any]:
107
+ try:
108
+ normalized_run_id = self._validate_run_id(run_id)
109
+ except ValueError as exc:
110
+ return self._error(
111
+ run_id=str(run_id or ""),
112
+ error_code="invalid_workspace_run_id",
113
+ message=str(exc),
114
+ )
115
+ with self._lock:
116
+ if normalized_run_id in self._records:
117
+ return self._error(
118
+ run_id=normalized_run_id,
119
+ error_code="workspace_run_already_exists",
120
+ message=f"An isolated workspace already exists for run {normalized_run_id}.",
121
+ )
122
+ try:
123
+ base_commit, parent_dirty_paths = self._parent_git_context()
124
+ except GitOpsError:
125
+ return self._error(
126
+ run_id=normalized_run_id,
127
+ error_code="isolated_workspace_requires_git",
128
+ message="workspace_view=isolated requires a git repository",
129
+ )
130
+
131
+ worktree_path = self.worktrees_root / normalized_run_id
132
+ if worktree_path.exists():
133
+ return self._error(
134
+ run_id=normalized_run_id,
135
+ error_code="workspace_path_exists",
136
+ message=f"Isolated workspace path already exists for run {normalized_run_id}.",
137
+ )
138
+ self.worktrees_root.mkdir(parents=True, exist_ok=True)
139
+ try:
140
+ _run_git_checked(
141
+ self.root,
142
+ ["worktree", "add", "--detach", os.fspath(worktree_path), base_commit],
143
+ error_message="failed to create isolated subagent workspace",
144
+ )
145
+ except GitOpsError as exc:
146
+ return self._error(
147
+ run_id=normalized_run_id,
148
+ error_code="workspace_prepare_failed",
149
+ message=str(exc),
150
+ )
151
+
152
+ record = SubagentWorkspaceRecord(
153
+ run_id=normalized_run_id,
154
+ worktree_path=worktree_path,
155
+ base_commit=base_commit,
156
+ parent_dirty_paths=parent_dirty_paths,
157
+ )
158
+ self._records[normalized_run_id] = record
159
+ self._event(
160
+ normalized_run_id,
161
+ "prepared",
162
+ base_commit=base_commit,
163
+ parent_dirty_paths=list(parent_dirty_paths),
164
+ )
165
+ return {
166
+ "ok": True,
167
+ "run_id": normalized_run_id,
168
+ "worktree_path": os.fspath(worktree_path),
169
+ "base_commit": base_commit,
170
+ "parent_dirty_paths": list(parent_dirty_paths),
171
+ }
172
+
173
+ def capture(self, run_id: str) -> dict[str, Any]:
174
+ with self._lock:
175
+ record = self._records.get(str(run_id or "").strip())
176
+ if record is None:
177
+ return self._error(
178
+ run_id=str(run_id or ""),
179
+ error_code="unknown_workspace_run",
180
+ message=f"Unknown isolated workspace run: {run_id}",
181
+ )
182
+ if record.state in {"applied", "discarded"}:
183
+ return self._error(
184
+ run_id=record.run_id,
185
+ error_code="workspace_already_released",
186
+ message=f"Isolated workspace {record.run_id} was already {record.state}.",
187
+ )
188
+ evidence = capture_candidate_git_state(
189
+ worktree_path=record.worktree_path,
190
+ base_ref=record.base_commit,
191
+ )
192
+ artifact_path = self.store.session_artifact_layout.artifact_fs_path(
193
+ "subagent_patches",
194
+ f"{record.run_id}.patch",
195
+ )
196
+ atomic_write_text(artifact_path, evidence.patch_text)
197
+ patch_artifact = self.store.session_artifact_layout.locator_for_path(artifact_path)
198
+ record.evidence = evidence
199
+ record.patch_artifact = patch_artifact
200
+ record.state = "captured"
201
+ paths = list(evidence.material_changed_paths)
202
+ no_changes = not paths
203
+ record.no_changes = no_changes
204
+ self._event(
205
+ record.run_id,
206
+ "captured",
207
+ base_commit=record.base_commit,
208
+ patch_artifact=patch_artifact,
209
+ paths=paths,
210
+ no_changes=no_changes,
211
+ )
212
+ result = {
213
+ "ok": True,
214
+ "run_id": record.run_id,
215
+ "base_commit": record.base_commit,
216
+ "parent_dirty_paths": list(record.parent_dirty_paths),
217
+ "patch_artifact": patch_artifact,
218
+ "paths": paths,
219
+ "insertions": _patch_line_count(evidence.patch_text, prefix="+"),
220
+ "deletions": _patch_line_count(evidence.patch_text, prefix="-"),
221
+ "sha256": evidence.patch_text_sha256,
222
+ "patch_capture_status": evidence.patch_capture_status,
223
+ "material_patch_complete": evidence.material_patch_complete,
224
+ "no_changes": no_changes,
225
+ }
226
+ if no_changes:
227
+ released = self.release(
228
+ record.run_id,
229
+ action="discarded",
230
+ reason="no_changes",
231
+ )
232
+ if not bool(released.get("ok")):
233
+ return released
234
+ return result
235
+
236
+ def get(self, run_id: str) -> SubagentWorkspaceRecord | None:
237
+ with self._lock:
238
+ record = self._records.get(str(run_id or "").strip())
239
+ return replace(record) if record is not None else None
240
+
241
+ def reattach_for_resume(self, source_run_id: str, new_run_id: str) -> dict[str, Any]:
242
+ try:
243
+ source = self._validate_run_id(source_run_id)
244
+ target = self._validate_run_id(new_run_id)
245
+ except ValueError as exc:
246
+ return self._error(
247
+ run_id=str(new_run_id or ""),
248
+ error_code="invalid_workspace_run_id",
249
+ message=str(exc),
250
+ )
251
+ with self._lock:
252
+ record = self._records.get(source)
253
+ if record is None:
254
+ return self._error(
255
+ run_id=source,
256
+ error_code="unknown_workspace_run",
257
+ message=f"Unknown isolated workspace run: {source}",
258
+ )
259
+ if record.state in {"applied", "discarded"}:
260
+ return self._error(
261
+ run_id=source,
262
+ error_code="subagent_resume_worktree_released",
263
+ message=f"Isolated workspace {source} was already {record.state}.",
264
+ )
265
+ if record.pinned_by_run_ids:
266
+ return self._release_locked_error(record)
267
+ if target in self._records:
268
+ return self._error(
269
+ run_id=target,
270
+ error_code="workspace_run_already_exists",
271
+ message=f"An isolated workspace already exists for run {target}.",
272
+ )
273
+ self._records.pop(source)
274
+ record.run_id = target
275
+ self._records[target] = record
276
+ self._event(
277
+ target,
278
+ "reattached",
279
+ resumed_from=source,
280
+ base_commit=record.base_commit,
281
+ patch_artifact=record.patch_artifact or None,
282
+ )
283
+ return {
284
+ "ok": True,
285
+ "run_id": target,
286
+ "resumed_from": source,
287
+ "worktree_path": os.fspath(record.worktree_path),
288
+ "patch_artifact": record.patch_artifact or None,
289
+ }
290
+
291
+ def inspect_pin_source(self, run_id: str) -> dict[str, Any]:
292
+ with self._lock:
293
+ return self._inspect_pin_source_locked(str(run_id or "").strip())
294
+
295
+ def _inspect_pin_source_locked(self, run_id: str) -> dict[str, Any]:
296
+ record = self._records.get(run_id)
297
+ if record is None:
298
+ return self._error(
299
+ run_id=run_id,
300
+ error_code="unknown_workspace_from_run",
301
+ message=f"Unknown isolated workspace run: {run_id}",
302
+ )
303
+ if record.state in {"applied", "discarded"}:
304
+ return self._error(
305
+ run_id=record.run_id,
306
+ error_code="workspace_from_run_released",
307
+ message=f"Isolated workspace {record.run_id} was already {record.state}.",
308
+ )
309
+ if record.state != "captured" or record.evidence is None:
310
+ return self._error(
311
+ run_id=record.run_id,
312
+ error_code="workspace_from_run_not_ready",
313
+ message=f"Isolated workspace {record.run_id} is not a completed result.",
314
+ )
315
+ return {
316
+ "ok": True,
317
+ "run_id": record.run_id,
318
+ "worktree_path": os.fspath(record.worktree_path),
319
+ "base_commit": record.base_commit,
320
+ "paths": list(record.evidence.material_changed_paths),
321
+ }
322
+
323
+ def acquire_pin(self, run_id: str, *, consumer_run_id: str) -> dict[str, Any]:
324
+ with self._lock:
325
+ inspected = self._inspect_pin_source_locked(str(run_id or "").strip())
326
+ if not bool(inspected.get("ok")):
327
+ return inspected
328
+ record = self._records[str(inspected["run_id"])]
329
+ consumer = str(consumer_run_id or "").strip()
330
+ if consumer not in record.pinned_by_run_ids:
331
+ record.pinned_by_run_ids = (*record.pinned_by_run_ids, consumer)
332
+ self._event(
333
+ record.run_id,
334
+ "pinned",
335
+ consumer_run_id=consumer,
336
+ pinned_by_run_ids=list(record.pinned_by_run_ids),
337
+ )
338
+ return inspected
339
+
340
+ def release_pin(self, run_id: str, *, consumer_run_id: str) -> None:
341
+ with self._lock:
342
+ record = self._records.get(str(run_id or "").strip())
343
+ if record is None:
344
+ return
345
+ consumer = str(consumer_run_id or "").strip()
346
+ if consumer not in record.pinned_by_run_ids:
347
+ return
348
+ record.pinned_by_run_ids = tuple(
349
+ candidate for candidate in record.pinned_by_run_ids if candidate != consumer
350
+ )
351
+ self._event(
352
+ record.run_id,
353
+ "unpinned",
354
+ consumer_run_id=consumer,
355
+ pinned_by_run_ids=list(record.pinned_by_run_ids),
356
+ )
357
+
358
+ def unapplied_summaries(self) -> list[dict[str, Any]]:
359
+ with self._lock:
360
+ records = [
361
+ replace(record)
362
+ for record in self._records.values()
363
+ if record.state == "captured" and not record.no_changes
364
+ ]
365
+ summaries: list[dict[str, Any]] = []
366
+ for record in records:
367
+ evidence = record.evidence
368
+ patch_text = evidence.patch_text if evidence is not None else ""
369
+ summaries.append(
370
+ {
371
+ "run_id": record.run_id,
372
+ "files": (
373
+ list(evidence.material_changed_paths) if evidence is not None else []
374
+ ),
375
+ "insertions": _patch_line_count(patch_text, prefix="+"),
376
+ "deletions": _patch_line_count(patch_text, prefix="-"),
377
+ }
378
+ )
379
+ return summaries
380
+
381
+ def apply(self, run_id: str) -> dict[str, Any]:
382
+ with self._lock:
383
+ record = self._records.get(str(run_id or "").strip())
384
+ if record is None:
385
+ return self._error(
386
+ run_id=str(run_id or ""),
387
+ error_code="unknown_workspace_run",
388
+ message=f"Unknown isolated workspace run: {run_id}",
389
+ )
390
+ if record.no_changes:
391
+ return self._error(
392
+ run_id=record.run_id,
393
+ error_code="no_changes",
394
+ message=f"Isolated workspace {record.run_id} produced no changes.",
395
+ )
396
+ if record.state in {"applied", "discarded"}:
397
+ return self._error(
398
+ run_id=record.run_id,
399
+ error_code=f"workspace_already_{record.state}",
400
+ message=f"Isolated workspace {record.run_id} was already {record.state}.",
401
+ )
402
+ if record.pinned_by_run_ids:
403
+ return self._release_locked_error(record)
404
+ if record.evidence is None or record.state != "captured":
405
+ return self._error(
406
+ run_id=record.run_id,
407
+ error_code="workspace_not_captured",
408
+ message=f"Isolated workspace {record.run_id} has not been captured.",
409
+ )
410
+ evidence = record.evidence
411
+ paths = list(evidence.material_changed_paths)
412
+ if not paths or not evidence.patch_text:
413
+ return self._error(
414
+ run_id=record.run_id,
415
+ error_code="no_captured_changes",
416
+ message=f"Isolated workspace {record.run_id} has no captured changes.",
417
+ )
418
+ if not evidence.material_patch_complete:
419
+ return self._error(
420
+ run_id=record.run_id,
421
+ error_code="incomplete_workspace_patch",
422
+ message=f"Isolated workspace {record.run_id} has an incomplete patch.",
423
+ )
424
+
425
+ patch_path = self.store.session_artifact_layout.artifact_fs_path(
426
+ "subagent_patches",
427
+ f"{record.run_id}.patch",
428
+ )
429
+ try:
430
+ _run_git_checked(
431
+ self.root,
432
+ ["apply", "--check", os.fspath(patch_path)],
433
+ error_message="isolated subagent patch conflicts with the parent workspace",
434
+ )
435
+ except GitOpsError as exc:
436
+ conflicts = _conflicting_patch_paths(str(exc), fallback=paths)
437
+ self._event(
438
+ record.run_id,
439
+ "conflict",
440
+ base_commit=record.base_commit,
441
+ patch_artifact=record.patch_artifact,
442
+ paths=conflicts,
443
+ )
444
+ return {
445
+ "ok": False,
446
+ "run_id": record.run_id,
447
+ "error": str(exc),
448
+ "error_code": "merge_conflict",
449
+ "conflicting_paths": conflicts,
450
+ "patch_artifact": record.patch_artifact,
451
+ }
452
+
453
+ try:
454
+ _run_git_checked(
455
+ self.root,
456
+ ["apply", os.fspath(patch_path)],
457
+ error_message="failed to apply isolated subagent patch",
458
+ )
459
+ except GitOpsError as exc:
460
+ conflicts = _conflicting_patch_paths(str(exc), fallback=paths)
461
+ self._event(
462
+ record.run_id,
463
+ "conflict",
464
+ base_commit=record.base_commit,
465
+ patch_artifact=record.patch_artifact,
466
+ paths=conflicts,
467
+ )
468
+ return {
469
+ "ok": False,
470
+ "run_id": record.run_id,
471
+ "error": str(exc),
472
+ "error_code": "merge_conflict",
473
+ "conflicting_paths": conflicts,
474
+ "patch_artifact": record.patch_artifact,
475
+ }
476
+
477
+ summary = {
478
+ "files": paths,
479
+ "insertions": _patch_line_count(evidence.patch_text, prefix="+"),
480
+ "deletions": _patch_line_count(evidence.patch_text, prefix="-"),
481
+ "patch_artifact": record.patch_artifact,
482
+ "sha256": evidence.patch_text_sha256,
483
+ }
484
+ # The parent mutation is authoritative once ``git apply`` succeeds.
485
+ # Worktree cleanup is ancillary and cannot roll that mutation back,
486
+ # so record the logical state before attempting cleanup.
487
+ record.state = "applied"
488
+ self._event(
489
+ record.run_id,
490
+ "applied",
491
+ base_commit=record.base_commit,
492
+ patch_artifact=record.patch_artifact or None,
493
+ paths=paths,
494
+ )
495
+ result: dict[str, Any] = {
496
+ "ok": True,
497
+ "run_id": record.run_id,
498
+ "applied_paths": paths,
499
+ "patch_summary": summary,
500
+ }
501
+ cleanup_error = self._remove_worktree(record.worktree_path)
502
+ if cleanup_error:
503
+ record.cleanup_pending = True
504
+ record.cleanup_error = cleanup_error
505
+ self._event(
506
+ record.run_id,
507
+ "cleanup_failed",
508
+ release_action="applied",
509
+ error=cleanup_error,
510
+ )
511
+ result.update(
512
+ {
513
+ "cleanup_pending": True,
514
+ "cleanup_warning": cleanup_error,
515
+ }
516
+ )
517
+ return result
518
+
519
+ def release(
520
+ self,
521
+ run_id: str,
522
+ *,
523
+ action: WorkspaceReleaseAction,
524
+ reason: str | None = None,
525
+ ) -> dict[str, Any]:
526
+ with self._lock:
527
+ record = self._records.get(str(run_id or "").strip())
528
+ if record is None:
529
+ return self._error(
530
+ run_id=str(run_id or ""),
531
+ error_code="unknown_workspace_run",
532
+ message=f"Unknown isolated workspace run: {run_id}",
533
+ )
534
+ if record.state in {"applied", "discarded"}:
535
+ return self._error(
536
+ run_id=record.run_id,
537
+ error_code=f"workspace_already_{record.state}",
538
+ message=f"Isolated workspace {record.run_id} was already {record.state}.",
539
+ )
540
+ if record.pinned_by_run_ids:
541
+ return self._release_locked_error(record)
542
+ cleanup_error = self._remove_worktree(record.worktree_path)
543
+ if cleanup_error:
544
+ return self._error(
545
+ run_id=record.run_id,
546
+ error_code="workspace_release_failed",
547
+ message=cleanup_error,
548
+ )
549
+ record.state = action
550
+ paths = (
551
+ list(record.evidence.material_changed_paths) if record.evidence is not None else []
552
+ )
553
+ self._event(
554
+ record.run_id,
555
+ action,
556
+ base_commit=record.base_commit,
557
+ patch_artifact=record.patch_artifact or None,
558
+ paths=paths,
559
+ **({"reason": reason} if reason else {}),
560
+ )
561
+ return {
562
+ "ok": True,
563
+ "run_id": record.run_id,
564
+ "action": action,
565
+ "paths": paths,
566
+ "patch_artifact": record.patch_artifact or None,
567
+ }
568
+
569
+ def _remove_worktree(self, worktree_path: Path) -> str:
570
+ try:
571
+ _run_git_checked(
572
+ self.root,
573
+ ["worktree", "remove", "--force", os.fspath(worktree_path)],
574
+ error_message="failed to remove isolated subagent workspace",
575
+ )
576
+ return ""
577
+ except GitOpsError as exc:
578
+ try:
579
+ _cleanup_workspace_path(worktree_path)
580
+ _run_git_checked(
581
+ self.root,
582
+ ["worktree", "prune"],
583
+ error_message="failed to prune isolated subagent workspace metadata",
584
+ )
585
+ except GitOpsError:
586
+ return str(exc)
587
+ return ""
588
+
589
+ def close(self) -> None:
590
+ with self._lock:
591
+ for record in self._records.values():
592
+ record.pinned_by_run_ids = ()
593
+ pending = [
594
+ run_id
595
+ for run_id, record in self._records.items()
596
+ if record.state not in {"applied", "discarded"}
597
+ ]
598
+ cleanup_pending = [
599
+ run_id for run_id, record in self._records.items() if record.cleanup_pending
600
+ ]
601
+ for run_id in pending:
602
+ self.release(run_id, action="discarded")
603
+ for run_id in cleanup_pending:
604
+ with self._lock:
605
+ record = self._records.get(run_id)
606
+ if record is None or not record.cleanup_pending:
607
+ continue
608
+ cleanup_error = self._remove_worktree(record.worktree_path)
609
+ if cleanup_error:
610
+ record.cleanup_error = cleanup_error
611
+ self._event(
612
+ record.run_id,
613
+ "cleanup_failed",
614
+ release_action=record.state,
615
+ error=cleanup_error,
616
+ retry="session_close",
617
+ )
618
+ continue
619
+ record.cleanup_pending = False
620
+ record.cleanup_error = ""
621
+ self._event(
622
+ record.run_id,
623
+ "cleanup_completed",
624
+ release_action=record.state,
625
+ retry="session_close",
626
+ )
627
+
628
+
629
+ def _porcelain_paths(status: str) -> tuple[str, ...]:
630
+ records = status.split("\0")
631
+ paths: set[str] = set()
632
+ index = 0
633
+ while index < len(records):
634
+ record = records[index]
635
+ index += 1
636
+ if not record:
637
+ continue
638
+ state = record[:2]
639
+ path = record[3:] if len(record) > 3 else ""
640
+ if path:
641
+ paths.add(path.replace("\\", "/"))
642
+ if any(marker in state for marker in ("R", "C")) and index < len(records):
643
+ original = records[index]
644
+ index += 1
645
+ if original:
646
+ paths.add(original.replace("\\", "/"))
647
+ return tuple(sorted(paths))
648
+
649
+
650
+ def _patch_line_count(patch_text: str, *, prefix: str) -> int:
651
+ header = f"{prefix}{prefix}{prefix}"
652
+ return sum(
653
+ 1
654
+ for line in patch_text.splitlines()
655
+ if line.startswith(prefix) and not line.startswith(header)
656
+ )
657
+
658
+
659
+ def _conflicting_patch_paths(message: str, *, fallback: list[str]) -> list[str]:
660
+ found = {
661
+ match.group(1).strip().replace("\\", "/")
662
+ for pattern in (_PATCH_FAILED_PATH_RE, _PATCH_DOES_NOT_APPLY_PATH_RE)
663
+ for match in pattern.finditer(message)
664
+ if match.group(1).strip()
665
+ }
666
+ return sorted(found) if found else sorted(set(fallback))