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,273 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ _PYTEST_FAILED_RE = re.compile(
8
+ r"\bFAILED\s+(?P<nodeid>(?P<path>(?:[A-Za-z]:)?[^\s:]+\.py)(?:::[^\s]+)+)(?:\s+-\s+(?P<msg>.+))?"
9
+ )
10
+ _PYTHON_FILE_RE = re.compile(
11
+ r'File "(?P<path>[^"]+\.py)", line (?P<line>\d+), in (?P<symbol>[^\s]+)'
12
+ )
13
+ _PYTEST_SHORT_FILE_RE = re.compile(
14
+ r"(?P<path>[A-Za-z0-9_./\\-]+\.py):(?P<line>\d+):(?:\s+)?(?P<msg>.+)"
15
+ )
16
+ _JS_FAIL_RE = re.compile(r"\b(?:FAIL|Failed)\s+(?P<path>[^\s]+(?:\.[cm]?[jt]sx?))")
17
+ _JS_FRAME_RE = re.compile(
18
+ r"(?:at\s+(?P<symbol>[^\s(]+)\s+\()?(?P<path>[A-Za-z0-9_./\\-]+(?:\.[cm]?[jt]sx?)):(?P<line>\d+):(?P<column>\d+)\)?"
19
+ )
20
+ _GO_FAIL_RE = re.compile(r"---\s+FAIL:\s+(?P<name>[A-Za-z0-9_/.-]+)")
21
+ _GO_FILE_RE = re.compile(r"(?P<path>[A-Za-z0-9_./\\-]+\.go):(?P<line>\d+):\s*(?P<msg>.+)")
22
+ _RUST_FAIL_RE = re.compile(r"----\s+(?P<name>[^-\n]+?)\s+stdout\s+----")
23
+ _RUST_PANIC_RE = re.compile(
24
+ r"panicked at (?P<path>[A-Za-z0-9_./\\-]+\.rs):(?P<line>\d+):(?P<column>\d+)"
25
+ )
26
+ _JAVA_FRAME_RE = re.compile(
27
+ r"at\s+(?P<symbol>[A-Za-z0-9_.$<>]+)\((?P<path>[A-Za-z0-9_.$/-]+\.java):(?P<line>\d+)\)"
28
+ )
29
+ _PATH_LINE_RE = re.compile(
30
+ r"(?P<path>[A-Za-z0-9_./\\-]+\.(?:py|js|jsx|ts|tsx|go|rs|java)):(?P<line>\d+)"
31
+ )
32
+ _EXCEPTION_SUMMARY_RE = re.compile(
33
+ r"^(?:[A-Za-z_][\w.]*\.)?[A-Za-z_]\w*(?:Error|Exception|Warning|Interrupt|Exit):(?:\s+.+)?$"
34
+ )
35
+
36
+ _ERROR_MARKERS = (
37
+ "AssertionError",
38
+ "ModuleNotFoundError",
39
+ "ImportError",
40
+ "TypeError",
41
+ "ValueError",
42
+ "RuntimeError",
43
+ "SyntaxError",
44
+ "Error:",
45
+ "Exception:",
46
+ "panic",
47
+ "panicked at",
48
+ "FAILED ",
49
+ "--- FAIL:",
50
+ )
51
+ _SKIP_PATH_PARTS = {
52
+ ".git",
53
+ ".venv",
54
+ "venv",
55
+ "site-packages",
56
+ "node_modules",
57
+ "__pycache__",
58
+ "dist-packages",
59
+ }
60
+
61
+
62
+ def summarize_verification_failure(
63
+ *,
64
+ root: Path,
65
+ command: str,
66
+ effective_command: str,
67
+ output: str,
68
+ output_truncated: bool = False,
69
+ ) -> dict[str, Any] | None:
70
+ text = str(output or "")
71
+ if not text.strip():
72
+ return None
73
+
74
+ framework = _infer_framework(command=effective_command or command, output=text)
75
+ failing_tests = _extract_failing_tests(root=root, output=text, framework=framework)
76
+ stack_frames = _extract_stack_frames(root=root, output=text)
77
+ primary_error = _primary_error_line(text)
78
+ likely_files = _dedupe(
79
+ [
80
+ *[item.get("path", "") for item in failing_tests],
81
+ *[item.get("path", "") for item in stack_frames],
82
+ ]
83
+ )[:10]
84
+
85
+ if not primary_error and not failing_tests and not stack_frames:
86
+ return None
87
+
88
+ confidence = 0.45
89
+ if failing_tests:
90
+ confidence = 0.8
91
+ elif stack_frames:
92
+ confidence = 0.65
93
+ elif primary_error:
94
+ confidence = 0.55
95
+
96
+ return {
97
+ "framework": framework,
98
+ "primary_error": primary_error,
99
+ "failing_tests": failing_tests[:8],
100
+ "stack_frames": stack_frames[:12],
101
+ "likely_next_files": likely_files,
102
+ "output_truncated": bool(output_truncated),
103
+ "heuristic": True,
104
+ "confidence": confidence,
105
+ }
106
+
107
+
108
+ def _infer_framework(*, command: str, output: str) -> str:
109
+ combined = f"{command}\n{output}".casefold()
110
+ if "pytest" in combined or "::test" in combined:
111
+ return "pytest"
112
+ if "vitest" in combined:
113
+ return "vitest"
114
+ if "jest" in combined or re.search(r"\bFAIL\s+.*\.(?:js|jsx|ts|tsx)\b", output):
115
+ return "jest"
116
+ if "go test" in combined or "--- fail:" in combined:
117
+ return "go test"
118
+ if "cargo test" in combined or "panicked at" in combined:
119
+ return "cargo test"
120
+ if "mvn test" in combined or "gradle" in combined or ".java:" in combined:
121
+ return "junit"
122
+ if "traceback (most recent call last)" in combined:
123
+ return "python"
124
+ return "unknown"
125
+
126
+
127
+ def _extract_failing_tests(*, root: Path, output: str, framework: str) -> list[dict[str, Any]]:
128
+ tests: list[dict[str, Any]] = []
129
+ for match in _PYTEST_FAILED_RE.finditer(output):
130
+ path = _normalize_path(root=root, raw_path=match.group("path"))
131
+ if not path:
132
+ continue
133
+ nodeid = _normalize_pytest_nodeid(normalized_path=path, raw_nodeid=match.group("nodeid"))
134
+ item: dict[str, Any] = {
135
+ "id": nodeid,
136
+ "path": path,
137
+ }
138
+ msg = (match.group("msg") or "").strip()
139
+ if msg:
140
+ item["message"] = _clip(msg, 240)
141
+ tests.append(item)
142
+
143
+ if framework in {"go test", "unknown"}:
144
+ current_name = ""
145
+ for line in output.splitlines():
146
+ name_match = _GO_FAIL_RE.search(line)
147
+ if name_match:
148
+ current_name = name_match.group("name")
149
+ continue
150
+ file_match = _GO_FILE_RE.search(line)
151
+ if file_match and current_name:
152
+ path = _normalize_path(root=root, raw_path=file_match.group("path"))
153
+ if not path:
154
+ continue
155
+ tests.append(
156
+ {
157
+ "id": current_name,
158
+ "path": path,
159
+ "line": int(file_match.group("line")),
160
+ "message": _clip(file_match.group("msg").strip(), 240),
161
+ }
162
+ )
163
+
164
+ if framework in {"cargo test", "unknown"}:
165
+ for match in _RUST_FAIL_RE.finditer(output):
166
+ name = " ".join(match.group("name").split())
167
+ if name:
168
+ tests.append({"id": name})
169
+
170
+ return _dedupe_dicts(tests)
171
+
172
+
173
+ def _extract_stack_frames(*, root: Path, output: str) -> list[dict[str, Any]]:
174
+ frames: list[dict[str, Any]] = []
175
+ for regex in (_PYTHON_FILE_RE, _JS_FRAME_RE, _GO_FILE_RE, _RUST_PANIC_RE, _JAVA_FRAME_RE):
176
+ for match in regex.finditer(output):
177
+ path = _normalize_path(root=root, raw_path=match.group("path"))
178
+ if not path:
179
+ continue
180
+ frame: dict[str, Any] = {
181
+ "path": path,
182
+ "line": int(match.group("line")),
183
+ }
184
+ symbol = match.groupdict().get("symbol")
185
+ if symbol:
186
+ frame["symbol"] = symbol
187
+ frames.append(frame)
188
+
189
+ for match in _PATH_LINE_RE.finditer(output):
190
+ path = _normalize_path(root=root, raw_path=match.group("path"))
191
+ if not path:
192
+ continue
193
+ frames.append({"path": path, "line": int(match.group("line"))})
194
+
195
+ return _dedupe_dicts(frames)
196
+
197
+
198
+ def _primary_error_line(output: str) -> str:
199
+ lines = [" ".join(line.strip().split()) for line in output.splitlines() if line.strip()]
200
+ for line in lines:
201
+ if line.startswith("E "):
202
+ return _clip(line[2:].strip(), 320)
203
+ for line in reversed(lines):
204
+ if _EXCEPTION_SUMMARY_RE.match(line):
205
+ return _clip(line, 320)
206
+ for marker in _ERROR_MARKERS:
207
+ for line in lines:
208
+ if marker == "FAILED " and " - " in line:
209
+ continue
210
+ if line.startswith(("raise ", "assert ")):
211
+ continue
212
+ if marker in line:
213
+ return _clip(line, 320)
214
+ for line in lines:
215
+ lowered = line.casefold()
216
+ if "error" in lowered or "failed" in lowered or "exception" in lowered:
217
+ return _clip(line, 320)
218
+ return ""
219
+
220
+
221
+ def _normalize_path(*, root: Path, raw_path: str) -> str:
222
+ clean = str(raw_path or "").strip().strip("'\"")
223
+ if not clean:
224
+ return ""
225
+ clean = clean.replace("\\", "/")
226
+ parts = {part for part in clean.split("/") if part}
227
+ if parts & _SKIP_PATH_PARTS:
228
+ return ""
229
+
230
+ path_obj = Path(clean)
231
+ root_abs = root.resolve()
232
+ try:
233
+ resolved = path_obj.resolve() if path_obj.is_absolute() else (root_abs / path_obj).resolve()
234
+ relative = resolved.relative_to(root_abs).as_posix()
235
+ except (OSError, ValueError):
236
+ return ""
237
+ return "" if relative == "." else relative
238
+
239
+
240
+ def _normalize_pytest_nodeid(*, normalized_path: str, raw_nodeid: str) -> str:
241
+ _path_part, sep, suffix = str(raw_nodeid or "").strip().partition("::")
242
+ return f"{normalized_path}{sep}{suffix}" if sep else normalized_path
243
+
244
+
245
+ def _clip(text: str, limit: int) -> str:
246
+ clean = str(text or "").strip()
247
+ if len(clean) <= limit:
248
+ return clean
249
+ return clean[: limit - 3].rstrip() + "..."
250
+
251
+
252
+ def _dedupe(values: list[str]) -> list[str]:
253
+ seen: set[str] = set()
254
+ deduped: list[str] = []
255
+ for value in values:
256
+ clean = str(value or "").strip()
257
+ if not clean or clean in seen:
258
+ continue
259
+ seen.add(clean)
260
+ deduped.append(clean)
261
+ return deduped
262
+
263
+
264
+ def _dedupe_dicts(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
265
+ seen: set[tuple[tuple[str, str], ...]] = set()
266
+ deduped: list[dict[str, Any]] = []
267
+ for item in items:
268
+ key = tuple(sorted((str(k), str(v)) for k, v in item.items()))
269
+ if key in seen:
270
+ continue
271
+ seen.add(key)
272
+ deduped.append(item)
273
+ return deduped
@@ -0,0 +1,385 @@
1
+ """Verification outcomes that never throw away work.
2
+
3
+ Two separate things used to collapse into the same "task failed" verdict:
4
+
5
+ * **Verification could not run.** No authoritative command exists for the task --
6
+ ``verify_gate`` deliberately suppresses generic fallbacks and empties the command
7
+ list for docs-only, static-web, and CI-only workspaces. The files were written and
8
+ are on disk, but strict mode reported a plain failure, and the swarm's failure
9
+ cleanup then deleted the worktree. Missing tooling is not a defect in the work, so
10
+ it now produces :data:`TASK_STATUS_COMPLETED_UNVERIFIED` instead.
11
+ * **Verification ran and failed.** That *is* a real signal about the work, but a
12
+ single shot at it is a waste: the failing command's own output is usually enough
13
+ for the executing agent to fix the problem. :func:`run_verification_repair_loop`
14
+ gives it a bounded number of tries before the task is failed.
15
+
16
+ This module is deliberately a stdlib-only leaf: it holds the policy (how many
17
+ attempts, when to stop, what the repair prompt says) while the caller keeps every
18
+ side effect (running the agent, committing, re-running verification). That split is
19
+ what makes the loop unit-testable without a git repository or a provider.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ from collections.abc import Callable, Sequence
26
+ from dataclasses import dataclass
27
+ from typing import Any, Protocol
28
+
29
+ # Plan status for "the work landed, but nothing authoritative could check it".
30
+ # Not a failure: the task is terminal-successful for progress and dependency
31
+ # purposes, and its files/branch are kept.
32
+ TASK_STATUS_COMPLETED_UNVERIFIED = "completed_unverified"
33
+
34
+ DEFAULT_VERIFICATION_REPAIR_ATTEMPTS = 2
35
+ VERIFICATION_REPAIR_ATTEMPTS_ENV = "ALYSIS_VERIFY_REPAIR_ATTEMPTS"
36
+
37
+ # A ceiling, not a policy: a mistyped budget should not turn one task into an
38
+ # unbounded provider spend loop.
39
+ MAX_VERIFICATION_REPAIR_ATTEMPTS = 10
40
+
41
+ # Per-command excerpt budget for the repair prompt. Enough to carry a traceback and
42
+ # the assertion that produced it; small enough that three failing commands still fit
43
+ # beside the task instruction.
44
+ DEFAULT_COMMAND_OUTPUT_CHARS = 4000
45
+
46
+
47
+ class _VerifyCommandResultLike(Protocol):
48
+ command: str
49
+ exit_code: int
50
+ output: str
51
+
52
+ @property
53
+ def ok(self) -> bool: ...
54
+
55
+
56
+ class _VerifyRunResultLike(Protocol):
57
+ command_results: list[Any]
58
+
59
+ @property
60
+ def all_passed(self) -> bool: ...
61
+
62
+ @property
63
+ def summary(self) -> str: ...
64
+
65
+ @property
66
+ def failed_commands(self) -> list[str]: ...
67
+
68
+
69
+ def resolve_repair_attempt_budget(
70
+ override: int | None = None,
71
+ *,
72
+ env: dict[str, str] | None = None,
73
+ ) -> int:
74
+ """Return how many repair attempts a failing verification gets.
75
+
76
+ Precedence: explicit override (CLI flag) > environment > default. Negative and
77
+ unparseable values fall back rather than raise -- a bad budget must not be the
78
+ reason a task cannot run at all.
79
+ """
80
+ if override is not None:
81
+ return _clamp_attempts(override)
82
+ raw = (env if env is not None else os.environ).get(VERIFICATION_REPAIR_ATTEMPTS_ENV)
83
+ if raw is not None and str(raw).strip():
84
+ try:
85
+ return _clamp_attempts(int(str(raw).strip()))
86
+ except (TypeError, ValueError):
87
+ return DEFAULT_VERIFICATION_REPAIR_ATTEMPTS
88
+ return DEFAULT_VERIFICATION_REPAIR_ATTEMPTS
89
+
90
+
91
+ def _clamp_attempts(value: int) -> int:
92
+ try:
93
+ parsed = int(value)
94
+ except (TypeError, ValueError):
95
+ return DEFAULT_VERIFICATION_REPAIR_ATTEMPTS
96
+ if parsed < 0:
97
+ return 0
98
+ return min(parsed, MAX_VERIFICATION_REPAIR_ATTEMPTS)
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class RepairAttemptExecution:
103
+ """What the caller actually did for one repair attempt.
104
+
105
+ ``verify_result`` is the re-run verification, or ``None`` when the attempt could
106
+ not get far enough to re-verify (agent crash, commit failure).
107
+ """
108
+
109
+ agent_exit_code: int
110
+ verify_result: Any | None
111
+ committed: bool = False
112
+ changed_files: tuple[str, ...] = ()
113
+ error: str | None = None
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class VerificationRepairAttempt:
118
+ """One recorded attempt, for the report, the event stream, and the artifact."""
119
+
120
+ attempt: int
121
+ agent_exit_code: int
122
+ committed: bool
123
+ verification_passed: bool
124
+ verification_summary: str
125
+ failed_commands: tuple[str, ...] = ()
126
+ changed_files: tuple[str, ...] = ()
127
+ error: str | None = None
128
+
129
+ def to_payload(self) -> dict[str, Any]:
130
+ return {
131
+ "attempt": self.attempt,
132
+ "agent_exit_code": self.agent_exit_code,
133
+ "committed": bool(self.committed),
134
+ "verification_passed": bool(self.verification_passed),
135
+ "verification_summary": self.verification_summary,
136
+ "failed_commands": list(self.failed_commands),
137
+ "changed_files": list(self.changed_files),
138
+ "error": self.error,
139
+ }
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class VerificationRepairOutcome:
144
+ """Result of the whole loop.
145
+
146
+ ``final_result`` is the verification result the task's outcome is decided on:
147
+ the last one that actually ran, which is the initial result when no attempt
148
+ re-verified.
149
+ """
150
+
151
+ final_result: Any
152
+ attempts: tuple[VerificationRepairAttempt, ...] = ()
153
+ skipped_reason: str | None = None
154
+
155
+ @property
156
+ def passed(self) -> bool:
157
+ result = self.final_result
158
+ return bool(getattr(result, "all_passed", False))
159
+
160
+ @property
161
+ def attempts_used(self) -> int:
162
+ return len(self.attempts)
163
+
164
+ @property
165
+ def repaired(self) -> bool:
166
+ """True when a repair attempt is what turned verification green."""
167
+ return self.passed and bool(self.attempts)
168
+
169
+ @property
170
+ def exhausted(self) -> bool:
171
+ """True when attempts were spent and verification still fails."""
172
+ return bool(self.attempts) and not self.passed
173
+
174
+ def to_payload(self) -> dict[str, Any]:
175
+ return {
176
+ "attempts_used": self.attempts_used,
177
+ "passed": self.passed,
178
+ "repaired": self.repaired,
179
+ "exhausted": self.exhausted,
180
+ "skipped_reason": self.skipped_reason,
181
+ "attempts": [item.to_payload() for item in self.attempts],
182
+ }
183
+
184
+ def report_lines(self) -> list[str]:
185
+ """Human-readable lines for the task report / warning list."""
186
+ if self.skipped_reason and not self.attempts:
187
+ return [f"Verification repair skipped: {self.skipped_reason}"]
188
+ lines: list[str] = []
189
+ for item in self.attempts:
190
+ state = "passed" if item.verification_passed else "still failing"
191
+ line = f"Repair attempt {item.attempt}: {state} -- {item.verification_summary}"
192
+ if item.error:
193
+ line += f" (attempt error: {item.error})"
194
+ lines.append(line)
195
+ if self.skipped_reason:
196
+ lines.append(f"Verification repair stopped: {self.skipped_reason}")
197
+ return lines
198
+
199
+
200
+ def run_verification_repair_loop(
201
+ *,
202
+ initial_result: Any,
203
+ max_attempts: int,
204
+ attempt_repair: Callable[[int, Any], RepairAttemptExecution],
205
+ repairable: Callable[[Any], bool] | None = None,
206
+ on_attempt: Callable[[VerificationRepairAttempt], None] | None = None,
207
+ ) -> VerificationRepairOutcome:
208
+ """Re-run the executing agent against its own failing verification output.
209
+
210
+ The caller's ``attempt_repair(attempt_number, failing_result)`` does the work --
211
+ build the prompt, run the agent, commit, re-verify -- and returns what happened.
212
+ This function owns only the decision to keep going.
213
+
214
+ An attempt that cannot re-verify (agent crashed, nothing committed, commit
215
+ failed) ends the loop: without a fresh verification result there is nothing new
216
+ to feed the next attempt, so spending the remaining budget would just repeat the
217
+ same prompt.
218
+ """
219
+ budget = _clamp_attempts(max_attempts)
220
+ if getattr(initial_result, "all_passed", False):
221
+ return VerificationRepairOutcome(final_result=initial_result)
222
+ if budget <= 0:
223
+ return VerificationRepairOutcome(
224
+ final_result=initial_result,
225
+ skipped_reason="no repair attempts are configured",
226
+ )
227
+ if repairable is not None and not repairable(initial_result):
228
+ return VerificationRepairOutcome(
229
+ final_result=initial_result,
230
+ skipped_reason="the verification failure is not repairable by editing code",
231
+ )
232
+
233
+ attempts: list[VerificationRepairAttempt] = []
234
+ current = initial_result
235
+ skipped_reason: str | None = None
236
+
237
+ for attempt_number in range(1, budget + 1):
238
+ execution = attempt_repair(attempt_number, current)
239
+ result = execution.verify_result
240
+ passed = bool(getattr(result, "all_passed", False)) if result is not None else False
241
+ record = VerificationRepairAttempt(
242
+ attempt=attempt_number,
243
+ agent_exit_code=int(execution.agent_exit_code),
244
+ committed=bool(execution.committed),
245
+ verification_passed=passed,
246
+ verification_summary=(
247
+ str(getattr(result, "summary", ""))
248
+ if result is not None
249
+ else "verification did not re-run"
250
+ ),
251
+ failed_commands=tuple(
252
+ str(item) for item in (getattr(result, "failed_commands", ()) or ())
253
+ ),
254
+ changed_files=tuple(execution.changed_files),
255
+ error=execution.error,
256
+ )
257
+ attempts.append(record)
258
+ if on_attempt is not None:
259
+ on_attempt(record)
260
+ if result is not None:
261
+ current = result
262
+ if passed:
263
+ break
264
+ if result is None:
265
+ skipped_reason = execution.error or "the repair attempt did not re-run verification"
266
+ break
267
+ if repairable is not None and not repairable(result):
268
+ skipped_reason = "the verification failure is not repairable by editing code"
269
+ break
270
+
271
+ return VerificationRepairOutcome(
272
+ final_result=current,
273
+ attempts=tuple(attempts),
274
+ skipped_reason=skipped_reason,
275
+ )
276
+
277
+
278
+ def verification_failure_excerpts(
279
+ result: Any,
280
+ *,
281
+ max_chars: int = DEFAULT_COMMAND_OUTPUT_CHARS,
282
+ max_commands: int = 5,
283
+ ) -> list[tuple[str, int, str]]:
284
+ """Return ``(command, exit_code, output excerpt)`` for each failing command.
285
+
286
+ The tail of the output is kept, not the head: a test runner puts the failure
287
+ summary last, and the first lines are usually collection noise.
288
+ """
289
+ excerpts: list[tuple[str, int, str]] = []
290
+ for item in list(getattr(result, "command_results", None) or []):
291
+ if getattr(item, "ok", False):
292
+ continue
293
+ command = str(getattr(item, "command", "") or "")
294
+ exit_code = int(getattr(item, "exit_code", 1) or 0)
295
+ excerpts.append(
296
+ (command, exit_code, _tail(str(getattr(item, "output", "") or ""), max_chars))
297
+ )
298
+ if len(excerpts) >= max_commands:
299
+ break
300
+ return excerpts
301
+
302
+
303
+ def _tail(text: str, max_chars: int) -> str:
304
+ if max_chars <= 0 or len(text) <= max_chars:
305
+ return text
306
+ return "[... earlier output truncated ...]\n" + text[-max_chars:]
307
+
308
+
309
+ def build_repair_instruction(
310
+ *,
311
+ base_instruction: str,
312
+ task_id: str,
313
+ attempt: int,
314
+ max_attempts: int,
315
+ verify_summary: str,
316
+ excerpts: Sequence[tuple[str, int, str]],
317
+ artifact_path: str | None = None,
318
+ ) -> str:
319
+ """Append the failing verification output to the task's own instruction.
320
+
321
+ The base instruction is repeated verbatim because a repair attempt runs in a
322
+ fresh session: without it the agent would be asked to fix a failure for a task
323
+ it has no context on.
324
+ """
325
+ lines: list[str] = [
326
+ "# Verification Repair Attempt",
327
+ "",
328
+ f"Task: {task_id}",
329
+ f"Repair attempt {attempt} of {max_attempts}.",
330
+ "",
331
+ "Your previous changes for this task are already committed, but the "
332
+ "authoritative verification commands below FAILED against them.",
333
+ "",
334
+ f"Verification summary: {verify_summary}",
335
+ "",
336
+ "## Failing verification output",
337
+ "",
338
+ ]
339
+ if not excerpts:
340
+ lines.append("(no per-command output was captured)")
341
+ lines.append("")
342
+ for command, exit_code, output in excerpts:
343
+ lines.append(f"### `{command}` (exit code {exit_code})")
344
+ lines.append("")
345
+ lines.append("```")
346
+ lines.append(output.rstrip("\n") if output.strip() else "(no output captured)")
347
+ lines.append("```")
348
+ lines.append("")
349
+ if artifact_path:
350
+ lines.append(f"Full verification log: {artifact_path}")
351
+ lines.append("")
352
+ lines.extend(
353
+ [
354
+ "## What to do now",
355
+ "",
356
+ "1. Read the failing output above and find the actual cause.",
357
+ "2. Fix it with the smallest correct change, inside this task's write scope.",
358
+ "3. Do not weaken, skip, or delete the failing checks to make them pass.",
359
+ "4. Do not re-do work that already succeeded; only repair what the output shows.",
360
+ "",
361
+ "Verification will be re-run automatically after this attempt.",
362
+ "",
363
+ "---",
364
+ "",
365
+ "# Original Task",
366
+ "",
367
+ ]
368
+ )
369
+ return "\n".join(lines) + base_instruction
370
+
371
+
372
+ __all__ = [
373
+ "DEFAULT_COMMAND_OUTPUT_CHARS",
374
+ "DEFAULT_VERIFICATION_REPAIR_ATTEMPTS",
375
+ "MAX_VERIFICATION_REPAIR_ATTEMPTS",
376
+ "TASK_STATUS_COMPLETED_UNVERIFIED",
377
+ "VERIFICATION_REPAIR_ATTEMPTS_ENV",
378
+ "RepairAttemptExecution",
379
+ "VerificationRepairAttempt",
380
+ "VerificationRepairOutcome",
381
+ "build_repair_instruction",
382
+ "resolve_repair_attempt_budget",
383
+ "run_verification_repair_loop",
384
+ "verification_failure_excerpts",
385
+ ]