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,2119 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import os
5
+ import re
6
+ import subprocess
7
+ import tarfile
8
+ import tempfile
9
+ import threading
10
+ import traceback
11
+ from collections.abc import Callable
12
+ from dataclasses import dataclass, replace
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from rich.console import Console
17
+
18
+ from .agent_loop import run_agent
19
+ from .assets import AssetError
20
+ from .assets.budget_allocator import (
21
+ TaskAssetAllocation,
22
+ allocate_task_assets,
23
+ write_task_asset_allocation,
24
+ )
25
+ from .assets.surface import build_asset_surface
26
+ from .assets.usage_logger import AssetUsageLogger
27
+ from .assets.worker_mirror import TaskAssetMirror, mirror_task_assets
28
+ from .assets.worker_section import render_relevant_assets_section
29
+ from .assets.worker_tools import build_worker_asset_mcp_manager, compose_worker_asset_mcp_manager
30
+ from .cancellation import CooperativeCancellationError, EventCancellationToken
31
+ from .config import AppConfig, clone_cfg
32
+ from .error_text import (
33
+ redact_sensitive_error_text,
34
+ sanitize_error_summary,
35
+ sanitize_optional_error_summary,
36
+ )
37
+ from .execution_shared import (
38
+ build_execution_reporting_diff_with_commit_range,
39
+ build_task_execution_instruction_bundle,
40
+ build_workspace_snapshot_reporting_diff,
41
+ mirror_plan_into_worktree,
42
+ mirror_selected_knowledge_into_worktree,
43
+ prepare_task_execution_knowledge,
44
+ resolve_managed_task_step_budget,
45
+ safe_task_file_component,
46
+ snapshot_runtime_tree,
47
+ snapshot_workspace_tree,
48
+ write_exec_log_artifacts,
49
+ write_execution_budget_artifact,
50
+ write_execution_context_artifact,
51
+ )
52
+ from .failure_category import (
53
+ FailureCategory,
54
+ classify_failure_category,
55
+ failure_category_value,
56
+ )
57
+ from .forge import RunPaths, ensure_execution_dirs, now_iso, write_task_report
58
+ from .git_ops import (
59
+ GitOpsError,
60
+ added_files_since,
61
+ changed_files_between,
62
+ changed_files_since,
63
+ commit_all,
64
+ ensure_not_staged_paths,
65
+ ensure_not_staged_prefixes,
66
+ ensure_not_staged_runtime_artifacts,
67
+ ensure_runtime_artifact_excludes,
68
+ format_patch_stdout,
69
+ head_commit,
70
+ reset_mixed,
71
+ stage_all,
72
+ staged_files,
73
+ unstage_staged_paths,
74
+ unstage_staged_prefixes,
75
+ unstage_staged_runtime_artifacts,
76
+ )
77
+ from .knowledge_base import rebuild_knowledge_index, write_issue_entry, write_task_attempt_entry
78
+ from .knowledge_capture import (
79
+ RecordingSurface,
80
+ mark_knowledge_capture_promotion_skipped,
81
+ persist_execution_knowledge_capture,
82
+ )
83
+ from .mcp.manager import create_mcp_manager
84
+ from .model_registry import ModelRegistry
85
+ from .model_router import ROLE_CODING, resolve_model_for_role
86
+ from .repo_scan import RepoScanResult, scan_workspace
87
+ from .runtime_artifacts import has_grounded_rust_target_runtime_artifacts
88
+ from .runtime_kind import RuntimeKind
89
+ from .surface.console import make_console
90
+ from .swarm_trace import (
91
+ NoopSwarmTraceSink,
92
+ SwarmTraceSink,
93
+ SwarmWorkerTraceSurface,
94
+ build_swarm_trace_event,
95
+ )
96
+ from .swarm_write_guard import SwarmWriteScopeGuard
97
+ from .task_readiness import TASK_KIND_ANALYSIS_ONLY, classify_task_lifecycle
98
+ from .task_scope import (
99
+ SCRATCH_ARTIFACT_ENV,
100
+ ScopeViolationDiagnostic,
101
+ assess_scope_changes,
102
+ is_non_material_untracked_path,
103
+ list_changed_files_including_untracked,
104
+ list_untracked_packaging_metadata_paths,
105
+ normalize_scope_patterns,
106
+ relocate_known_scratch_artifacts,
107
+ )
108
+ from .verify_gate import (
109
+ ResolvedVerifyCommands,
110
+ VerifyRunResult,
111
+ resolve_authoritative_task_verify_command_selection,
112
+ run_task_verification,
113
+ verify_run_result_to_payload,
114
+ )
115
+ from .workspace_context import WorkspaceContextError, resolve_workspace_context
116
+
117
+ _DIAGNOSTIC_TASK_TITLE_RE = re.compile(
118
+ r"^\s*(?:explore|investigate|examine|inspect|locate|find|survey|map|analy[sz]e|diagnose|audit|understand|identify)\b",
119
+ re.IGNORECASE,
120
+ )
121
+ _MUTATING_TASK_TEXT_RE = re.compile(
122
+ r"\b(?:fix|implement|update|add|change|modify|create|delete|remove|refactor|write)\b",
123
+ re.IGNORECASE,
124
+ )
125
+ _CONDITIONAL_NOOP_TASK_RE = re.compile(
126
+ r"\b(?:if|when)\b.{0,100}\b(?:exists?|present|available|needed|applicable)\b"
127
+ r"|\bif\s+needed\b|\bwhere\s+applicable\b",
128
+ re.IGNORECASE | re.DOTALL,
129
+ )
130
+ _VERIFY_FAILED_TEST_RE = re.compile(r"^\s*(?:FAILED|ERROR)\s+([^\s]+)", re.MULTILINE)
131
+ _VERIFY_FAILURE_SECTION_RE = re.compile(r"^_{3,}\s+(.+?)\s+_{3,}\s*$", re.MULTILINE)
132
+ _VERIFY_ACTIONABLE_FAILURE_RE = re.compile(
133
+ r"^\s*(?:E\s+|AssertionError|ImportError|ModuleNotFoundError|ValueError|TypeError|"
134
+ r"SyntaxError|NameError|AttributeError)(.+)?$",
135
+ re.MULTILINE,
136
+ )
137
+ _KNOWLEDGE_INDEX_REBUILD_LOCK = threading.Lock()
138
+ _BASELINE_FAILURE_TOKEN_RE = re.compile(r"[A-Za-z][A-Za-z0-9_]{2,}")
139
+ _BASELINE_FAILURE_STOPWORDS = {
140
+ "assert",
141
+ "assertion",
142
+ "command",
143
+ "error",
144
+ "expected",
145
+ "failed",
146
+ "failure",
147
+ "format",
148
+ "formatting",
149
+ "function",
150
+ "got",
151
+ "line",
152
+ "module",
153
+ "pytest",
154
+ "render",
155
+ "render_value",
156
+ "return",
157
+ "returns",
158
+ "style",
159
+ "test",
160
+ "tests",
161
+ "unknown",
162
+ "value",
163
+ "values",
164
+ }
165
+
166
+
167
+ class _HostManagedApprovalSurface:
168
+ """Route worker approval requests to the owning IDE session."""
169
+
170
+ host_managed_approvals = True
171
+
172
+ def __init__(self, delegate: Any, *, task_id: str, handler: Callable[..., Any]) -> None:
173
+ self._delegate = delegate
174
+ self._task_id = task_id
175
+ self._handler = handler
176
+
177
+ def request_approval(self, request: Any) -> Any:
178
+ return self._handler(task_id=self._task_id, request=request)
179
+
180
+ def __getattr__(self, name: str) -> Any:
181
+ return getattr(self._delegate, name)
182
+
183
+
184
+ @dataclass(frozen=True)
185
+ class TaskWorkerResult:
186
+ task_id: str
187
+ title: str
188
+ branch: str
189
+ worktree_path: str
190
+ started_at: str
191
+ finished_at: str
192
+ success: bool
193
+ summary: str
194
+ commit_hash: str | None
195
+ error: str | None
196
+ report_path: str
197
+ patch_path: str
198
+ log_path: str
199
+ log_pointer_path: str
200
+ warnings: list[str]
201
+ changed_files: list[str]
202
+ verify_failed: bool
203
+ verify_summary: str | None
204
+ verify_artifact_path: str | None
205
+ # True when the task succeeded but nothing authoritative could check it. A
206
+ # completion, not a failure -- the orchestrator keeps the work and merges nothing.
207
+ verification_unavailable: bool = False
208
+ knowledge_capture_artifact_dir: str | None = None
209
+ task_attempt_entry_id: str | None = None
210
+ task_attempt_knowledge_file_path: str | None = None
211
+ verify_payload: dict[str, Any] | None = None
212
+ verify_command_source: str | None = None
213
+ failure_reason: str | None = None
214
+ failure_category: FailureCategory | str | None = None
215
+ scope_violation_files: list[str] | None = None
216
+ scope_diagnostics: list[dict[str, Any]] | None = None
217
+ allowed_scope: list[str] | None = None
218
+ agent_exit_code: int = 0
219
+ salvaged_nonzero_exit: bool = False
220
+ salvaged_agent_exception: bool = False
221
+ agent_exception_summary: str | None = None
222
+ result_kind: str | None = None
223
+ noop_reason: str | None = None
224
+ task_kind: str | None = None
225
+ task_lifecycle_reason: str | None = None
226
+ interrupted: bool = False
227
+
228
+ @property
229
+ def effective_result_kind(self) -> str:
230
+ if self.result_kind:
231
+ return self.result_kind
232
+ if self.success:
233
+ return "success_commit"
234
+ return "failure"
235
+
236
+ @property
237
+ def noop_success(self) -> bool:
238
+ return self.effective_result_kind == "success_noop"
239
+
240
+ def to_json(self) -> dict[str, Any]:
241
+ clean_agent_exception_summary = sanitize_optional_error_summary(
242
+ self.agent_exception_summary
243
+ )
244
+ return {
245
+ "task_id": self.task_id,
246
+ "title": self.title,
247
+ "branch": self.branch,
248
+ "worktree_path": self.worktree_path,
249
+ "started_at": self.started_at,
250
+ "finished_at": self.finished_at,
251
+ "success": self.success,
252
+ "summary": self.summary,
253
+ "commit_hash": self.commit_hash,
254
+ "error": self.error,
255
+ "report_path": self.report_path,
256
+ "patch_path": self.patch_path,
257
+ "log_path": self.log_path,
258
+ "log_pointer_path": self.log_pointer_path,
259
+ "warnings": self.warnings,
260
+ "changed_files": self.changed_files,
261
+ "verify_failed": self.verify_failed,
262
+ "verification_unavailable": self.verification_unavailable,
263
+ "verify_summary": self.verify_summary,
264
+ "verify_artifact_path": self.verify_artifact_path,
265
+ "knowledge_capture_artifact_dir": self.knowledge_capture_artifact_dir,
266
+ "task_attempt_entry_id": self.task_attempt_entry_id,
267
+ "task_attempt_knowledge_file_path": self.task_attempt_knowledge_file_path,
268
+ "verify_payload": self.verify_payload,
269
+ "verify_command_source": self.verify_command_source,
270
+ "failure_reason": self.failure_reason,
271
+ "failure_category": None
272
+ if self.success
273
+ else failure_category_value(self.failure_category),
274
+ "scope_violation_files": self.scope_violation_files,
275
+ "scope_diagnostics": self.scope_diagnostics,
276
+ "allowed_scope": self.allowed_scope,
277
+ "agent_exit_code": self.agent_exit_code,
278
+ "salvaged_nonzero_exit": self.salvaged_nonzero_exit,
279
+ "salvaged_agent_exception": self.salvaged_agent_exception,
280
+ "agent_exception_summary": clean_agent_exception_summary,
281
+ "result_kind": self.effective_result_kind,
282
+ "noop_success": self.noop_success,
283
+ "noop_reason": self.noop_reason,
284
+ "task_kind": self.task_kind,
285
+ "task_lifecycle_reason": self.task_lifecycle_reason,
286
+ "interrupted": self.interrupted,
287
+ }
288
+
289
+
290
+ @dataclass(frozen=True)
291
+ class ResolvedWorkerVerifyContract:
292
+ cfg: AppConfig
293
+ commands: tuple[str, ...]
294
+ selection: ResolvedVerifyCommands | None
295
+
296
+
297
+ def resolve_worker_verify_contract(
298
+ *,
299
+ cfg: AppConfig,
300
+ verify_mode: str,
301
+ verify_commands: list[str] | None,
302
+ verify_command_selection: ResolvedVerifyCommands | None,
303
+ task: dict[str, Any] | None,
304
+ root: Path,
305
+ repo_scan: RepoScanResult | None = None,
306
+ plan_requirements: list[str] | None = None,
307
+ ) -> ResolvedWorkerVerifyContract:
308
+ run_cfg = clone_cfg(cfg)
309
+ if verify_mode == "off":
310
+ return ResolvedWorkerVerifyContract(
311
+ cfg=run_cfg,
312
+ commands=(),
313
+ selection=None,
314
+ )
315
+
316
+ selection = resolve_authoritative_task_verify_command_selection(
317
+ cfg=cfg,
318
+ verify_cmd=verify_commands,
319
+ task=task,
320
+ root=root,
321
+ repo_scan=repo_scan,
322
+ plan_requirements=plan_requirements,
323
+ selection=verify_command_selection,
324
+ allow_empty_config=True,
325
+ )
326
+ run_cfg.verify_commands = list(selection.commands)
327
+ return ResolvedWorkerVerifyContract(
328
+ cfg=run_cfg,
329
+ commands=selection.commands,
330
+ selection=selection,
331
+ )
332
+
333
+
334
+ def reject_abnormal_success_result(result: TaskWorkerResult) -> TaskWorkerResult:
335
+ if not result.success:
336
+ return result
337
+ abnormal_exit = result.agent_exit_code not in (None, 0)
338
+ salvage_label = result.salvaged_nonzero_exit or result.salvaged_agent_exception
339
+ if not abnormal_exit and not salvage_label:
340
+ return result
341
+ reasons: list[str] = []
342
+ if abnormal_exit:
343
+ reasons.append(f"agent_exit_code={result.agent_exit_code}")
344
+ if result.salvaged_agent_exception:
345
+ reasons.append("salvaged_agent_exception=true")
346
+ if result.salvaged_nonzero_exit:
347
+ reasons.append("salvaged_nonzero_exit=true")
348
+ reason = ", ".join(reasons)
349
+ error = (
350
+ "worker result reported success after abnormal agent termination "
351
+ f"({reason}); refusing to accept partial worker result"
352
+ )
353
+ summary = f"Worker rejected abnormal success result. Error: {error}"
354
+ return replace(
355
+ result,
356
+ success=False,
357
+ summary=summary,
358
+ error=error,
359
+ failure_reason=result.failure_reason or "agent_abnormal_success",
360
+ failure_category=result.failure_category or FailureCategory.IMPLEMENTATION_FAILED,
361
+ salvaged_nonzero_exit=False,
362
+ salvaged_agent_exception=False,
363
+ result_kind=None,
364
+ noop_reason=None,
365
+ )
366
+
367
+
368
+ def _repo_rel(root: Path, path: Path) -> str:
369
+ return os.fspath(path.resolve().relative_to(root.resolve()))
370
+
371
+
372
+ def _normalize_rel_prefix(value: str) -> str:
373
+ cleaned = value.strip().replace("\\", "/")
374
+ while cleaned.startswith("./"):
375
+ cleaned = cleaned[2:]
376
+ return cleaned.rstrip("/")
377
+
378
+
379
+ def _matches_prefix(rel_path: str, prefix: str) -> bool:
380
+ return rel_path == prefix or rel_path.startswith(prefix + "/")
381
+
382
+
383
+ def _protected_path_violations(paths: list[str], prefixes: list[str]) -> list[str]:
384
+ normalized = [_normalize_rel_prefix(prefix) for prefix in prefixes if prefix.strip()]
385
+ violations: list[str] = []
386
+ for path in paths:
387
+ rel = _normalize_rel_prefix(path)
388
+ if any(_matches_prefix(rel, prefix) for prefix in normalized):
389
+ violations.append(path)
390
+ return violations
391
+
392
+
393
+ def _scope_violation_message(
394
+ *,
395
+ violations: list[str],
396
+ allowed_scope: list[str],
397
+ blocked: bool,
398
+ diagnostics: list[dict[str, Any]] | None = None,
399
+ ) -> str:
400
+ violations_preview = ", ".join(violations[:20])
401
+ if len(violations) > 20:
402
+ violations_preview += ", ..."
403
+ message = (
404
+ f"Out-of-scope file changes detected ({len(violations)}): {violations_preview}. "
405
+ f"Allowed scope: {allowed_scope or ['(none)']}."
406
+ )
407
+ if diagnostics:
408
+ classes = sorted(
409
+ {
410
+ str(item.get("classification") or "unknown")
411
+ for item in diagnostics
412
+ if not bool(item.get("allowed"))
413
+ }
414
+ )
415
+ if classes:
416
+ message += f" Scope classifications: {', '.join(classes)}."
417
+ if blocked:
418
+ message += " Task was blocked due to strict scope isolation."
419
+ return message
420
+
421
+
422
+ def _scope_diagnostics_payload(
423
+ diagnostics: list[ScopeViolationDiagnostic],
424
+ ) -> list[dict[str, Any]]:
425
+ return [item.to_payload() for item in diagnostics]
426
+
427
+
428
+ def _scope_recovery_warning(diagnostic: ScopeViolationDiagnostic) -> str:
429
+ return (
430
+ "Scope recovery: "
431
+ f"{diagnostic.classification} for {diagnostic.path} "
432
+ f"({diagnostic.reason_code}; action={diagnostic.recommended_action})."
433
+ )
434
+
435
+
436
+ def _scratch_artifact_section(scratch_dir: Path) -> str:
437
+ return "\n".join(
438
+ [
439
+ "## Scratch Artifacts",
440
+ "",
441
+ "Temporary diagnostic dumps, command-output captures, and ad hoc test logs must not be",
442
+ "written in the repository root. Use the managed scratch artifact directory below,",
443
+ f"or `{SCRATCH_ARTIFACT_ENV}` when the shell environment exposes it. Use `/tmp` only",
444
+ "for throwaway files that do not need to be retained.",
445
+ f"Managed scratch artifact directory: `{scratch_dir}`.",
446
+ "",
447
+ ]
448
+ )
449
+
450
+
451
+ def _merge_changed_files(*path_groups: list[str]) -> list[str]:
452
+ seen: set[str] = set()
453
+ merged: list[str] = []
454
+ for group in path_groups:
455
+ for raw in group:
456
+ value = str(raw).strip().replace("\\", "/")
457
+ while value.startswith("./"):
458
+ value = value[2:]
459
+ if not value or value in seen:
460
+ continue
461
+ seen.add(value)
462
+ merged.append(value)
463
+ return merged
464
+
465
+
466
+ def _task_is_diagnostic_analysis_only(task: dict[str, Any]) -> bool:
467
+ raw_flag = task.get("analysis_only")
468
+ if isinstance(raw_flag, bool):
469
+ return raw_flag
470
+ lifecycle = classify_task_lifecycle(
471
+ title=str(task.get("title") or "").strip(),
472
+ description=str(task.get("description") or "").strip(),
473
+ acceptance_criteria=[
474
+ str(item or "")
475
+ for item in (task.get("acceptance_criteria") or [])
476
+ if str(item or "").strip()
477
+ ],
478
+ estimated_files=[
479
+ str(item or "")
480
+ for item in (task.get("estimated_files") or [])
481
+ if str(item or "").strip()
482
+ ],
483
+ write_scope=[
484
+ str(item or "") for item in (task.get("write_scope") or []) if str(item or "").strip()
485
+ ],
486
+ )
487
+ if lifecycle.kind == TASK_KIND_ANALYSIS_ONLY:
488
+ return True
489
+ title = str(task.get("title") or "").strip()
490
+ if not title or not _DIAGNOSTIC_TASK_TITLE_RE.search(title):
491
+ return False
492
+ return _MUTATING_TASK_TEXT_RE.search(title) is None
493
+
494
+
495
+ def _path_has_cargo_manifest(root: Path, rel_dir: str) -> bool:
496
+ normalized = str(rel_dir or "").strip().replace("\\", "/").strip("/")
497
+ candidate = root if not normalized else root / normalized
498
+ try:
499
+ candidate.relative_to(root)
500
+ except ValueError:
501
+ return False
502
+ return (candidate / "Cargo.toml").is_file()
503
+
504
+
505
+ def _is_rust_build_metadata_path_for_repo(root: Path, path: str) -> bool:
506
+ normalized = str(path or "").strip().replace("\\", "/").strip("/")
507
+ if not normalized or normalized.startswith("../") or "/../" in normalized:
508
+ return False
509
+ parts = [part for part in normalized.split("/") if part]
510
+ if not parts:
511
+ return False
512
+ if parts[-1] == "Cargo.lock":
513
+ return _path_has_cargo_manifest(root, "/".join(parts[:-1]))
514
+ if "target" in parts:
515
+ target_index = parts.index("target")
516
+ return _path_has_cargo_manifest(root, "/".join(parts[:target_index]))
517
+ return False
518
+
519
+
520
+ def _diagnostic_side_effects_are_rust_build_metadata(root: Path, paths: list[str]) -> bool:
521
+ return bool(paths) and all(_is_rust_build_metadata_path_for_repo(root, path) for path in paths)
522
+
523
+
524
+ def _task_allows_conditional_noop(task: dict[str, Any]) -> bool:
525
+ acceptance = task.get("acceptance_criteria") or []
526
+ if not isinstance(acceptance, list):
527
+ acceptance = []
528
+ text = "\n".join(
529
+ [
530
+ str(task.get("title") or ""),
531
+ str(task.get("description") or ""),
532
+ *(str(item or "") for item in acceptance),
533
+ ]
534
+ )
535
+ return bool(_CONDITIONAL_NOOP_TASK_RE.search(text))
536
+
537
+
538
+ def _verification_failure_fingerprints(result: VerifyRunResult) -> set[str]:
539
+ fingerprints: set[str] = set()
540
+ for command_result in result.command_results:
541
+ if command_result.exit_code == 0:
542
+ continue
543
+ command = command_result.command or command_result.effective_command or "<unknown>"
544
+ output = command_result.output or "\n".join(
545
+ part for part in (command_result.stdout, command_result.stderr) if part
546
+ )
547
+ matched = False
548
+ for match in _VERIFY_FAILED_TEST_RE.finditer(output):
549
+ fingerprints.add(f"{command}::{match.group(1).strip()}")
550
+ matched = True
551
+ for match in _VERIFY_FAILURE_SECTION_RE.finditer(output):
552
+ label = " ".join(match.group(1).strip().split())
553
+ if label and label.lower() not in {"failures", "errors"}:
554
+ fingerprints.add(f"{command}::{label}")
555
+ matched = True
556
+ if matched:
557
+ continue
558
+ fallback_match = _VERIFY_ACTIONABLE_FAILURE_RE.search(output)
559
+ if fallback_match is not None:
560
+ line = " ".join(fallback_match.group(0).strip().split())
561
+ if line:
562
+ fingerprints.add(f"{command}::{line[:200]}")
563
+ continue
564
+ fingerprints.add(f"{command}::exit:{command_result.exit_code}")
565
+ return fingerprints
566
+
567
+
568
+ def _failure_fingerprints_by_command(fingerprints: set[str]) -> dict[str, set[str]]:
569
+ by_command: dict[str, set[str]] = {}
570
+ for fingerprint in fingerprints:
571
+ command, _, _detail = fingerprint.partition("::")
572
+ if not command:
573
+ continue
574
+ by_command.setdefault(command, set()).add(fingerprint)
575
+ return by_command
576
+
577
+
578
+ def _baseline_failure_tokens(text: str) -> set[str]:
579
+ tokens: set[str] = set()
580
+ for match in _BASELINE_FAILURE_TOKEN_RE.finditer(text):
581
+ token = match.group(0).lower().strip("_")
582
+ if not token or token in _BASELINE_FAILURE_STOPWORDS:
583
+ continue
584
+ tokens.add(token)
585
+ return tokens
586
+
587
+
588
+ def _task_focus_tokens(task: dict[str, Any] | None) -> set[str]:
589
+ if not isinstance(task, dict):
590
+ return set()
591
+ acceptance = task.get("acceptance_criteria") or []
592
+ if not isinstance(acceptance, list):
593
+ acceptance = []
594
+ text = "\n".join(
595
+ [
596
+ str(task.get("title") or ""),
597
+ str(task.get("description") or ""),
598
+ *(str(item or "") for item in acceptance),
599
+ ]
600
+ )
601
+ return _baseline_failure_tokens(text)
602
+
603
+
604
+ def _residual_failure_is_unrelated_to_task(fingerprint: str, task_tokens: set[str]) -> bool:
605
+ if not task_tokens:
606
+ return False
607
+ _command, _separator, detail = fingerprint.partition("::")
608
+ failure_tokens = _baseline_failure_tokens(detail or fingerprint)
609
+ if not failure_tokens:
610
+ return False
611
+ return failure_tokens.isdisjoint(task_tokens)
612
+
613
+
614
+ def _baseline_improved_failure_comparison(
615
+ *,
616
+ baseline: VerifyRunResult | None,
617
+ current: VerifyRunResult,
618
+ task: dict[str, Any] | None = None,
619
+ ) -> dict[str, Any] | None:
620
+ if baseline is None or baseline.all_passed or current.all_passed:
621
+ return None
622
+ if (
623
+ failure_category_value(current.failure_category_value)
624
+ == FailureCategory.INFRA_UNAVAILABLE.value
625
+ ):
626
+ return None
627
+
628
+ baseline_failures = _verification_failure_fingerprints(baseline)
629
+ current_failures = _verification_failure_fingerprints(current)
630
+ if not baseline_failures or not current_failures:
631
+ return None
632
+ if not current_failures <= baseline_failures:
633
+ return None
634
+ baseline_by_command = _failure_fingerprints_by_command(baseline_failures)
635
+ current_by_command = _failure_fingerprints_by_command(current_failures)
636
+ task_tokens = _task_focus_tokens(task)
637
+ unchanged_residual_failures: set[str] = set()
638
+ for command, command_failures in current_by_command.items():
639
+ baseline_command_failures = baseline_by_command.get(command, set())
640
+ if command_failures < baseline_command_failures:
641
+ continue
642
+ unchanged_residual_failures.update(command_failures)
643
+ task_related_unchanged = [
644
+ failure
645
+ for failure in unchanged_residual_failures
646
+ if not _residual_failure_is_unrelated_to_task(failure, task_tokens)
647
+ ]
648
+ if task_related_unchanged:
649
+ return None
650
+ resolved_failures = sorted(baseline_failures - current_failures)
651
+ if not resolved_failures and not unchanged_residual_failures:
652
+ return None
653
+ reason = (
654
+ "post-task verification failures are unchanged from baseline but unrelated to "
655
+ "the task focus"
656
+ if not resolved_failures
657
+ else "post-task verification failures are a subset of baseline failures"
658
+ )
659
+ return {
660
+ "accepted": True,
661
+ "reason": reason,
662
+ "baseline_failure_count": len(baseline_failures),
663
+ "current_failure_count": len(current_failures),
664
+ "resolved_failure_count": len(resolved_failures),
665
+ "current_failures": sorted(current_failures),
666
+ "resolved_failures": resolved_failures,
667
+ "unchanged_unrelated_failures": sorted(unchanged_residual_failures),
668
+ }
669
+
670
+
671
+ def _extract_git_archive_to_directory(
672
+ *,
673
+ repo: Path,
674
+ commit: str,
675
+ target_dir: Path,
676
+ ) -> bool:
677
+ try:
678
+ cp = subprocess.run(
679
+ ["git", "-C", os.fspath(repo), "archive", "--format=tar", commit],
680
+ check=False,
681
+ capture_output=True,
682
+ )
683
+ except OSError:
684
+ return False
685
+ if cp.returncode != 0:
686
+ return False
687
+
688
+ target_root = target_dir.resolve()
689
+ with tarfile.open(fileobj=io.BytesIO(cp.stdout), mode="r:") as archive:
690
+ members = archive.getmembers()
691
+ for member in members:
692
+ destination = (target_root / member.name).resolve()
693
+ if not destination.is_relative_to(target_root):
694
+ return False
695
+ archive.extractall(target_root, members=members)
696
+ return True
697
+
698
+
699
+ def _run_baseline_verification_snapshot(
700
+ *,
701
+ repo: Path,
702
+ commit: str | None,
703
+ commands: list[str],
704
+ artifact_path: Path,
705
+ cfg: AppConfig,
706
+ ) -> VerifyRunResult | None:
707
+ if not commit or not commands:
708
+ return None
709
+ try:
710
+ with tempfile.TemporaryDirectory(prefix="alysis-verify-baseline-") as temp_root:
711
+ baseline_root = Path(temp_root) / "repo"
712
+ baseline_root.mkdir(parents=True, exist_ok=True)
713
+ if not _extract_git_archive_to_directory(
714
+ repo=repo,
715
+ commit=commit,
716
+ target_dir=baseline_root,
717
+ ):
718
+ return None
719
+ return run_task_verification(
720
+ root=baseline_root,
721
+ commands=commands,
722
+ artifact_path=artifact_path,
723
+ cfg=cfg,
724
+ )
725
+ except (OSError, tarfile.TarError):
726
+ return None
727
+
728
+
729
+ def _append_patch_debug_section(patch_path: Path, *, title: str, patch_text: str) -> None:
730
+ existing = patch_path.read_text(encoding="utf-8") if patch_path.exists() else ""
731
+ parts: list[str] = []
732
+ if existing:
733
+ parts.append(existing if existing.endswith("\n") else existing + "\n")
734
+ parts.append(f"# {title}\n")
735
+ if patch_text:
736
+ parts.append(patch_text if patch_text.endswith("\n") else patch_text + "\n")
737
+ patch_path.write_text("\n".join(part.rstrip("\n") for part in parts if part), encoding="utf-8")
738
+
739
+
740
+ def _format_agent_exception_summary(exc: Exception) -> str:
741
+ name = exc.__class__.__name__
742
+ message = str(exc).strip()
743
+ return sanitize_error_summary(f"{name}: {message}" if message else name)
744
+
745
+
746
+ def _persist_agent_exception_traceback(artifact_dir: Path, exc: BaseException) -> None:
747
+ """Persist the full, secret-redacted agent traceback for post-mortem.
748
+
749
+ The worker result keeps only a short summary; the full traceback is what an
750
+ autonomous fix-loop actually needs to root-cause a Forge worker failure. The
751
+ text is run through the shared secret redactor before it touches disk. Best
752
+ effort: a failure to persist must never mask the agent error itself.
753
+ """
754
+ try:
755
+ text = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
756
+ artifact_dir.mkdir(parents=True, exist_ok=True)
757
+ (artifact_dir / "agent_exception_traceback.txt").write_text(
758
+ redact_sensitive_error_text(text),
759
+ encoding="utf-8",
760
+ )
761
+ except Exception: # noqa: BLE001 - diagnostics must not mask the real failure
762
+ return
763
+
764
+
765
+ def _empty_task_asset_mirror(workspace_path: Path, *, task_id: str = "") -> TaskAssetMirror:
766
+ workspace = workspace_path.resolve()
767
+ return TaskAssetMirror(
768
+ workspace_path=workspace,
769
+ manifest_path=workspace / ".alysis" / "task_assets" / "manifest.json",
770
+ primary=[],
771
+ may_need=[],
772
+ pinned=[],
773
+ task_id=task_id,
774
+ )
775
+
776
+
777
+ def _combined_task_image_paths(
778
+ *,
779
+ legacy_paths: list[str],
780
+ mirror: TaskAssetMirror,
781
+ allocation: TaskAssetAllocation | None,
782
+ cfg: AppConfig,
783
+ role_model: str,
784
+ model_registry: ModelRegistry,
785
+ usage_logger: AssetUsageLogger,
786
+ ) -> list[str] | None:
787
+ combined: list[str] = []
788
+ seen: set[str] = set()
789
+
790
+ def _append(path: str) -> None:
791
+ try:
792
+ normalized = os.fspath(Path(path).resolve())
793
+ except OSError:
794
+ return
795
+ if normalized in seen:
796
+ return
797
+ seen.add(normalized)
798
+ combined.append(normalized)
799
+
800
+ for path in legacy_paths:
801
+ _append(path)
802
+ if cfg.assets.worker.inline_images and model_registry.get(role_model).supports_vision:
803
+ decision_by_id = {
804
+ decision.asset_id: decision.mode
805
+ for decision in (allocation.decisions if allocation else [])
806
+ }
807
+ max_new = max(0, int(cfg.assets.worker.max_inline_images))
808
+ added_new = 0
809
+ for entry in mirror.primary:
810
+ if entry.kind != "image" or entry.status != "mirrored":
811
+ continue
812
+ if decision_by_id.get(entry.asset_id) not in {"full_inline", "focused_extract"}:
813
+ continue
814
+ if entry.raw_workspace_path is None or not entry.raw_workspace_path.exists():
815
+ continue
816
+ before = len(combined)
817
+ _append(os.fspath(entry.raw_workspace_path))
818
+ if len(combined) > before:
819
+ usage_logger.inline_injection(asset_id=entry.asset_id, kind=entry.kind)
820
+ added_new += 1
821
+ if added_new >= max_new:
822
+ break
823
+ max_total = max(0, int(cfg.assets.worker.max_inline_images))
824
+ if max_total > 0:
825
+ combined = combined[:max_total]
826
+ return combined or None
827
+
828
+
829
+ def run_task_worker(
830
+ *,
831
+ task: dict[str, Any],
832
+ plan: dict[str, Any],
833
+ worktree_repo_path: Path,
834
+ base_branch: str,
835
+ run_paths: RunPaths,
836
+ cfg: AppConfig,
837
+ mode: str,
838
+ yes: bool,
839
+ max_steps: int | None,
840
+ api_key_override: str | None,
841
+ no_log: bool = False,
842
+ console: Console | None = None,
843
+ scope_mode: str = "strict",
844
+ verify_mode: str = "warn",
845
+ verify_commands: list[str] | None = None,
846
+ verify_command_selection: ResolvedVerifyCommands | None = None,
847
+ trace_sink: SwarmTraceSink | None = None,
848
+ trace_level: str = "off",
849
+ cancellation_event: threading.Event | None = None,
850
+ approval_handler: Callable[..., Any] | None = None,
851
+ ) -> TaskWorkerResult:
852
+ ensure_execution_dirs(run_paths)
853
+ (run_paths.execution_dir / "worker_results").mkdir(parents=True, exist_ok=True)
854
+
855
+ task_id = str(task.get("id") or "").strip()
856
+ task_title = str(task.get("title") or "").strip()
857
+ task_branch = str(task.get("branch") or "").strip()
858
+ safe_task = safe_task_file_component(task_id)
859
+ plan_requirements = (
860
+ [str(item).strip() for item in (plan.get("requirements") or []) if str(item).strip()]
861
+ if isinstance(plan, dict)
862
+ else None
863
+ )
864
+ verify_contract = resolve_worker_verify_contract(
865
+ cfg=cfg,
866
+ verify_mode=verify_mode,
867
+ verify_commands=verify_commands,
868
+ verify_command_selection=verify_command_selection,
869
+ task=task,
870
+ root=worktree_repo_path,
871
+ plan_requirements=plan_requirements,
872
+ )
873
+ verify_resolution = verify_contract.selection
874
+ verify_cmds = list(verify_contract.commands)
875
+ verify_command_source = verify_resolution.source if verify_resolution is not None else None
876
+ run_cfg = verify_contract.cfg
877
+ run_cfg.model = resolve_model_for_role(
878
+ cfg=cfg,
879
+ role=ROLE_CODING,
880
+ plan=plan,
881
+ prefer_context="forge",
882
+ )
883
+ worker_trace_sink = trace_sink or NoopSwarmTraceSink()
884
+ worker_surface = SwarmWorkerTraceSurface(
885
+ run_id=run_paths.run_id,
886
+ task_id=task_id,
887
+ trace_sink=worker_trace_sink,
888
+ trace_level=trace_level,
889
+ )
890
+ if verify_mode != "off":
891
+ resolved_verify_source = verify_command_source or "unknown"
892
+ resolved_verify_commands = ", ".join(verify_cmds) if verify_cmds else "(none)"
893
+ worker_trace_sink.emit(
894
+ build_swarm_trace_event(
895
+ run_id=run_paths.run_id,
896
+ task_id=task_id,
897
+ phase="verify.lifecycle",
898
+ message=(
899
+ "Resolved authoritative verification commands "
900
+ f"from {resolved_verify_source}: {resolved_verify_commands}."
901
+ ),
902
+ verbosity="full",
903
+ )
904
+ )
905
+ recording_surface = RecordingSurface(worker_surface)
906
+ agent_surface: Any = recording_surface
907
+ if approval_handler is not None:
908
+ agent_surface = _HostManagedApprovalSurface(
909
+ recording_surface,
910
+ task_id=task_id,
911
+ handler=approval_handler,
912
+ )
913
+ worker_console = console or make_console(
914
+ file=io.StringIO(), force_terminal=False, no_color=True
915
+ )
916
+
917
+ started_at = now_iso()
918
+ patch_path = run_paths.execution_patches_dir / f"{safe_task}.diff"
919
+ verify_path = run_paths.execution_verify_dir / f"{safe_task}.txt"
920
+ baseline_verify_path = run_paths.execution_verify_dir / f"{safe_task}.baseline.txt"
921
+ allowed_scope = normalize_scope_patterns(task, root=worktree_repo_path)
922
+ task_lifecycle = classify_task_lifecycle(
923
+ title=task_title,
924
+ description=str(task.get("description") or "").strip(),
925
+ acceptance_criteria=[
926
+ str(item or "")
927
+ for item in (task.get("acceptance_criteria") or [])
928
+ if str(item or "").strip()
929
+ ],
930
+ estimated_files=[
931
+ str(item or "")
932
+ for item in (task.get("estimated_files") or [])
933
+ if str(item or "").strip()
934
+ ],
935
+ write_scope=[
936
+ str(item or "") for item in (task.get("write_scope") or []) if str(item or "").strip()
937
+ ],
938
+ explicit_analysis_only=True if task.get("analysis_only") is True else None,
939
+ )
940
+ diagnostic_analysis_only = _task_is_diagnostic_analysis_only(task)
941
+ conditional_noop_allowed = _task_allows_conditional_noop(task)
942
+ prompt_verification_enabled = (
943
+ verify_mode != "off" and bool(verify_cmds) and not diagnostic_analysis_only
944
+ )
945
+ mirror_plan_into_worktree(run_paths=run_paths, worktree_repo_path=worktree_repo_path)
946
+ prepared_knowledge = prepare_task_execution_knowledge(
947
+ run_paths=run_paths,
948
+ task=task,
949
+ selection_label="execution",
950
+ )
951
+ mirror_selected_knowledge_into_worktree(
952
+ materialized=prepared_knowledge,
953
+ run_paths=run_paths,
954
+ worktree_repo_path=worktree_repo_path,
955
+ )
956
+ scratch_artifact_dir = run_paths.execution_dir / "scratch" / safe_task
957
+ scratch_artifact_dir.mkdir(parents=True, exist_ok=True)
958
+ scratch_artifact_section = _scratch_artifact_section(scratch_artifact_dir)
959
+
960
+ def _cancel_requested() -> bool:
961
+ return cancellation_event is not None and cancellation_event.is_set()
962
+
963
+ cancellation_token = (
964
+ EventCancellationToken(cancellation_event) if cancellation_event is not None else None
965
+ )
966
+ write_guard: SwarmWriteScopeGuard | None = None
967
+ if scope_mode == "strict":
968
+ write_guard = SwarmWriteScopeGuard(
969
+ worktree_root=worktree_repo_path,
970
+ allowed_patterns=allowed_scope,
971
+ extra_allowed_roots=(scratch_artifact_dir,),
972
+ )
973
+ asset_setup_warnings: list[str] = []
974
+ asset_setup_error: str | None = None
975
+ asset_usage_logger = AssetUsageLogger(run_paths=run_paths, task_id=task_id)
976
+ asset_model_registry = ModelRegistry(cfg=run_cfg)
977
+ asset_surface = (
978
+ build_asset_surface(
979
+ cfg=run_cfg,
980
+ run_paths=run_paths,
981
+ model_registry=asset_model_registry,
982
+ )
983
+ if run_cfg.assets.enabled
984
+ else None
985
+ )
986
+ task_asset_mirror = _empty_task_asset_mirror(worktree_repo_path, task_id=task_id)
987
+ if asset_surface is not None:
988
+ try:
989
+ task_asset_mirror = mirror_task_assets(
990
+ task=task,
991
+ plan=plan,
992
+ surface=asset_surface,
993
+ workspace_path=worktree_repo_path,
994
+ )
995
+ except AssetError as exc:
996
+ if run_cfg.assets.worker.fail_on_mirror_error:
997
+ asset_setup_error = (
998
+ f"worker asset mirror failed: {sanitize_error_summary(str(exc))}"
999
+ )
1000
+ else:
1001
+ warning = (
1002
+ f"worker asset mirror skipped: {sanitize_optional_error_summary(str(exc))}"
1003
+ )
1004
+ asset_setup_warnings.append(warning)
1005
+ for entry in [
1006
+ *task_asset_mirror.primary,
1007
+ *task_asset_mirror.may_need,
1008
+ *task_asset_mirror.pinned,
1009
+ ]:
1010
+ asset_usage_logger.mirror(
1011
+ asset_id=entry.asset_id,
1012
+ kind=entry.kind,
1013
+ status=entry.status,
1014
+ )
1015
+ instruction_bundle = build_task_execution_instruction_bundle(
1016
+ plan=plan,
1017
+ task=task,
1018
+ root=worktree_repo_path,
1019
+ cfg=run_cfg,
1020
+ role_model=run_cfg.model,
1021
+ mode=mode,
1022
+ yes=yes,
1023
+ deny_write_prefixes=[".alysis"],
1024
+ allow_write_globs=allowed_scope if scope_mode == "strict" else None,
1025
+ non_interactive=True,
1026
+ verification_enabled=prompt_verification_enabled,
1027
+ authoritative_verification_commands=(verify_cmds if prompt_verification_enabled else None),
1028
+ api_key=api_key_override,
1029
+ subagents_enabled=False,
1030
+ leading_sections=[scratch_artifact_section, prepared_knowledge.prompt_section],
1031
+ )
1032
+ asset_allocation: TaskAssetAllocation | None = None
1033
+ relevant_assets_section = ""
1034
+ if asset_surface is not None and task_asset_mirror.primary and not _cancel_requested():
1035
+ asset_allocation = allocate_task_assets(
1036
+ task=task,
1037
+ plan=plan,
1038
+ mirror=task_asset_mirror,
1039
+ cfg=run_cfg,
1040
+ model_registry=asset_model_registry,
1041
+ instruction_token_budget=instruction_bundle.budget.final_instruction_budget,
1042
+ api_key=api_key_override,
1043
+ )
1044
+ for decision in asset_allocation.decisions:
1045
+ asset_usage_logger.allocation_decision(
1046
+ asset_id=decision.asset_id,
1047
+ mode=decision.mode,
1048
+ )
1049
+ relevant_assets_section = render_relevant_assets_section(
1050
+ mirror=task_asset_mirror,
1051
+ allocation=asset_allocation,
1052
+ cfg=run_cfg,
1053
+ surface=asset_surface,
1054
+ model_registry=asset_model_registry,
1055
+ api_key=api_key_override,
1056
+ )
1057
+ elif asset_surface is not None and (task_asset_mirror.may_need or task_asset_mirror.pinned):
1058
+ asset_allocation = TaskAssetAllocation(
1059
+ task_id=task_id,
1060
+ decisions=[],
1061
+ elapsed_ms=0,
1062
+ model=None,
1063
+ tokens_used={},
1064
+ fallback_used=False,
1065
+ fallback_reason=None,
1066
+ )
1067
+ relevant_assets_section = render_relevant_assets_section(
1068
+ mirror=task_asset_mirror,
1069
+ allocation=asset_allocation,
1070
+ cfg=run_cfg,
1071
+ surface=asset_surface,
1072
+ model_registry=asset_model_registry,
1073
+ api_key=api_key_override,
1074
+ )
1075
+ if relevant_assets_section:
1076
+ instruction_bundle = build_task_execution_instruction_bundle(
1077
+ plan=plan,
1078
+ task=task,
1079
+ root=worktree_repo_path,
1080
+ cfg=run_cfg,
1081
+ role_model=run_cfg.model,
1082
+ mode=mode,
1083
+ yes=yes,
1084
+ deny_write_prefixes=[".alysis"],
1085
+ allow_write_globs=allowed_scope if scope_mode == "strict" else None,
1086
+ non_interactive=True,
1087
+ verification_enabled=prompt_verification_enabled,
1088
+ authoritative_verification_commands=(
1089
+ verify_cmds if prompt_verification_enabled else None
1090
+ ),
1091
+ api_key=api_key_override,
1092
+ subagents_enabled=False,
1093
+ leading_sections=[scratch_artifact_section, prepared_knowledge.prompt_section],
1094
+ relevant_assets_section=relevant_assets_section,
1095
+ )
1096
+ instruction = instruction_bundle.instruction
1097
+ write_execution_context_artifact(
1098
+ run_paths=run_paths,
1099
+ task_id=task_id,
1100
+ context_text=instruction_bundle.artifact_text,
1101
+ )
1102
+ task_image_paths = _combined_task_image_paths(
1103
+ legacy_paths=list(instruction_bundle.image_paths),
1104
+ mirror=task_asset_mirror,
1105
+ allocation=asset_allocation,
1106
+ cfg=run_cfg,
1107
+ role_model=run_cfg.model,
1108
+ model_registry=asset_model_registry,
1109
+ usage_logger=asset_usage_logger,
1110
+ )
1111
+ task_asset_mcp_manager: Any | None = None
1112
+ task_attempts_raw = task.get("attempts")
1113
+ try:
1114
+ task_attempt_count = max(1, int(task_attempts_raw if task_attempts_raw is not None else 1))
1115
+ except (TypeError, ValueError):
1116
+ task_attempt_count = 1
1117
+ task_step_budget = resolve_managed_task_step_budget(
1118
+ cfg=run_cfg,
1119
+ plan=plan,
1120
+ task=task,
1121
+ kind="managed_task",
1122
+ mode=mode,
1123
+ verification_enabled=verify_mode != "off",
1124
+ max_steps_override=max_steps,
1125
+ attempt_count=task_attempt_count,
1126
+ image_count=len(task_image_paths or []),
1127
+ )
1128
+ budget_artifact_payload = instruction_bundle.to_budget_artifact_payload()
1129
+ budget_artifact_payload["step_budget"] = task_step_budget.to_payload()
1130
+ budget_artifact_path = write_execution_budget_artifact(
1131
+ run_paths=run_paths,
1132
+ task_id=task_id,
1133
+ payload=budget_artifact_payload,
1134
+ )
1135
+ protected_prefixes = [".alysis", ".alysis_images", "alysis-feedback"]
1136
+ head_before_run = head_commit(worktree_repo_path)
1137
+ before_runtime_snapshot = snapshot_runtime_tree(worktree_repo_path)
1138
+
1139
+ run_code = 1
1140
+ run_err: str | None = asset_setup_error
1141
+ agent_run_exception = False
1142
+ agent_exception_summary: str | None = None
1143
+ interrupted = False
1144
+ failure_category: FailureCategory | str | None = None
1145
+ worktree_git_marker = worktree_repo_path / ".git"
1146
+ if worktree_git_marker.exists():
1147
+ try:
1148
+ ensure_runtime_artifact_excludes(worktree_repo_path)
1149
+ except GitOpsError as e:
1150
+ run_err = f"worker setup failed: {e}"
1151
+ try:
1152
+ if run_err is None and _cancel_requested():
1153
+ interrupted = True
1154
+ run_code = 130
1155
+ run_err = "interrupted before the agent turn started"
1156
+ elif run_err is None:
1157
+ if asset_surface is not None:
1158
+ task_asset_mcp_manager = compose_worker_asset_mcp_manager(
1159
+ base_manager=create_mcp_manager(
1160
+ workspace_root=worktree_repo_path,
1161
+ runtime_kind=RuntimeKind.SWARM_WORKER,
1162
+ session_id=safe_task,
1163
+ ),
1164
+ asset_manager=build_worker_asset_mcp_manager(
1165
+ cfg=run_cfg,
1166
+ surface=asset_surface,
1167
+ model_registry=asset_model_registry,
1168
+ mirror=task_asset_mirror,
1169
+ usage_logger=asset_usage_logger,
1170
+ api_key=api_key_override,
1171
+ ),
1172
+ )
1173
+ run_code = run_agent(
1174
+ cfg=run_cfg,
1175
+ root=worktree_repo_path,
1176
+ instruction=instruction,
1177
+ mode=mode,
1178
+ runtime_kind=RuntimeKind.SWARM_WORKER,
1179
+ yes=yes,
1180
+ max_steps=task_step_budget.resolved_max_steps,
1181
+ no_log=no_log,
1182
+ api_key_override=api_key_override,
1183
+ console=worker_console,
1184
+ surface=agent_surface,
1185
+ image_paths=task_image_paths,
1186
+ deny_write_prefixes=[".alysis"],
1187
+ session_log_dir_override=run_paths.execution_sessions_dir,
1188
+ session_id_override=safe_task,
1189
+ allow_write_globs=allowed_scope if scope_mode == "strict" else None,
1190
+ non_interactive=True,
1191
+ usage_role=f"swarm_worker:{task_id}",
1192
+ enable_compaction=False,
1193
+ enable_tool_output_offload=True,
1194
+ enable_conversation_summarization=True,
1195
+ compaction_profile="execution",
1196
+ enable_chat_turn_step_budget=False,
1197
+ one_shot_execution=True,
1198
+ verification_enabled=prompt_verification_enabled,
1199
+ authoritative_verification_commands=(
1200
+ verify_cmds if prompt_verification_enabled else None
1201
+ ),
1202
+ subagents_enabled=False,
1203
+ enforce_explicit_subagent_requests=False,
1204
+ mcp_manager=task_asset_mcp_manager,
1205
+ cancellation_token=cancellation_token,
1206
+ tool_dispatch_guard=write_guard,
1207
+ )
1208
+ except CooperativeCancellationError as e:
1209
+ interrupted = True
1210
+ run_code = 130
1211
+ run_err = f"interrupted at cooperative checkpoint: {e.reason}"
1212
+ except Exception as e: # noqa: BLE001
1213
+ run_code = 1
1214
+ agent_run_exception = True
1215
+ agent_exception_summary = _format_agent_exception_summary(e)
1216
+ # Classify with the shared mapper so a worker that hits a permanent provider
1217
+ # rejection (auth / bad-request / unknown-model) is recorded as PROVIDER_ERROR
1218
+ # and joins the chat/run vocabulary, instead of silently defaulting to
1219
+ # IMPLEMENTATION_FAILED downstream.
1220
+ failure_category = classify_failure_category(e)
1221
+ _persist_agent_exception_traceback(scratch_artifact_dir, e)
1222
+ finally:
1223
+ if task_asset_mcp_manager is not None:
1224
+ task_asset_mcp_manager.close()
1225
+
1226
+ after_runtime_snapshot = snapshot_runtime_tree(worktree_repo_path)
1227
+ runtime_artifacts_changed = before_runtime_snapshot != after_runtime_snapshot
1228
+ if asset_allocation is not None:
1229
+ write_task_asset_allocation(
1230
+ run_paths=run_paths,
1231
+ allocation=asset_allocation,
1232
+ started_at=started_at,
1233
+ )
1234
+ asset_usage_logger.summary(
1235
+ primary_count=len(task_asset_mirror.primary),
1236
+ may_need_count=len(task_asset_mirror.may_need),
1237
+ pinned_count=len(task_asset_mirror.pinned),
1238
+ )
1239
+ head_after_run = head_commit(worktree_repo_path)
1240
+ scope_inspection_error: str | None = None
1241
+ try:
1242
+ reporting_diff = build_execution_reporting_diff_with_commit_range(
1243
+ worktree_repo_path,
1244
+ before_commit=head_before_run,
1245
+ after_commit=head_after_run,
1246
+ )
1247
+ except GitOpsError as e:
1248
+ reporting_diff = build_execution_reporting_diff_with_commit_range(
1249
+ worktree_repo_path,
1250
+ before_commit=None,
1251
+ after_commit=None,
1252
+ )
1253
+ scope_inspection_error = f"scope inspection failed: {e}"
1254
+ patch_path.write_text(reporting_diff.patch_text, encoding="utf-8")
1255
+ scratch_scope_diagnostics = relocate_known_scratch_artifacts(
1256
+ root=worktree_repo_path,
1257
+ artifact_dir=scratch_artifact_dir,
1258
+ )
1259
+ relocated_scratch_paths = {item.path for item in scratch_scope_diagnostics}
1260
+ changed_files = _merge_changed_files(
1261
+ list(reporting_diff.changed_files),
1262
+ list_changed_files_including_untracked(worktree_repo_path),
1263
+ )
1264
+ if relocated_scratch_paths:
1265
+ changed_files = [path for path in changed_files if path not in relocated_scratch_paths]
1266
+ agent_added_non_material_paths: list[str] = []
1267
+ if head_before_run and head_after_run and head_after_run != head_before_run:
1268
+ agent_added_non_material_paths = [
1269
+ path
1270
+ for path in added_files_since(
1271
+ worktree_repo_path,
1272
+ before_commit=head_before_run,
1273
+ after_commit=head_after_run,
1274
+ )
1275
+ if is_non_material_untracked_path(path)
1276
+ ]
1277
+ if agent_added_non_material_paths:
1278
+ changed_files = [
1279
+ path for path in changed_files if path not in agent_added_non_material_paths
1280
+ ]
1281
+ warnings: list[str] = list(asset_setup_warnings)
1282
+ warnings.extend(_scope_recovery_warning(item) for item in scratch_scope_diagnostics)
1283
+
1284
+ commit_hash: str | None = None
1285
+ verify_failed = False
1286
+ verification_unavailable = False
1287
+ verify_summary: str | None = None
1288
+ verify_payload: dict[str, Any] | None = None
1289
+ failure_reason: str | None = None
1290
+ scope_violation_files: list[str] = []
1291
+ scope_diagnostics: list[dict[str, Any]] = _scope_diagnostics_payload(scratch_scope_diagnostics)
1292
+ result_kind: str | None = None
1293
+ noop_reason: str | None = None
1294
+ material_changes_detected = bool(changed_files)
1295
+ diagnostic_tolerated_side_effects: list[str] = []
1296
+ if (
1297
+ diagnostic_analysis_only
1298
+ and material_changes_detected
1299
+ and _diagnostic_side_effects_are_rust_build_metadata(worktree_repo_path, changed_files)
1300
+ ):
1301
+ diagnostic_tolerated_side_effects = list(changed_files)
1302
+ changed_files = []
1303
+ material_changes_detected = False
1304
+ warnings.append(
1305
+ "Diagnostic analysis-only task produced generated Rust build metadata "
1306
+ f"({', '.join(diagnostic_tolerated_side_effects[:20])}); ignored for merge."
1307
+ )
1308
+ nonzero_agent_exit = run_code != 0 and run_err is None and not agent_run_exception
1309
+ strict_scope_blocked = False
1310
+ success = False
1311
+
1312
+ def _append_run_error(message: str) -> None:
1313
+ nonlocal run_err
1314
+ run_err = (run_err + "; " if run_err else "") + message
1315
+
1316
+ if interrupted:
1317
+ failure_reason = failure_reason or "interrupted"
1318
+ failure_category = failure_category or "interrupted"
1319
+ elif nonzero_agent_exit:
1320
+ failure_reason = failure_reason or "agent_nonzero_exit"
1321
+ failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
1322
+ _append_run_error(
1323
+ f"agent exited non-zero ({run_code}); refusing to accept partial worker result"
1324
+ )
1325
+ elif agent_run_exception:
1326
+ failure_reason = failure_reason or "agent_exception"
1327
+ failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
1328
+
1329
+ def _refresh_authoritative_verification_after_material_changes() -> None:
1330
+ nonlocal run_cfg
1331
+ nonlocal verify_cmds
1332
+ nonlocal verify_command_source
1333
+ nonlocal verify_resolution
1334
+
1335
+ if verify_mode == "off" or verify_cmds or diagnostic_analysis_only:
1336
+ return
1337
+ try:
1338
+ post_change_scan = scan_workspace(context=resolve_workspace_context(worktree_repo_path))
1339
+ except (WorkspaceContextError, OSError):
1340
+ return
1341
+ refreshed_contract = resolve_worker_verify_contract(
1342
+ cfg=cfg,
1343
+ verify_mode=verify_mode,
1344
+ verify_commands=None,
1345
+ verify_command_selection=None,
1346
+ task=task,
1347
+ root=worktree_repo_path,
1348
+ repo_scan=post_change_scan,
1349
+ plan_requirements=plan_requirements,
1350
+ )
1351
+ refreshed_cmds = list(refreshed_contract.commands)
1352
+ if not refreshed_cmds:
1353
+ return
1354
+ verify_resolution = refreshed_contract.selection
1355
+ verify_cmds = refreshed_cmds
1356
+ verify_command_source = verify_resolution.source if verify_resolution is not None else None
1357
+ run_cfg.verify_commands = list(refreshed_cmds)
1358
+ resolved_verify_source = verify_command_source or "unknown"
1359
+ worker_trace_sink.emit(
1360
+ build_swarm_trace_event(
1361
+ run_id=run_paths.run_id,
1362
+ task_id=task_id,
1363
+ phase="verify.lifecycle",
1364
+ message=(
1365
+ "Refreshed authoritative verification commands after material changes "
1366
+ f"from {resolved_verify_source}: {', '.join(refreshed_cmds)}."
1367
+ ),
1368
+ verbosity="full",
1369
+ )
1370
+ )
1371
+
1372
+ _refresh_authoritative_verification_after_material_changes()
1373
+
1374
+ def _run_authoritative_verification(
1375
+ *,
1376
+ fail_on_failed_verify: bool,
1377
+ fail_on_repo_mutation: bool,
1378
+ failed_verify_reason: str = "verification_failed",
1379
+ failed_verify_error_prefix: str = "verification failed",
1380
+ infra_unavailable_error_prefix: str = "verification infrastructure unavailable",
1381
+ mark_verify_failed: bool = True,
1382
+ allow_baseline_improved_failure: bool = False,
1383
+ ) -> bool:
1384
+ nonlocal success
1385
+ nonlocal commit_hash
1386
+ nonlocal verify_failed
1387
+ nonlocal verify_summary
1388
+ nonlocal verify_payload
1389
+ nonlocal failure_reason
1390
+ nonlocal failure_category
1391
+ nonlocal changed_files
1392
+ nonlocal scope_violation_files
1393
+ nonlocal scope_diagnostics
1394
+
1395
+ before_verify_snapshot = snapshot_workspace_tree(worktree_repo_path)
1396
+ verify_result = run_task_verification(
1397
+ root=worktree_repo_path,
1398
+ commands=verify_cmds,
1399
+ artifact_path=verify_path,
1400
+ cfg=run_cfg,
1401
+ )
1402
+ verify_summary = verify_result.summary
1403
+ verify_payload = verify_run_result_to_payload(
1404
+ root=run_paths.root,
1405
+ result=verify_result,
1406
+ )
1407
+ after_verify_snapshot = snapshot_workspace_tree(worktree_repo_path)
1408
+ verify_mutation_diff = build_workspace_snapshot_reporting_diff(
1409
+ worktree_repo_path,
1410
+ before_snapshot=before_verify_snapshot,
1411
+ after_snapshot=after_verify_snapshot,
1412
+ )
1413
+ verify_mutation_paths = list(verify_mutation_diff.changed_files)
1414
+ verify_extra_diagnostics = relocate_known_scratch_artifacts(
1415
+ root=worktree_repo_path,
1416
+ artifact_dir=scratch_artifact_dir,
1417
+ )
1418
+ if verify_extra_diagnostics:
1419
+ relocated_verify_scratch = {item.path for item in verify_extra_diagnostics}
1420
+ verify_mutation_paths = [
1421
+ path for path in verify_mutation_paths if path not in relocated_verify_scratch
1422
+ ]
1423
+ warnings.extend(_scope_recovery_warning(item) for item in verify_extra_diagnostics)
1424
+ scope_diagnostics.extend(_scope_diagnostics_payload(verify_extra_diagnostics))
1425
+ verification_ok = True
1426
+
1427
+ if verify_mutation_paths:
1428
+ verify_mutation_msg = "Verification commands modified repository state"
1429
+ if commit_hash is not None:
1430
+ verify_mutation_msg += (
1431
+ f" after the task commit: {', '.join(verify_mutation_paths[:20])}"
1432
+ )
1433
+ else:
1434
+ verify_mutation_msg += f": {', '.join(verify_mutation_paths[:20])}"
1435
+ if len(verify_mutation_paths) > 20:
1436
+ verify_mutation_msg += ", ..."
1437
+ verify_mutation_msg += "."
1438
+ if scope_mode in {"warn", "strict"}:
1439
+ scope_assessment = assess_scope_changes(
1440
+ verify_mutation_paths,
1441
+ allowed_scope,
1442
+ task=task,
1443
+ root=worktree_repo_path,
1444
+ )
1445
+ scope_diagnostics.extend(_scope_diagnostics_payload(scope_assessment.diagnostics))
1446
+ if not scope_assessment.ok:
1447
+ verify_scope_violations = scope_assessment.blocking_paths
1448
+ scope_violation_files = _merge_changed_files(
1449
+ scope_violation_files,
1450
+ verify_scope_violations,
1451
+ )
1452
+ scope_msg = _scope_violation_message(
1453
+ violations=verify_scope_violations,
1454
+ allowed_scope=allowed_scope,
1455
+ blocked=scope_mode == "strict" or fail_on_repo_mutation,
1456
+ diagnostics=_scope_diagnostics_payload(scope_assessment.diagnostics),
1457
+ )
1458
+ if commit_hash is not None:
1459
+ scope_msg += " Verification commands modified repository state after the task commit."
1460
+ else:
1461
+ scope_msg += " Verification commands modified repository state during already-satisfied verification."
1462
+ if scope_mode == "strict" or fail_on_repo_mutation:
1463
+ success = False
1464
+ verification_ok = False
1465
+ failure_reason = "scope_violation"
1466
+ failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
1467
+ commit_hash = None
1468
+ _append_run_error(scope_msg)
1469
+ changed_files = _merge_changed_files(changed_files, verify_mutation_paths)
1470
+ _append_patch_debug_section(
1471
+ patch_path,
1472
+ title="Post-verification workspace diff",
1473
+ patch_text=verify_mutation_diff.patch_text,
1474
+ )
1475
+ else:
1476
+ warnings.append(scope_msg)
1477
+ elif scope_mode == "strict" or fail_on_repo_mutation:
1478
+ success = False
1479
+ verification_ok = False
1480
+ failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
1481
+ commit_hash = None
1482
+ _append_run_error(verify_mutation_msg)
1483
+ changed_files = _merge_changed_files(changed_files, verify_mutation_paths)
1484
+ _append_patch_debug_section(
1485
+ patch_path,
1486
+ title="Post-verification workspace diff",
1487
+ patch_text=verify_mutation_diff.patch_text,
1488
+ )
1489
+ else:
1490
+ warnings.append(verify_mutation_msg)
1491
+ elif fail_on_repo_mutation:
1492
+ success = False
1493
+ verification_ok = False
1494
+ failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
1495
+ commit_hash = None
1496
+ _append_run_error(verify_mutation_msg)
1497
+ changed_files = _merge_changed_files(changed_files, verify_mutation_paths)
1498
+ _append_patch_debug_section(
1499
+ patch_path,
1500
+ title="Post-verification workspace diff",
1501
+ patch_text=verify_mutation_diff.patch_text,
1502
+ )
1503
+ else:
1504
+ warnings.append(verify_mutation_msg)
1505
+
1506
+ if not verify_result.all_passed:
1507
+ verify_failure_category = (
1508
+ verify_result.failure_category_value or FailureCategory.VERIFICATION_FAILED
1509
+ )
1510
+ verify_infra_unavailable = (
1511
+ failure_category_value(verify_failure_category)
1512
+ == FailureCategory.INFRA_UNAVAILABLE.value
1513
+ )
1514
+ worker_had_material_change = (
1515
+ material_changes_detected
1516
+ or commit_hash is not None
1517
+ or bool(head_before_run and head_after_run and head_after_run != head_before_run)
1518
+ )
1519
+ tolerate_warn_infra = (
1520
+ verify_mode == "warn" and verify_infra_unavailable and worker_had_material_change
1521
+ )
1522
+ baseline_comparison: dict[str, Any] | None = None
1523
+ if (
1524
+ allow_baseline_improved_failure
1525
+ and verify_mode == "warn"
1526
+ and fail_on_failed_verify
1527
+ and not verify_infra_unavailable
1528
+ ):
1529
+ baseline_result = _run_baseline_verification_snapshot(
1530
+ repo=worktree_repo_path,
1531
+ commit=head_before_run,
1532
+ commands=verify_cmds,
1533
+ artifact_path=baseline_verify_path,
1534
+ cfg=run_cfg,
1535
+ )
1536
+ baseline_comparison = _baseline_improved_failure_comparison(
1537
+ baseline=baseline_result,
1538
+ current=verify_result,
1539
+ task=task,
1540
+ )
1541
+ if baseline_comparison is not None:
1542
+ if verify_payload is not None:
1543
+ verify_payload["baseline_comparison"] = baseline_comparison
1544
+ verify_payload["baseline_artifact_path"] = str(
1545
+ baseline_verify_path.relative_to(run_paths.root)
1546
+ if baseline_verify_path.is_relative_to(run_paths.root)
1547
+ else baseline_verify_path
1548
+ )
1549
+ if baseline_comparison["resolved_failure_count"]:
1550
+ warnings.append(
1551
+ "Verification warning: post-task verification still fails, but it "
1552
+ "reduced pre-existing baseline failures "
1553
+ f"({baseline_comparison['baseline_failure_count']} -> "
1554
+ f"{baseline_comparison['current_failure_count']})."
1555
+ )
1556
+ else:
1557
+ warnings.append(
1558
+ "Verification warning: post-task verification still fails, but the "
1559
+ "remaining failures match the pre-task baseline and are unrelated to "
1560
+ "this task's focus."
1561
+ )
1562
+ return verification_ok
1563
+ if verify_mode == "strict":
1564
+ verify_failed = True
1565
+ failure_reason = failure_reason or (
1566
+ "verification_infra_unavailable"
1567
+ if verify_infra_unavailable
1568
+ else "verification_failed"
1569
+ )
1570
+ failure_category = failure_category or verify_failure_category
1571
+ success = False
1572
+ verification_ok = False
1573
+ if verify_infra_unavailable:
1574
+ _append_run_error(
1575
+ f"strict verification infrastructure unavailable: {verify_result.summary}"
1576
+ )
1577
+ else:
1578
+ _append_run_error(f"strict verification failed: {verify_result.summary}")
1579
+ elif fail_on_failed_verify and not tolerate_warn_infra:
1580
+ if mark_verify_failed:
1581
+ verify_failed = True
1582
+ failure_reason = failure_reason or (
1583
+ "verification_infra_unavailable"
1584
+ if verify_infra_unavailable
1585
+ else failed_verify_reason
1586
+ )
1587
+ failure_category = failure_category or verify_failure_category
1588
+ success = False
1589
+ verification_ok = False
1590
+ if verify_infra_unavailable:
1591
+ _append_run_error(f"{infra_unavailable_error_prefix}: {verify_result.summary}")
1592
+ else:
1593
+ _append_run_error(f"{failed_verify_error_prefix}: {verify_result.summary}")
1594
+ else:
1595
+ warning_prefix = (
1596
+ "Verification infrastructure warning"
1597
+ if verify_infra_unavailable
1598
+ else "Verification warning"
1599
+ )
1600
+ warnings.append(f"{warning_prefix}: {verify_result.summary}")
1601
+
1602
+ return verification_ok and verify_result.all_passed
1603
+
1604
+ if scope_inspection_error:
1605
+ if scope_mode == "strict":
1606
+ strict_scope_blocked = True
1607
+ failure_reason = "scope_violation"
1608
+ failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
1609
+ _append_run_error(scope_inspection_error)
1610
+ else:
1611
+ warnings.append(scope_inspection_error)
1612
+ if scope_mode in {"warn", "strict"}:
1613
+ scope_assessment = assess_scope_changes(
1614
+ changed_files,
1615
+ allowed_scope,
1616
+ task=task,
1617
+ root=worktree_repo_path,
1618
+ extra_diagnostics=scratch_scope_diagnostics,
1619
+ )
1620
+ changed_files = list(scope_assessment.effective_changed_files)
1621
+ scope_diagnostics = _scope_diagnostics_payload(scope_assessment.diagnostics)
1622
+ for diagnostic in scope_assessment.diagnostics:
1623
+ if diagnostic.allowed and diagnostic.classification != "in_scope":
1624
+ warning = _scope_recovery_warning(diagnostic)
1625
+ if warning not in warnings:
1626
+ warnings.append(warning)
1627
+ if not scope_assessment.ok:
1628
+ violations = scope_assessment.blocking_paths
1629
+ scope_violation_files = list(violations)
1630
+ scope_msg = _scope_violation_message(
1631
+ violations=violations,
1632
+ allowed_scope=allowed_scope,
1633
+ blocked=scope_mode == "strict",
1634
+ diagnostics=scope_diagnostics,
1635
+ )
1636
+ if scope_mode == "strict":
1637
+ strict_scope_blocked = True
1638
+ failure_reason = "scope_violation"
1639
+ failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
1640
+ _append_run_error(scope_msg)
1641
+ else:
1642
+ warnings.append(scope_msg)
1643
+
1644
+ guard_violations = write_guard.violations if write_guard is not None else []
1645
+ if guard_violations:
1646
+ strict_scope_blocked = True
1647
+ if not interrupted:
1648
+ failure_reason = "write_scope_guard_violation"
1649
+ failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
1650
+ scope_violation_files = _merge_changed_files(
1651
+ scope_violation_files,
1652
+ [item.resolved_relpath or item.raw_path for item in guard_violations],
1653
+ )
1654
+ scope_diagnostics.extend(item.to_payload() for item in guard_violations)
1655
+ violation_preview = "; ".join(
1656
+ f"{item.tool_name}: {item.raw_path} ({item.reason})" for item in guard_violations[:5]
1657
+ )
1658
+ if len(guard_violations) > 5:
1659
+ violation_preview += "; ..."
1660
+ _append_run_error(
1661
+ f"swarm write-scope guard blocked {len(guard_violations)} write(s) "
1662
+ f"outside the task write scope: {violation_preview}"
1663
+ )
1664
+ for item in guard_violations:
1665
+ worker_trace_sink.emit(
1666
+ build_swarm_trace_event(
1667
+ run_id=run_paths.run_id,
1668
+ task_id=task_id,
1669
+ phase="scope.violation",
1670
+ message=(
1671
+ f"Write blocked by swarm write-scope guard ({item.reason}): "
1672
+ f"{item.tool_name} -> {item.raw_path}"
1673
+ ),
1674
+ )
1675
+ )
1676
+
1677
+ can_attempt_commit_verify = (
1678
+ not runtime_artifacts_changed
1679
+ and material_changes_detected
1680
+ and not strict_scope_blocked
1681
+ and run_code == 0
1682
+ )
1683
+
1684
+ if can_attempt_commit_verify:
1685
+ try:
1686
+ agent_commit_hash: str | None = None
1687
+ if head_before_run and head_after_run and head_after_run != head_before_run:
1688
+ agent_commit_hash = head_after_run
1689
+ agent_commit_paths = changed_files_since(
1690
+ worktree_repo_path,
1691
+ before_commit=head_before_run,
1692
+ after_commit=head_after_run,
1693
+ )
1694
+ protected_violations = _protected_path_violations(
1695
+ agent_commit_paths,
1696
+ protected_prefixes,
1697
+ )
1698
+ if protected_violations:
1699
+ preview = ", ".join(protected_violations[:20])
1700
+ if len(protected_violations) > 20:
1701
+ preview += ", ..."
1702
+ raise GitOpsError(f"worker created commit touching protected paths: {preview}")
1703
+ agent_added_non_material_paths = [
1704
+ path
1705
+ for path in added_files_since(
1706
+ worktree_repo_path,
1707
+ before_commit=head_before_run,
1708
+ after_commit=head_after_run,
1709
+ )
1710
+ if is_non_material_untracked_path(path)
1711
+ ]
1712
+ if agent_added_non_material_paths:
1713
+ reset_mixed(worktree_repo_path, target=head_before_run)
1714
+ agent_commit_hash = None
1715
+ non_material_untracked_paths = list_untracked_packaging_metadata_paths(
1716
+ worktree_repo_path
1717
+ )
1718
+ stage_all(worktree_repo_path)
1719
+ unstage_staged_prefixes(worktree_repo_path, protected_prefixes)
1720
+ ensure_not_staged_prefixes(worktree_repo_path, protected_prefixes)
1721
+ if non_material_untracked_paths:
1722
+ unstage_staged_paths(worktree_repo_path, non_material_untracked_paths)
1723
+ ensure_not_staged_paths(worktree_repo_path, non_material_untracked_paths)
1724
+ staged_now = staged_files(worktree_repo_path)
1725
+ if staged_now and has_grounded_rust_target_runtime_artifacts(worktree_repo_path):
1726
+ unstage_staged_runtime_artifacts(
1727
+ worktree_repo_path,
1728
+ current_paths=staged_now,
1729
+ )
1730
+ staged_now = staged_files(worktree_repo_path)
1731
+ ensure_not_staged_runtime_artifacts(
1732
+ worktree_repo_path,
1733
+ current_paths=staged_now,
1734
+ )
1735
+ if staged_now:
1736
+ commit_hash = commit_all(
1737
+ worktree_repo_path,
1738
+ message=f"{task_id}: {task_title or 'task update'}",
1739
+ )
1740
+ elif agent_commit_hash is not None:
1741
+ commit_hash = agent_commit_hash
1742
+ else:
1743
+ raise GitOpsError("no repository changes were staged for commit")
1744
+ success = True
1745
+ patch_text = format_patch_stdout(worktree_repo_path, base_branch=base_branch)
1746
+ patch_path.write_text(
1747
+ patch_text if patch_text else "(empty format-patch output)\n",
1748
+ encoding="utf-8",
1749
+ )
1750
+ changed_files = changed_files_between(
1751
+ worktree_repo_path,
1752
+ revspec=f"{base_branch}..HEAD",
1753
+ )
1754
+ if verify_mode != "off" and verify_cmds:
1755
+ _run_authoritative_verification(
1756
+ fail_on_failed_verify=True,
1757
+ fail_on_repo_mutation=False,
1758
+ failed_verify_reason="verification_failed",
1759
+ failed_verify_error_prefix="verification failed",
1760
+ infra_unavailable_error_prefix="verification infrastructure unavailable",
1761
+ mark_verify_failed=True,
1762
+ allow_baseline_improved_failure=True,
1763
+ )
1764
+ elif verify_mode == "strict":
1765
+ # The commit exists and no authoritative command exists to check it --
1766
+ # a property of the workspace, not a defect in the work. Failing here
1767
+ # discarded finished tasks over missing tooling, so the worker now
1768
+ # reports an unverified completion: the commit and its worktree are
1769
+ # kept, and the orchestrator merges nothing.
1770
+ verification_unavailable = True
1771
+ verify_summary = (
1772
+ "verification skipped: no authoritative commands available; "
1773
+ "task kept as completed_unverified"
1774
+ )
1775
+ warnings.append(
1776
+ "Strict verification found no authoritative command for this task. "
1777
+ "The work was committed but nothing checked it -- review the task "
1778
+ "branch before merging, or provide an explicit verify command."
1779
+ )
1780
+ elif verify_mode != "off":
1781
+ verify_summary = "verification skipped: no authoritative commands available"
1782
+ else:
1783
+ verify_summary = "verification disabled (--verify off)"
1784
+ if success:
1785
+ result_kind = "success_commit"
1786
+ except GitOpsError as e:
1787
+ success = False
1788
+ failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
1789
+ _append_run_error(f"worker commit/patch failed: {e}")
1790
+ elif (
1791
+ not runtime_artifacts_changed
1792
+ and not material_changes_detected
1793
+ and not strict_scope_blocked
1794
+ and run_err is None
1795
+ and scope_inspection_error is None
1796
+ and run_code == 0
1797
+ ):
1798
+ if (
1799
+ diagnostic_analysis_only
1800
+ and not agent_run_exception
1801
+ and failure_category != FailureCategory.PROVIDER_THROTTLED
1802
+ ):
1803
+ success = True
1804
+ result_kind = "success_noop"
1805
+ noop_reason = "diagnostic_analysis_only"
1806
+ verify_summary = "verification skipped: diagnostic analysis-only task made no changes"
1807
+ if run_code != 0:
1808
+ warnings.append(
1809
+ "Diagnostic analysis-only task exited non-zero after making no repository "
1810
+ "changes; accepted as a non-blocking diagnostic no-op."
1811
+ )
1812
+ elif (
1813
+ conditional_noop_allowed
1814
+ and not agent_run_exception
1815
+ and failure_category != FailureCategory.PROVIDER_THROTTLED
1816
+ ):
1817
+ success = True
1818
+ result_kind = "success_noop"
1819
+ noop_reason = "conditional_noop"
1820
+ verify_summary = "verification skipped: conditional task made no changes"
1821
+ if run_code != 0:
1822
+ warnings.append(
1823
+ "Conditional task exited non-zero after making no repository changes; "
1824
+ "accepted because the task text allowed no work when not applicable."
1825
+ )
1826
+ elif verify_mode == "off":
1827
+ success = True
1828
+ result_kind = "success_noop"
1829
+ noop_reason = "already_satisfied"
1830
+ verify_summary = "verification skipped: no changes needed"
1831
+ elif not verify_cmds:
1832
+ success = True
1833
+ result_kind = "success_noop"
1834
+ noop_reason = "already_satisfied"
1835
+ verify_summary = "verification skipped: no changes needed"
1836
+ else:
1837
+ success = True
1838
+ if _run_authoritative_verification(
1839
+ fail_on_failed_verify=True,
1840
+ fail_on_repo_mutation=True,
1841
+ failed_verify_reason="noop_verification_failed",
1842
+ failed_verify_error_prefix="already-satisfied verification failed",
1843
+ infra_unavailable_error_prefix=(
1844
+ "already-satisfied verification infrastructure unavailable"
1845
+ ),
1846
+ mark_verify_failed=False,
1847
+ ):
1848
+ result_kind = "success_noop"
1849
+ noop_reason = "already_satisfied"
1850
+ else:
1851
+ success = False
1852
+
1853
+ salvaged_nonzero_exit = False
1854
+ salvaged_agent_exception = False
1855
+ if not success and agent_exception_summary:
1856
+ agent_exception_error = f"agent raised: {agent_exception_summary}"
1857
+ run_err = f"{agent_exception_error}; {run_err}" if run_err else agent_exception_error
1858
+ if not success and failure_category is None:
1859
+ # TODO(failure-category-audit): default=implementation_failed,
1860
+ # site=swarm_worker generic failure result, see failure_category.py
1861
+ failure_category = FailureCategory.IMPLEMENTATION_FAILED
1862
+
1863
+ if interrupted:
1864
+ summary = (
1865
+ "Worker interrupted by cooperative cancellation at a checkpoint; "
1866
+ "the task worktree is preserved for harvest."
1867
+ )
1868
+ elif runtime_artifacts_changed:
1869
+ summary = "Worker failed: attempted protected .alysis modifications."
1870
+ elif failure_reason == "write_scope_guard_violation":
1871
+ summary = "Worker blocked by the swarm write-scope guard."
1872
+ elif failure_reason == "scope_violation":
1873
+ summary = "Worker blocked due to strict scope isolation."
1874
+ elif success and result_kind == "success_noop" and noop_reason == "diagnostic_analysis_only":
1875
+ summary = "Worker completed diagnostic analysis with no repository changes."
1876
+ elif success and result_kind == "success_noop" and noop_reason == "conditional_noop":
1877
+ summary = "Worker completed conditional task with no repository changes."
1878
+ elif success and result_kind == "success_noop":
1879
+ summary = (
1880
+ "Worker completed successfully with no repository changes; task was already satisfied."
1881
+ )
1882
+ elif success and verification_unavailable:
1883
+ summary = (
1884
+ "Worker completed and produced a task commit, but nothing verified it: "
1885
+ "no authoritative verification command exists for this workspace."
1886
+ )
1887
+ elif success:
1888
+ summary = "Worker completed successfully and produced a task commit."
1889
+ else:
1890
+ summary = "Worker failed."
1891
+ if run_err:
1892
+ summary += f" Error: {run_err}"
1893
+ if warnings:
1894
+ summary += " Warnings: " + " | ".join(warnings)
1895
+
1896
+ finished_at = now_iso()
1897
+ exec_artifacts = write_exec_log_artifacts(
1898
+ paths=run_paths,
1899
+ task_id=task_id,
1900
+ cfg=run_cfg,
1901
+ no_log=no_log,
1902
+ before_logs=set(),
1903
+ sessions_dir=run_paths.execution_sessions_dir,
1904
+ expected_session_id=safe_task,
1905
+ )
1906
+ report_path = write_task_report(
1907
+ paths=run_paths,
1908
+ task=task,
1909
+ result="success" if success else "failure",
1910
+ result_kind=result_kind or ("success_commit" if success else "failure"),
1911
+ summary=summary,
1912
+ started_at=started_at,
1913
+ finished_at=finished_at,
1914
+ changed_files=changed_files,
1915
+ verify_commands=verify_cmds,
1916
+ patch_path=patch_path,
1917
+ budget_artifact_path=budget_artifact_path,
1918
+ execution_log_artifacts=exec_artifacts,
1919
+ verify_artifact_path=verify_path if verify_path.exists() else None,
1920
+ verify_summary=verify_summary,
1921
+ verify_payload=verify_payload,
1922
+ verify_command_source=verify_command_source,
1923
+ task_kind=task_lifecycle.kind,
1924
+ task_lifecycle_reason=task_lifecycle.reason_code,
1925
+ base_branch=base_branch,
1926
+ task_branch=task_branch,
1927
+ commit_hash=commit_hash,
1928
+ merge_commit_hash=None,
1929
+ merge_result=(
1930
+ "no merge required (analysis-only no-op)"
1931
+ if result_kind == "success_noop" and noop_reason == "diagnostic_analysis_only"
1932
+ else (
1933
+ "no merge required (conditional no-op)"
1934
+ if result_kind == "success_noop" and noop_reason == "conditional_noop"
1935
+ else (
1936
+ "no merge required (already-satisfied no-op)"
1937
+ if result_kind == "success_noop"
1938
+ else "not merged"
1939
+ )
1940
+ )
1941
+ ),
1942
+ salvaged_nonzero_exit=salvaged_nonzero_exit,
1943
+ salvaged_agent_exception=salvaged_agent_exception,
1944
+ agent_exception_summary=agent_exception_summary,
1945
+ noop_reason=noop_reason,
1946
+ )
1947
+ persisted_capture = persist_execution_knowledge_capture(
1948
+ paths=run_paths,
1949
+ task=task,
1950
+ source="swarm_worker",
1951
+ assistant_message=recording_surface.final_assistant_message,
1952
+ artifact_dir=(
1953
+ run_paths.execution_knowledge_capture_dir
1954
+ / safe_task
1955
+ / safe_task_file_component(started_at)
1956
+ ),
1957
+ report_path=report_path,
1958
+ patch_path=patch_path,
1959
+ verify_artifact_path=verify_path if verify_path.exists() else None,
1960
+ budget_artifact_path=budget_artifact_path,
1961
+ session_artifact_dir=exec_artifacts.session_artifact_dir,
1962
+ )
1963
+ if not success:
1964
+ mark_knowledge_capture_promotion_skipped(
1965
+ artifact_dir=persisted_capture.artifact_dir,
1966
+ reason="worker execution outcome was not accepted",
1967
+ )
1968
+
1969
+ task_attempt_entry = write_task_attempt_entry(
1970
+ paths=run_paths,
1971
+ task=task,
1972
+ source="swarm_worker",
1973
+ result="success" if success else "failure",
1974
+ summary=summary,
1975
+ changed_files=changed_files,
1976
+ verify_summary=verify_summary,
1977
+ report_path=report_path,
1978
+ patch_path=patch_path,
1979
+ verify_artifact_path=verify_path if verify_path.exists() else None,
1980
+ budget_artifact_path=budget_artifact_path,
1981
+ session_artifact_dir=exec_artifacts.session_artifact_dir,
1982
+ acceptance_state="pending" if success else "rejected",
1983
+ extra_tags=(
1984
+ ["execution", "worker", result_kind] if result_kind else ["execution", "worker"]
1985
+ ),
1986
+ )
1987
+
1988
+ result = TaskWorkerResult(
1989
+ task_id=task_id,
1990
+ title=task_title,
1991
+ branch=task_branch,
1992
+ worktree_path=os.fspath(worktree_repo_path),
1993
+ started_at=started_at,
1994
+ finished_at=finished_at,
1995
+ success=success,
1996
+ summary=summary,
1997
+ commit_hash=commit_hash,
1998
+ error=run_err,
1999
+ report_path=_repo_rel(run_paths.root, report_path),
2000
+ patch_path=_repo_rel(run_paths.root, patch_path),
2001
+ log_path=_repo_rel(run_paths.root, exec_artifacts.log_copy_path),
2002
+ log_pointer_path=_repo_rel(run_paths.root, exec_artifacts.pointer_path),
2003
+ warnings=warnings,
2004
+ changed_files=changed_files,
2005
+ verify_failed=verify_failed,
2006
+ verification_unavailable=verification_unavailable,
2007
+ verify_summary=verify_summary,
2008
+ verify_artifact_path=(
2009
+ _repo_rel(run_paths.root, verify_path) if verify_path.exists() else None
2010
+ ),
2011
+ knowledge_capture_artifact_dir=_repo_rel(run_paths.root, persisted_capture.artifact_dir),
2012
+ task_attempt_entry_id=task_attempt_entry.id,
2013
+ task_attempt_knowledge_file_path=(
2014
+ _repo_rel(run_paths.root, task_attempt_entry.file_path)
2015
+ if task_attempt_entry.file_path is not None
2016
+ else None
2017
+ ),
2018
+ verify_payload=verify_payload,
2019
+ verify_command_source=verify_command_source,
2020
+ failure_reason=failure_reason,
2021
+ failure_category=failure_category,
2022
+ scope_violation_files=(list(scope_violation_files) if scope_violation_files else None),
2023
+ scope_diagnostics=(list(scope_diagnostics) if scope_diagnostics else None),
2024
+ allowed_scope=list(allowed_scope),
2025
+ agent_exit_code=run_code,
2026
+ salvaged_nonzero_exit=salvaged_nonzero_exit,
2027
+ salvaged_agent_exception=salvaged_agent_exception,
2028
+ agent_exception_summary=agent_exception_summary,
2029
+ result_kind=result_kind,
2030
+ noop_reason=noop_reason,
2031
+ task_kind=task_lifecycle.kind,
2032
+ task_lifecycle_reason=task_lifecycle.reason_code,
2033
+ interrupted=interrupted,
2034
+ )
2035
+ issue_paths = changed_files or list(allowed_scope)
2036
+ if runtime_artifacts_changed:
2037
+ write_issue_entry(
2038
+ paths=run_paths,
2039
+ task=task,
2040
+ source="swarm_worker",
2041
+ title=f"{task_id}: protected .alysis mutation attempt",
2042
+ summary="Worker execution attempted to modify protected .alysis runtime state.",
2043
+ paths_in_scope=issue_paths,
2044
+ report_path=report_path,
2045
+ patch_path=patch_path,
2046
+ verify_artifact_path=verify_path if verify_path.exists() else None,
2047
+ budget_artifact_path=budget_artifact_path,
2048
+ session_artifact_dir=exec_artifacts.session_artifact_dir,
2049
+ tags=["protected_runtime_mutation"],
2050
+ )
2051
+ elif failure_reason == "noop_verification_failed":
2052
+ write_issue_entry(
2053
+ paths=run_paths,
2054
+ task=task,
2055
+ source="swarm_worker",
2056
+ title=f"{task_id}: already-satisfied verification failed",
2057
+ summary=verify_summary or "Authoritative verification failed for zero-diff success.",
2058
+ paths_in_scope=issue_paths,
2059
+ report_path=report_path,
2060
+ patch_path=patch_path,
2061
+ verify_artifact_path=verify_path if verify_path.exists() else None,
2062
+ budget_artifact_path=budget_artifact_path,
2063
+ session_artifact_dir=exec_artifacts.session_artifact_dir,
2064
+ tags=["verification_failure", "noop_verification_failure"],
2065
+ )
2066
+ elif failure_reason == "verification_infra_unavailable":
2067
+ write_issue_entry(
2068
+ paths=run_paths,
2069
+ task=task,
2070
+ source="swarm_worker",
2071
+ title=f"{task_id}: verification infrastructure unavailable",
2072
+ summary=verify_summary or "Worker verification could not run due to infrastructure.",
2073
+ paths_in_scope=issue_paths,
2074
+ report_path=report_path,
2075
+ patch_path=patch_path,
2076
+ verify_artifact_path=verify_path if verify_path.exists() else None,
2077
+ budget_artifact_path=budget_artifact_path,
2078
+ session_artifact_dir=exec_artifacts.session_artifact_dir,
2079
+ tags=["verification_failure", "infra_unavailable"],
2080
+ )
2081
+ elif verify_failed:
2082
+ write_issue_entry(
2083
+ paths=run_paths,
2084
+ task=task,
2085
+ source="swarm_worker",
2086
+ title=f"{task_id}: strict verification failed",
2087
+ summary=verify_summary or "Worker verification failed.",
2088
+ paths_in_scope=issue_paths,
2089
+ report_path=report_path,
2090
+ patch_path=patch_path,
2091
+ verify_artifact_path=verify_path if verify_path.exists() else None,
2092
+ budget_artifact_path=budget_artifact_path,
2093
+ session_artifact_dir=exec_artifacts.session_artifact_dir,
2094
+ tags=["verification_failure"],
2095
+ )
2096
+ elif not success:
2097
+ write_issue_entry(
2098
+ paths=run_paths,
2099
+ task=task,
2100
+ source="swarm_worker",
2101
+ title=f"{task_id}: worker execution failed",
2102
+ summary=summary,
2103
+ paths_in_scope=issue_paths,
2104
+ report_path=report_path,
2105
+ patch_path=patch_path,
2106
+ verify_artifact_path=verify_path if verify_path.exists() else None,
2107
+ budget_artifact_path=budget_artifact_path,
2108
+ session_artifact_dir=exec_artifacts.session_artifact_dir,
2109
+ tags=(
2110
+ ["execution_failure", "scope_violation"]
2111
+ if failure_reason == "scope_violation"
2112
+ else ["execution_failure"]
2113
+ ),
2114
+ )
2115
+ # Workers finish concurrently. Serializing the shared workspace index
2116
+ # avoids Windows replace/share violations while preserving atomic writes.
2117
+ with _KNOWLEDGE_INDEX_REBUILD_LOCK:
2118
+ rebuild_knowledge_index(run_paths)
2119
+ return result