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,2885 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field, replace
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from ..diff_paths import iter_patch_paths
10
+ from ..failure_category import FailureCategory, is_infra_unavailable_error
11
+ from ..language_policy import normalize_language_name
12
+ from ..runtime_kind import RuntimeKind
13
+ from ..tools.availability import is_tool_unavailable_result
14
+ from ..verification_command_analysis import (
15
+ analyze_verification_command,
16
+ is_benign_non_execution_reason,
17
+ )
18
+ from ..verify_gate import (
19
+ ResolvedVerifyCommands,
20
+ assess_verification_command_execution,
21
+ extract_actionable_failure_snippet,
22
+ extract_verification_failure_snippet,
23
+ is_authoritative_verify_command_selection,
24
+ is_toolchain_unavailable_verification_output,
25
+ resolve_task_aware_verify_command_selection,
26
+ verification_selection_payload,
27
+ )
28
+ from ..verify_gate import run_task_verification as run_task_verification
29
+ from .acceptance_contract import (
30
+ AcceptanceContract,
31
+ acceptance_contract_problem_payload,
32
+ extract_explicit_acceptance_commands,
33
+ record_acceptance_tool_effect,
34
+ )
35
+ from .blast_radius import (
36
+ MAX_SCOPE_RUNS,
37
+ BlastRadiusAssessment,
38
+ BlastRadiusPolicy,
39
+ BlastRadiusScope,
40
+ BlastRadiusStatus,
41
+ ScopePhase,
42
+ ScopeRun,
43
+ assess_blast_radius,
44
+ blast_radius_blocks_finalization,
45
+ build_blast_radius_nudge_line,
46
+ classify_scope_phase,
47
+ command_path_selectors,
48
+ )
49
+ from .completion_certificate import (
50
+ CompletionCertificateInput,
51
+ evaluate_completion_certificate,
52
+ )
53
+ from .completion_gate import CompletionGateControllerState
54
+ from .mutation_classification import classify_mutation_paths, material_mutation_paths
55
+ from .prompt_context import (
56
+ _extract_workspace_relation_paths_from_text,
57
+ _normalize_repo_relative_hint_path,
58
+ _paths_require_verification,
59
+ _session_repo_scan,
60
+ _session_task_brief_content,
61
+ _session_verify_command_selection,
62
+ _task_brief_lines_from_text,
63
+ _verification_commands_apply_to_paths,
64
+ refresh_session_environment_context_message,
65
+ )
66
+ from .regression_baseline import (
67
+ EMPTY_REGRESSION_DIFF,
68
+ BaselineRecord,
69
+ PostEditTestRun,
70
+ RegressionDiffResult,
71
+ TestReport,
72
+ aggregate_regression_results,
73
+ baseline_command_key,
74
+ classify_regression_diff,
75
+ command_is_test_runner,
76
+ parse_test_report,
77
+ )
78
+ from .reproduction_first import (
79
+ MAX_REPRO_ARTIFACTS,
80
+ MAX_REPRO_RUNS,
81
+ ReproAssessment,
82
+ ReproPhase,
83
+ ReproRun,
84
+ TaskShape,
85
+ assess_reproduction,
86
+ build_repro_artifacts_nudge_line,
87
+ build_repro_nudge_line,
88
+ classify_repro_phase,
89
+ match_repro_artifacts,
90
+ repro_blocks_finalization,
91
+ )
92
+ from .turn_contract import (
93
+ AdvisoryCompletion,
94
+ DispositionRecord,
95
+ Expectation,
96
+ ExpectationAssessment,
97
+ ExpectationEvidence,
98
+ assess_expectations,
99
+ match_expectation_evidence,
100
+ )
101
+ from .verification_commands import (
102
+ _matching_effective_verification_commands,
103
+ _normalize_shell_command_for_match,
104
+ )
105
+ from .verification_evidence import (
106
+ VerificationEvidence,
107
+ VerificationEvidenceCategory,
108
+ classify_verification_evidence,
109
+ command_is_qualifying_execution_evidence,
110
+ )
111
+
112
+ if TYPE_CHECKING:
113
+ from .turn_path import _OneShotRepoTurnIntent
114
+
115
+
116
+ _COMMAND_LIKE_MUTATION_TOOL_NAMES = {"verify_run", "shell_run"}
117
+ _MATERIAL_EDIT_TOOL_NAMES = {
118
+ "fs_write",
119
+ "fs_edit",
120
+ "git_apply_patch",
121
+ "fs_move",
122
+ "fs_copy",
123
+ "fs_delete",
124
+ "fs_mkdir",
125
+ "shell_service_start",
126
+ "workspace_preview_start",
127
+ }
128
+ _VERIFICATION_SHELL_MARKERS = (
129
+ "pytest",
130
+ "py.test",
131
+ "unittest",
132
+ "tox",
133
+ "nox",
134
+ "go test",
135
+ "cargo test",
136
+ "npm test",
137
+ "pnpm test",
138
+ "yarn test",
139
+ "vitest",
140
+ "jest",
141
+ "ruff check",
142
+ "mypy",
143
+ "flake8",
144
+ "pylint",
145
+ "make test",
146
+ "make check",
147
+ )
148
+ _TEST_EXECUTION_COMMAND_RE = re.compile(
149
+ r"(?:^|\s)(?:pytest|py\.test|tox|nox)(?:\s|$)|"
150
+ r"\b(?:python(?:3)?|py)\s+-m\s+(?:pytest|unittest)\b|"
151
+ r"\b(?:python(?:3)?|py)\b[^\n]*\bmanage\.py\s+test\b|"
152
+ r"\b(?:python(?:3)?|py)\b[^\n]*\b(?:runtests?|test_[^\s/]+|[^\s/]+_test)\.py\b|"
153
+ r"(?:^|\s)(?:\./)?(?:bin/)?(?:runtests?|test)(?:\s|$)|"
154
+ r"\b(?:go|cargo)\s+test\b|"
155
+ r"\b(?:npm|pnpm|yarn)\s+test\b|"
156
+ r"\b(?:vitest|jest|rspec|phpunit|ctest)\b|"
157
+ r"\b(?:mvn|mvnw|maven|gradle|gradlew|dotnet|bazel|mix)\b[^\n]*\btest\b|"
158
+ r"\b(?:make|just)\s+(?:test|check)\b",
159
+ re.IGNORECASE,
160
+ )
161
+ _TEST_SUCCESS_CLAIM_RE = re.compile(
162
+ r"\b(?:all\s+)?(?:\d+\s+)?tests?(?:\s+suite)?\s+"
163
+ r"(?:(?:is|are|was|were)\s+)?"
164
+ r"(?:pass(?:ed|es|ing)?|succeed(?:ed|s)?|green)\b|"
165
+ r"\btests?\s*:\s*[^\n]{0,120}\b(?:pass(?:ed|es|ing)?|succeed(?:ed|s)?)\b|"
166
+ r"\b(?:pass(?:ed|es|ing)?|green)\s+(?:all\s+)?tests?\b",
167
+ re.IGNORECASE,
168
+ )
169
+ _GENERIC_VERIFICATION_SUCCESS_CLAIM_RE = re.compile(
170
+ r"\bverified\b|"
171
+ r"\bverification\s+(?:passed|succeeded|completed|was\s+successful)\b|"
172
+ r"\b(?:validation|checks?)\s+(?:passed|succeeded)\b",
173
+ re.IGNORECASE,
174
+ )
175
+ _NEGATED_CLAIM_PREFIX_RE = re.compile(
176
+ r"(?:\b(?:not|never|no|without)\b[^.!?\n]{0,32}|"
177
+ r"\b(?:cannot|can't|could\s+not|couldn't|did\s+not|didn't|wasn't|isn't|"
178
+ r"unable\s+to|failed\s+to)(?:\s+be)?)\s*$",
179
+ re.IGNORECASE,
180
+ )
181
+ _SAFE_LEADING_CD_RE = re.compile(
182
+ r"^\s*cd(?:\s+/d)?\s+(?:\"[^\"]*\"|'[^']*'|[^\s]+)\s*&&\s*",
183
+ re.IGNORECASE,
184
+ )
185
+ _UNSAFE_CLAIM_EVIDENCE_SHELL_RE = re.compile(
186
+ r"\|\||(?<![&])\|(?![&])|;|[\r\n]|&&|(?:^|\s)&(?:\s|$)",
187
+ )
188
+ _SHELL_REDIRECTION_RE = re.compile(
189
+ r"\s+(?:\d*>&\d+|\d*(?:>>?|<)\s*[^\s]+)(?=\s|$)",
190
+ )
191
+ SUPPLEMENTAL_VERIFICATION_ADVISORY = (
192
+ "Note: every passing check so far was authored during this session. "
193
+ "Self-written tests verify your interpretation, not the task's. Re-read the "
194
+ "task's exact requirements (output path, format, names, values) and confirm "
195
+ "your deliverable against the spec itself before finalizing."
196
+ )
197
+ # One-shot advisory emitted at the first verification-relevant edit when no
198
+ # baseline exists for any known verification-contract command. Advisory only -
199
+ # it never blocks the edit; it teaches the baseline-first protocol so failures
200
+ # can later be attributed to the change vs pre-existing breakage.
201
+ REGRESSION_BASELINE_PRE_EDIT_ADVISORY = (
202
+ "Baseline advisory: this is your first change to a verifiable surface and no "
203
+ "pre-edit test run is recorded. To let me tell failures your change causes "
204
+ "apart from ones already present in the repo, run the relevant test command "
205
+ "(your verification command) once before further edits. Advisory only - this "
206
+ "does not block your edit."
207
+ )
208
+ # One-shot advisory emitted the first time a material edit lands inside a
209
+ # generated or vendored tree (node_modules, vendor, externals, third_party, ...).
210
+ # Advisory only - it never blocks the edit; legitimate vendored fixes exist, but
211
+ # in practice edits there are usually a mistargeted change that breaks
212
+ # neighboring tests wholesale.
213
+ VENDORED_PATH_EDIT_ADVISORY = (
214
+ "Scope advisory: this edit changes files under a generated or vendored tree "
215
+ "({paths}). Vendored/generated code is almost never where the fix belongs - "
216
+ "it is overwritten by upstream syncs and edits there tend to break many "
217
+ "unrelated tests. Prefer the first-party source module; if the vendored copy "
218
+ "truly is the target, keep the change minimal and run the neighboring tests "
219
+ "before finalizing. Advisory only - this does not block your edit."
220
+ )
221
+ # One-shot finalize-time advisory for execute turns whose completion gate is
222
+ # otherwise clear. Small diffs that satisfy the agent's own reproduction are the
223
+ # dominant shape of "almost right" outcomes: the issue usually implies more
224
+ # behavior (exact message wording, boundary inputs, interactions) than the first
225
+ # repro covers. One adversarial pass converts a measurable share of these.
226
+ ADVERSARIAL_FINALIZE_REVIEW_ADVISORY = (
227
+ "Adversarial review - one pass before you finish: re-read the original "
228
+ "request end to end and enumerate every behavior it implies, not just the "
229
+ "headline symptom: exact error/message wording, boundary and degenerate "
230
+ "inputs (zero, empty, None, negative), types and units, and every "
231
+ "interaction or API named anywhere in the report. For each implied "
232
+ "behavior, either point at evidence you already ran that covers it, or "
233
+ "extend your reproduction to cover it and run it now. Acceptance checks "
234
+ "usually probe edge semantics beyond the reported case. If everything is "
235
+ "already covered, finalize."
236
+ )
237
+
238
+
239
+ def _adversarial_finalize_enabled(cfg: Any | None) -> bool:
240
+ """Kill-switch for the adversarial finalize review (near-miss pass).
241
+
242
+ ``ALYSIS_ADVERSARIAL_FINALIZE`` (off/0/false/no/disabled) wins over the
243
+ config value; default is on.
244
+ """
245
+ from ..branding import env_get
246
+
247
+ env_value = env_get("ALYSIS_ADVERSARIAL_FINALIZE")
248
+ if env_value is not None:
249
+ normalized = str(env_value).strip().lower()
250
+ if normalized in {"off", "0", "false", "no", "disabled"}:
251
+ return False
252
+ if normalized in {"on", "1", "true", "yes", "enabled"}:
253
+ return True
254
+ return bool(getattr(cfg, "adversarial_finalize_review", True))
255
+
256
+
257
+ # Maximum bounded repair rounds for a post-edit execution-evidence deficit before
258
+ # the gate finalizes honestly-unverified rather than accepting prose in its place.
259
+ EVIDENCE_REPAIR_ROUND_BOUND = 2
260
+ # Visible marker appended to the final summary when the run finalizes without the
261
+ # execution evidence the ordering rule requires (fail honest, never silent).
262
+ HONEST_UNVERIFIED_FINALIZATION_MARKER = (
263
+ "\n\n---\n"
264
+ "⚠️ Unverified: I could not obtain a passing test execution after my last "
265
+ "change to the code within this run. This result is finalized as UNVERIFIED — "
266
+ "the change has not been confirmed by running the relevant tests."
267
+ )
268
+ # Visible marker appended when the run finalizes with regressions the change
269
+ # introduced that a bounded action-only repair could not resolve (fail honest).
270
+ # Distinct wording from the unverified marker; leads with "REGRESSIONS UNRESOLVED".
271
+ _REGRESSIONS_UNRESOLVED_FINALIZATION_MARKER_PREFIX = (
272
+ "\n\n---\n"
273
+ "⛔ REGRESSIONS UNRESOLVED: {ids}. These tests passed in the pre-edit baseline "
274
+ "of `{baseline}` and fail after my change; I could not make them pass within "
275
+ "this run. This result is finalized with KNOWN REGRESSIONS my change introduced."
276
+ )
277
+
278
+
279
+ def build_regressions_unresolved_marker(
280
+ regressed_ids: list[str] | tuple[str, ...],
281
+ *,
282
+ baseline_command: str = "",
283
+ ) -> str:
284
+ ids = ", ".join(str(item) for item in regressed_ids if str(item).strip())
285
+ baseline = str(baseline_command or "").strip() or "the baseline command"
286
+ return _REGRESSIONS_UNRESOLVED_FINALIZATION_MARKER_PREFIX.format(ids=ids, baseline=baseline)
287
+
288
+
289
+ # Visible marker appended when the run finalizes with test failures whose
290
+ # relationship to the change could not be established (no comparable baseline).
291
+ # Distinct wording again; leads with "UNATTRIBUTED FAILURES".
292
+ _UNATTRIBUTED_FAILURES_FINALIZATION_MARKER_PREFIX = (
293
+ "\n\n---\n"
294
+ "⚠️ UNATTRIBUTED FAILURES: {ids}. These tests fail after my change, but I have "
295
+ "no comparable pre-edit baseline for the same command to determine whether my "
296
+ "change caused them. This result is finalized with their cause UNATTRIBUTED — "
297
+ "neither confirmed pre-existing nor confirmed a regression."
298
+ )
299
+
300
+
301
+ def build_unattributed_failures_marker(unattributed_ids: list[str] | tuple[str, ...]) -> str:
302
+ ids = ", ".join(str(item) for item in unattributed_ids if str(item).strip())
303
+ return _UNATTRIBUTED_FAILURES_FINALIZATION_MARKER_PREFIX.format(ids=ids)
304
+
305
+
306
+ _COMPLETION_GATE_PROBLEM_LABELS = {
307
+ "empty_final_response": "empty final response",
308
+ "no_material_edits": "no material edits",
309
+ "verification_not_attempted": "verification not attempted",
310
+ "verification_incomplete": "verification coverage incomplete",
311
+ "verification_failed": "verification failing",
312
+ "regressions_detected": "regressions introduced",
313
+ "unattributed_failures": "failures not yet attributed",
314
+ "expectations_unaddressed": "task expectations unaddressed",
315
+ "repro_unconfirmed": "reported symptom not reproduced",
316
+ "repro_artifacts_present": "reproduction scaffolding left in the tree",
317
+ "blast_radius_regressions": "neighbouring tests broken by the change",
318
+ "blast_radius_unverified": "blast radius not measured",
319
+ "acceptance_criteria_unverified": "acceptance criteria unverified",
320
+ "acceptance_criteria_failed": "acceptance criteria failed",
321
+ "acceptance_evidence_insufficient": "acceptance evidence insufficient",
322
+ "unexpected_scope_changes": "unexpected scope changes",
323
+ }
324
+ _ONE_SHOT_COMPLETION_GATE_NUDGE_PREFIX = (
325
+ "Completion gate: this one-shot execution run cannot finalize yet."
326
+ )
327
+ _RUNTIME_DEFAULT_LANGUAGE = "english"
328
+ _RUNTIME_MESSAGE_CATALOG: dict[str, dict[str, str]] = {
329
+ "english": {
330
+ "phase_understanding_request": "Understanding your request.",
331
+ "phase_drafting_response": "Contacting model provider.",
332
+ "phase_compacted_history": "Compacted conversation history.",
333
+ "phase_retrying_step": "Retrying with higher temperature for this step.",
334
+ "phase_running_tool_steps": "Running {count} tool step(s): {names}.",
335
+ "phase_post_explore_bootstrap": (
336
+ "Detected post-explore stagnation; nudging implementation bootstrap."
337
+ ),
338
+ "phase_exploration_stagnation": (
339
+ "Detected exploration stagnation; nudging toward implementation."
340
+ ),
341
+ "phase_failed_edit_loop": "Detected failed edit loop; nudging strategy switch.",
342
+ "phase_continuing_one_shot": (
343
+ "Continuing one-shot execution after non-final progress update."
344
+ ),
345
+ "phase_continuing_execution": "Continuing execution after non-final progress update.",
346
+ "phase_completion_gate_repair": (
347
+ "Completion gate detected missing execution evidence; requesting action-oriented repair."
348
+ ),
349
+ "phase_optional_finalization_review": (
350
+ "Requirements satisfied; running an optional final review."
351
+ ),
352
+ "phase_step_budget_handoff": (
353
+ "Step budget exhausted; preparing a concise handoff so the chat can continue."
354
+ ),
355
+ "phase_writing_final_response": "Writing the final response.",
356
+ "one_shot_continuation_nudge": (
357
+ "Continue execution now. A text-only plan or progress update is incomplete "
358
+ "for this one-shot run. Use the next required tool action to implement or "
359
+ "create the requested deliverable, run an implementation-producing command, "
360
+ "verify only after material work exists or when the implementation already "
361
+ "exists, or explain a concrete evidence-backed blocker."
362
+ ),
363
+ "interactive_continuation_nudge": (
364
+ "Continue execution now. Do not stop at a planning/progress update. "
365
+ "Use tools to make progress, run relevant verification, or explain a concrete blocker."
366
+ ),
367
+ "one_shot_exploration_nudge": (
368
+ "Avoid repeated read-only exploration. Start implementing or creating the "
369
+ "requested deliverable now, delegate once to a suitable available subagent "
370
+ "if more investigation is genuinely needed, or explain a concrete "
371
+ "evidence-backed blocker."
372
+ ),
373
+ "one_shot_post_explore_bootstrap_nudge": (
374
+ "A subagent already returned useful context in this one-shot turn. You now have enough "
375
+ "context to start implementation. Do not call the same research subagent again "
376
+ "in this turn. Do not use more read-only tools unless there is a concrete blocker. "
377
+ "Your next step must be an implementation or deliverable-creation action "
378
+ "(for example fs_edit, fs_write, git_apply_patch, fs_move, fs_copy, or "
379
+ "shell_run only when it actually performs implementation or creates the "
380
+ "requested deliverable) or a concrete evidence-backed blocker report. "
381
+ "Verification comes after material work exists."
382
+ ),
383
+ "one_shot_post_explore_bootstrap_targets": ("Likely repo-root-relative targets: {joined}."),
384
+ "one_shot_edit_strategy_nudge": (
385
+ "Edit strategy is stuck. Switch approach now: re-read the target lines, then use "
386
+ "fs_edit replace_lines/insert_before_line/insert_after_line with expected_old when "
387
+ "possible, or exact ops replace_exact/insert_before_exact/insert_after_exact when "
388
+ "matching known text. If localized fs_edit is a poor fit, use git_apply_patch or "
389
+ "fs_write. Do not repeat the same failing edit call."
390
+ ),
391
+ "one_shot_non_final_progress_stopped": (
392
+ "One-shot run stopped: model returned repeated/non-final progress text "
393
+ "without continuing implementation."
394
+ ),
395
+ "interactive_non_final_progress_stopped": (
396
+ "Execution turn stopped: model returned repeated/non-final progress text "
397
+ "without continuing implementation."
398
+ ),
399
+ "one_shot_post_explore_retry_exhausted": (
400
+ "One-shot run stopped: post-explore stagnation persisted after bounded "
401
+ "implementation-bootstrap nudges. Start implementing or creating the requested "
402
+ "deliverable now or report a concrete blocker."
403
+ ),
404
+ "one_shot_exploration_retry_exhausted": (
405
+ "One-shot run stopped: exploration stagnation persisted after bounded nudges. "
406
+ "Start implementing or creating the requested deliverable, delegate once to a "
407
+ "suitable available subagent if more "
408
+ "investigation is genuinely needed, or report a concrete blocker."
409
+ ),
410
+ "one_shot_edit_retry_exhausted": (
411
+ "One-shot run stopped: failed edit/write loop persisted after bounded strategy "
412
+ "nudges. Switch to exact-match fs_edit ops, or use git_apply_patch/fs_write, "
413
+ "or report a concrete blocker."
414
+ ),
415
+ "one_shot_post_explore_step_budget_exhausted": (
416
+ "One-shot run stopped: post-explore stagnation consumed the step budget. "
417
+ "Start implementing or creating the requested deliverable now or report a concrete blocker."
418
+ ),
419
+ "one_shot_exploration_step_budget_exhausted": (
420
+ "One-shot run stopped: exploration stagnation consumed the step budget. "
421
+ "Start implementing or creating the requested deliverable, delegate once to a "
422
+ "suitable available subagent if more "
423
+ "investigation is genuinely needed, or report a concrete blocker."
424
+ ),
425
+ "one_shot_edit_step_budget_exhausted": (
426
+ "One-shot run stopped: failed edit/write loop consumed the step budget. "
427
+ "Switch to exact-match fs_edit ops, or use git_apply_patch/fs_write, "
428
+ "or report a concrete blocker."
429
+ ),
430
+ "completion_gate_nudge_prefix": _ONE_SHOT_COMPLETION_GATE_NUDGE_PREFIX,
431
+ "interactive_completion_gate_nudge_prefix": (
432
+ "Completion gate: this interactive execution turn cannot finalize yet."
433
+ ),
434
+ "max_steps_exceeded": "max_steps exceeded",
435
+ },
436
+ }
437
+
438
+
439
+ @dataclass
440
+ class TurnExecutionState:
441
+ execution_requested: bool
442
+ expected_verification_commands: set[str] = field(default_factory=set)
443
+ covered_verification_commands: set[str] = field(default_factory=set)
444
+ covered_verification_command_generations: dict[str, int] = field(default_factory=dict)
445
+ material_edit_count: int = 0
446
+ material_edit_generation: int = 0
447
+ material_edit_tools: set[str] = field(default_factory=set)
448
+ touched_repo_paths: set[str] = field(default_factory=set)
449
+ last_diff_review_generation: int | None = None
450
+ verification_attempt_count: int = 0
451
+ verification_tools: set[str] = field(default_factory=set)
452
+ last_verification_passed: bool | None = None
453
+ last_verification_failure_snippet: str = ""
454
+ last_verification_failure_category: str = ""
455
+ failed_verification_command_snippets: dict[str, str] = field(default_factory=dict)
456
+ verification_relevant_edit_generation: int = 0
457
+ last_successful_verification_generation: int | None = None
458
+ verification_evidence_counts: dict[str, int] = field(default_factory=dict)
459
+ latest_verification_evidence_category: str = ""
460
+ latest_verification_evidence_reason: str = ""
461
+ accepted_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
462
+ supplemental_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
463
+ rejected_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
464
+ executed_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
465
+ verification_evidence_generation: int = 0
466
+ # Ordering rule: the verification-relevant edit generation at the time of the
467
+ # most recent qualifying execution-evidence event (a real test/execution run,
468
+ # not a syntax-only or static check). Post-edit execution evidence exists when
469
+ # this equals the current verification_relevant_edit_generation. Keying on the
470
+ # verification-relevant generation (not every material edit) means a docs-only
471
+ # edit after a passing run does not re-open the requirement.
472
+ last_post_edit_execution_generation: int | None = None
473
+ completion_gate_repair_attempts: int = 0
474
+ completion_gate_no_material_edits_repair_attempts: int = 0
475
+ completion_gate_missing_verify_repair_attempts: int = 0
476
+ completion_gate_failed_verify_repair_attempts: int = 0
477
+ completion_gate_regression_repair_attempts: int = 0
478
+ completion_gate_unattributed_repair_attempts: int = 0
479
+ completion_gate_expectations_repair_attempts: int = 0
480
+ completion_gate_repro_repair_attempts: int = 0
481
+ # Baseline-first regression protocol (step 3). Baselines are parsed per-test
482
+ # outcomes of runs recorded before the first verification-relevant edit
483
+ # (generation 0), keyed by the normalized executed command. Post-edit runs
484
+ # are compared against the same-command baseline at the completion gate.
485
+ test_baselines: dict[str, BaselineRecord] = field(default_factory=dict)
486
+ post_edit_test_runs: list[PostEditTestRun] = field(default_factory=list)
487
+ agent_created_paths: set[str] = field(default_factory=set)
488
+ regression_baseline_pre_edit_nudge_sent: bool = False
489
+ # True when the most recent verification attempt executed a test-runner
490
+ # command (pytest/unittest). Combined with an all-pre-existing/agent-authored
491
+ # diff, this lets the gate clear a non-contract test failure (the sympy-12489
492
+ # model) without masking a failing non-test command.
493
+ last_verification_attempt_was_test_run: bool = False
494
+ latest_regression_diff: dict[str, Any] = field(default_factory=dict)
495
+ pending_regression_capture_events: list[dict[str, Any]] = field(default_factory=list)
496
+ # Turn-contract v2 (step 4). ``post_edit_run_outputs`` are bounded observed
497
+ # outputs of post-edit runs, the fact surface the expected-output evidence
498
+ # linker substring-matches contract literals against. ``recorded_*`` hold any
499
+ # agent-declared dispositions / advisory-completion reason (unpopulated in this
500
+ # release; the gate synthesizes mechanically — see turn_contract.py).
501
+ post_edit_run_outputs: list[dict[str, Any]] = field(default_factory=list)
502
+ recorded_expectation_dispositions: dict[str, DispositionRecord] = field(default_factory=dict)
503
+ recorded_advisory_completion: AdvisoryCompletion | None = None
504
+ latest_expectation_assessment: dict[str, Any] = field(default_factory=dict)
505
+ latest_expectation_evidence: list[dict[str, Any]] = field(default_factory=list)
506
+ # Reproduction-first (step 5). ``repro_task_shape`` is set once at turn start;
507
+ # ``repro_runs`` are the observed executions of agent-created artifacts, each
508
+ # already phase-classified against the artifact set known at the time. The
509
+ # remaining fields carry the guardrail signals the summary must surface.
510
+ repro_task_shape: TaskShape = TaskShape.OTHER
511
+ repro_runs: list[ReproRun] = field(default_factory=list)
512
+ repro_artifact_paths: set[str] = field(default_factory=set)
513
+ repro_revision_rounds: int = 0
514
+ repro_artifacts_edited_after_fix: set[str] = field(default_factory=set)
515
+ repro_surviving_artifacts: tuple[str, ...] = ()
516
+ repro_pre_edit_nudge_sent: bool = False
517
+ repro_not_reproducing_nudge_sent: bool = False
518
+ repro_edited_after_fix_nudge_sent: bool = False
519
+ latest_repro_assessment: dict[str, Any] = field(default_factory=dict)
520
+ pending_repro_run_events: list[dict[str, Any]] = field(default_factory=list)
521
+ # Blast radius (step 6). ``blast_radius_scope`` is recomputed by the turn loop
522
+ # as the touched-path set grows; ``blast_radius_runs`` are every parsed test run
523
+ # observed this turn, each already tagged with what it selected and whether it
524
+ # ran on the clean tree. Capture is command-agnostic on purpose: the scope is
525
+ # matched by coverage at assessment time, so the agent may run it any way.
526
+ blast_radius_scope: BlastRadiusScope = field(default_factory=BlastRadiusScope)
527
+ blast_radius_runs: list[ScopeRun] = field(default_factory=list)
528
+ blast_radius_policy: BlastRadiusPolicy = field(default_factory=BlastRadiusPolicy)
529
+ blast_radius_scope_advisory_sent: bool = False
530
+ blast_radius_shrink_rounds: int = 0
531
+ completion_gate_blast_radius_repair_attempts: int = 0
532
+ latest_blast_radius_assessment: dict[str, Any] = field(default_factory=dict)
533
+ pending_blast_radius_events: list[dict[str, Any]] = field(default_factory=list)
534
+ completion_gate_controller_state: CompletionGateControllerState = field(
535
+ default_factory=CompletionGateControllerState
536
+ )
537
+ acceptance_contract: AcceptanceContract | None = None
538
+ latest_completion_certificate: dict[str, Any] = field(default_factory=dict)
539
+
540
+ def refresh_verification_coverage(self) -> None:
541
+ self.covered_verification_commands = {
542
+ command
543
+ for command, generation in self.covered_verification_command_generations.items()
544
+ if generation == self.verification_relevant_edit_generation
545
+ }
546
+
547
+ def note_verification_relevant_edit(self) -> None:
548
+ self.verification_relevant_edit_generation += 1
549
+ self.refresh_verification_coverage()
550
+
551
+ def note_material_edit(self) -> None:
552
+ self.material_edit_count += 1
553
+ self.material_edit_generation += 1
554
+
555
+ def record_diff_review(self) -> None:
556
+ self.last_diff_review_generation = self.material_edit_generation
557
+
558
+ def diff_review_is_stale(self) -> bool:
559
+ return self.material_edit_count > 0 and (
560
+ self.last_diff_review_generation is None
561
+ or self.last_diff_review_generation < self.material_edit_generation
562
+ )
563
+
564
+ def record_verification_coverage(self, commands: set[str]) -> None:
565
+ if not commands:
566
+ return
567
+ for command in commands:
568
+ self.covered_verification_command_generations[command] = (
569
+ self.verification_relevant_edit_generation
570
+ )
571
+ self.failed_verification_command_snippets.pop(command, None)
572
+ self.last_successful_verification_generation = self.verification_relevant_edit_generation
573
+ self.refresh_verification_coverage()
574
+
575
+ def record_verification_failures(self, failures: dict[str, str]) -> None:
576
+ for command, snippet in failures.items():
577
+ clean_command = str(command or "").strip()
578
+ if not clean_command:
579
+ continue
580
+ clean_snippet = str(snippet or "").strip()
581
+ self.failed_verification_command_snippets[clean_command] = clean_snippet
582
+
583
+ def record_verification_evidence(
584
+ self,
585
+ evidence: VerificationEvidence,
586
+ *,
587
+ accepted: bool,
588
+ observed_exit_code: int | None = None,
589
+ observed_output: bool = False,
590
+ ) -> None:
591
+ category = evidence.category.value
592
+ self.verification_evidence_counts[category] = (
593
+ self.verification_evidence_counts.get(category, 0) + 1
594
+ )
595
+ self.latest_verification_evidence_category = category
596
+ self.latest_verification_evidence_reason = evidence.reason
597
+ payload = evidence.as_payload()
598
+ payload["accepted"] = bool(accepted)
599
+ payload["generation"] = self.verification_relevant_edit_generation
600
+ payload["observed_exit_code"] = observed_exit_code
601
+ payload["observed_output"] = bool(observed_output)
602
+ if evidence.real_execution is True:
603
+ self.executed_verification_evidence.append(payload)
604
+ self.executed_verification_evidence[:] = self.executed_verification_evidence[-20:]
605
+ if accepted:
606
+ self.verification_evidence_generation += 1
607
+ self.accepted_verification_evidence.append(payload)
608
+ self.accepted_verification_evidence[:] = self.accepted_verification_evidence[-10:]
609
+ elif evidence.supplemental_only:
610
+ self.supplemental_verification_evidence.append(payload)
611
+ self.supplemental_verification_evidence[:] = self.supplemental_verification_evidence[
612
+ -10:
613
+ ]
614
+ else:
615
+ self.rejected_verification_evidence.append(payload)
616
+ self.rejected_verification_evidence[:] = self.rejected_verification_evidence[-10:]
617
+
618
+ def record_executed_command_evidence(
619
+ self,
620
+ *,
621
+ normalized_command: str,
622
+ observed_exit_code: int,
623
+ observed_output: bool,
624
+ ) -> None:
625
+ payload: dict[str, Any] = {
626
+ "evidence_category": "COMMAND_EXECUTION",
627
+ "normalized_command": normalized_command,
628
+ "matched_command": None,
629
+ "real_execution": True,
630
+ "allowed_to_satisfy_contract": False,
631
+ "reason": "observed_shell_verification_execution",
632
+ "covered_verification_commands": [],
633
+ "supplemental_only": False,
634
+ "accepted": False,
635
+ "generation": self.verification_relevant_edit_generation,
636
+ "observed_exit_code": observed_exit_code,
637
+ "observed_output": bool(observed_output),
638
+ }
639
+ self.executed_verification_evidence.append(payload)
640
+ self.executed_verification_evidence[:] = self.executed_verification_evidence[-20:]
641
+
642
+ def note_qualifying_execution_evidence(self) -> None:
643
+ self.last_post_edit_execution_generation = self.verification_relevant_edit_generation
644
+
645
+ def has_post_edit_execution_evidence(self) -> bool:
646
+ return (
647
+ self.material_edit_count > 0
648
+ and self.last_post_edit_execution_generation is not None
649
+ and self.last_post_edit_execution_generation
650
+ == self.verification_relevant_edit_generation
651
+ )
652
+
653
+ def note_agent_created_path(self, path: str) -> None:
654
+ cleaned = str(path or "").strip()
655
+ if cleaned:
656
+ self.agent_created_paths.add(cleaned)
657
+
658
+ def has_baseline_for_any(self, commands: list[str] | tuple[str, ...] | set[str] | None) -> bool:
659
+ """True when a usable baseline exists for any of ``commands``."""
660
+ for command in commands or []:
661
+ record = self.test_baselines.get(baseline_command_key(str(command)))
662
+ if record is not None and record.usable:
663
+ return True
664
+ return False
665
+
666
+ def note_test_execution(
667
+ self,
668
+ *,
669
+ command: str,
670
+ report: Any,
671
+ timestamp: str = "",
672
+ ) -> None:
673
+ """Record a parsed test run as a baseline (gen 0) or a post-edit run.
674
+
675
+ A run recorded before any verification-relevant edit (generation 0) with
676
+ a usable parse is a baseline for its normalized command; a later run is a
677
+ post-edit run. Unparseable pre-edit output is noted for telemetry but can
678
+ never serve as a baseline.
679
+ """
680
+ command_key = baseline_command_key(command)
681
+ if not command_key:
682
+ return
683
+ generation = self.verification_relevant_edit_generation
684
+ if generation == 0:
685
+ if report.usable_as_baseline:
686
+ record = BaselineRecord(
687
+ command=str(command),
688
+ command_key=command_key,
689
+ report=report,
690
+ edit_generation=0,
691
+ timestamp=timestamp,
692
+ )
693
+ self.test_baselines[command_key] = record
694
+ self.pending_regression_capture_events.append(
695
+ {
696
+ "kind": "baseline",
697
+ "command": str(command),
698
+ "command_key": command_key,
699
+ "edit_generation": 0,
700
+ "report": report.as_payload(),
701
+ }
702
+ )
703
+ else:
704
+ self.pending_regression_capture_events.append(
705
+ {
706
+ "kind": "baseline_unusable",
707
+ "command": str(command),
708
+ "command_key": command_key,
709
+ "edit_generation": 0,
710
+ "report": report.as_payload(),
711
+ }
712
+ )
713
+ return
714
+ run = PostEditTestRun(
715
+ command=str(command),
716
+ command_key=command_key,
717
+ report=report,
718
+ generation=generation,
719
+ )
720
+ self.post_edit_test_runs.append(run)
721
+ self.post_edit_test_runs[:] = self.post_edit_test_runs[-40:]
722
+ self.pending_regression_capture_events.append(
723
+ {
724
+ "kind": "post_edit",
725
+ "command": str(command),
726
+ "command_key": command_key,
727
+ "generation": generation,
728
+ "report": report.as_payload(),
729
+ }
730
+ )
731
+
732
+ def current_post_edit_test_runs(self) -> list[PostEditTestRun]:
733
+ """Post-edit runs recorded after the last verification-relevant edit."""
734
+ return [
735
+ run
736
+ for run in self.post_edit_test_runs
737
+ if run.generation == self.verification_relevant_edit_generation
738
+ ]
739
+
740
+ def compute_regression_diff(self, *, enabled: bool) -> RegressionDiffResult:
741
+ """Aggregate the same-command diff over current post-edit runs.
742
+
743
+ Pure attribution: each current-generation post-edit run is compared only
744
+ against a baseline of the same normalized command. With ``enabled`` off,
745
+ returns the empty diff (legacy gate policy).
746
+ """
747
+ if not enabled:
748
+ self.latest_regression_diff = {}
749
+ return EMPTY_REGRESSION_DIFF
750
+ results = [
751
+ classify_regression_diff(
752
+ post_report=run.report,
753
+ baseline=self.test_baselines.get(run.command_key),
754
+ agent_created_paths=self.agent_created_paths,
755
+ )
756
+ for run in self.current_post_edit_test_runs()
757
+ ]
758
+ aggregate = aggregate_regression_results(results)
759
+ self.latest_regression_diff = aggregate.as_payload()
760
+ return aggregate
761
+
762
+ def note_post_edit_run_output(self, *, command: str, output: str, generation: int) -> None:
763
+ """Record a bounded post-edit run output for expectation evidence linking.
764
+
765
+ Only runs after a verification-relevant edit (generation > 0) are captured;
766
+ each output is bounded and the buffer is capped, so evidence matching stays
767
+ cheap and never balloons a long turn's state.
768
+ """
769
+ text = str(output or "")
770
+ if not text:
771
+ return
772
+ self.post_edit_run_outputs.append(
773
+ {
774
+ "normalized_command": _normalize_shell_command_for_match(str(command or "")),
775
+ "output": text[:8000],
776
+ "generation": int(generation),
777
+ }
778
+ )
779
+ self.post_edit_run_outputs[:] = self.post_edit_run_outputs[-30:]
780
+
781
+ def current_expectation_evidence(
782
+ self, expectations: list[Expectation]
783
+ ) -> list[ExpectationEvidence]:
784
+ """Link expected-output literals to post-edit runs at the current generation."""
785
+ generation = self.verification_relevant_edit_generation
786
+ runs = [
787
+ run
788
+ for run in self.post_edit_run_outputs
789
+ if int(run.get("generation") or 0) >= generation
790
+ ]
791
+ return match_expectation_evidence(expectations, runs)
792
+
793
+ def note_repro_run(
794
+ self,
795
+ *,
796
+ command: str,
797
+ artifact_paths: tuple[str, ...],
798
+ exit_code: int | None,
799
+ passed: bool,
800
+ ) -> None:
801
+ """Record one observed execution of an agent-created artifact.
802
+
803
+ The phase is resolved *at record time* against the paths the agent created
804
+ this turn, so a run is pre-fix exactly when no pre-existing repo path has
805
+ been modified yet. Writing the repro is itself a material edit, which is
806
+ why the edit generation cannot decide this.
807
+ """
808
+ if not artifact_paths:
809
+ return
810
+ phase, product_paths = classify_repro_phase(
811
+ touched_repo_paths=self.touched_repo_paths,
812
+ created_paths=self.agent_created_paths,
813
+ )
814
+ self.repro_artifact_paths.update(artifact_paths)
815
+ if len(self.repro_artifact_paths) > MAX_REPRO_ARTIFACTS:
816
+ self.repro_artifact_paths = set(sorted(self.repro_artifact_paths)[:MAX_REPRO_ARTIFACTS])
817
+ run = ReproRun(
818
+ command=str(command or ""),
819
+ artifact_paths=tuple(artifact_paths),
820
+ phase=phase,
821
+ passed=bool(passed),
822
+ exit_code=exit_code,
823
+ product_paths=product_paths,
824
+ )
825
+ self.repro_runs.append(run)
826
+ self.repro_runs[:] = self.repro_runs[-MAX_REPRO_RUNS:]
827
+ self.pending_repro_run_events.append(run.as_payload())
828
+
829
+ def note_blast_radius_run(
830
+ self,
831
+ *,
832
+ command: str,
833
+ report: Any,
834
+ duration_seconds: float | None = None,
835
+ ) -> None:
836
+ """Record one observed test run for the blast-radius diff (step 6).
837
+
838
+ The phase is resolved *at record time* from the edits recorded so far, so a
839
+ run counts as a clean-tree baseline exactly when no pre-existing repo path
840
+ had been modified when it ran. A run made afterwards is never graced into a
841
+ baseline: by then the agent may already have finished its fix, and crediting
842
+ it would mask the very breakage this step exists to catch.
843
+ """
844
+ cleaned = str(command or "").strip()
845
+ if not cleaned:
846
+ return
847
+ phase = classify_scope_phase(
848
+ touched_repo_paths=self.touched_repo_paths,
849
+ created_paths=self.agent_created_paths,
850
+ )
851
+ run = ScopeRun(
852
+ command=cleaned,
853
+ selectors=command_path_selectors(cleaned),
854
+ phase=phase,
855
+ report=report,
856
+ duration_seconds=duration_seconds,
857
+ )
858
+ self.blast_radius_runs.append(run)
859
+ self.blast_radius_runs[:] = self.blast_radius_runs[-MAX_SCOPE_RUNS:]
860
+ self.pending_blast_radius_events.append(run.as_payload())
861
+
862
+ def has_blast_radius_baseline(self) -> bool:
863
+ """True when a usable clean-tree run already covers the selected scope."""
864
+ paths = self.blast_radius_scope.paths
865
+ if not paths:
866
+ return False
867
+ return any(
868
+ run.phase == ScopePhase.BASELINE and run.usable and run.covers(paths)
869
+ for run in self.blast_radius_runs
870
+ )
871
+
872
+ def compute_blast_radius_assessment(
873
+ self, *, enabled: bool, turn_intent: str
874
+ ) -> BlastRadiusAssessment:
875
+ """Assess the blast-radius gate mechanically (step 6).
876
+
877
+ With the feature disabled, on a non-execute turn, or with no scope selected
878
+ (nothing edited yet, or no test surface near the change), returns the empty
879
+ (non-applicable) assessment — the gate then behaves exactly as it did before
880
+ this step.
881
+ """
882
+ applicable = bool(enabled and str(turn_intent or "") == "execute")
883
+ assessment = assess_blast_radius(
884
+ scope=self.blast_radius_scope,
885
+ runs=self.blast_radius_runs,
886
+ applicable=applicable,
887
+ policy=self.blast_radius_policy,
888
+ agent_created_paths=self.agent_created_paths,
889
+ )
890
+ self.latest_blast_radius_assessment = (
891
+ assessment.as_payload() if assessment.applicable else {}
892
+ )
893
+ return assessment
894
+
895
+ def note_repro_revision_round(self) -> None:
896
+ self.repro_revision_rounds += 1
897
+
898
+ def note_repro_artifact_edited_after_fix(self, paths: set[str] | tuple[str, ...]) -> None:
899
+ for path in paths:
900
+ cleaned = str(path or "").strip()
901
+ if cleaned:
902
+ self.repro_artifacts_edited_after_fix.add(cleaned)
903
+
904
+ def repro_protocol_applicable(
905
+ self, *, enabled: bool, turn_intent: str, engagement_based: bool = False
906
+ ) -> bool:
907
+ if not enabled or str(turn_intent or "") != "execute":
908
+ return False
909
+ if self.repro_task_shape == TaskShape.BUG_FIX:
910
+ return True
911
+ if not engagement_based:
912
+ return False
913
+ # Router-free path: no pre-turn task-shape prediction exists. The
914
+ # protocol binds exactly when the agent demonstrably reproduced a
915
+ # failure on the unpatched tree — from then on "the same repro must
916
+ # pass after the fix" is enforceable without interpreting language.
917
+ # Helper scripts that only ever passed never engage the gate.
918
+ return any(run.phase is ReproPhase.PRE_FIX and not run.passed for run in self.repro_runs)
919
+
920
+ def compute_repro_assessment(
921
+ self, *, enabled: bool, turn_intent: str, engagement_based: bool = False
922
+ ) -> ReproAssessment:
923
+ """Assess the reproduction protocol mechanically (step 5).
924
+
925
+ With the feature disabled, on a non-execute turn, or on a task that reports
926
+ no symptom, returns the empty (non-applicable) assessment — the gate then
927
+ behaves exactly as it did before this step.
928
+ """
929
+ applicable = self.repro_protocol_applicable(
930
+ enabled=enabled,
931
+ turn_intent=turn_intent,
932
+ engagement_based=engagement_based,
933
+ )
934
+ if not applicable:
935
+ self.latest_repro_assessment = {}
936
+ return ReproAssessment()
937
+ assessment = assess_reproduction(
938
+ runs=self.repro_runs,
939
+ applicable=True,
940
+ artifact_paths=self.repro_artifact_paths,
941
+ revision_rounds=self.repro_revision_rounds,
942
+ edited_after_fix=sorted(self.repro_artifacts_edited_after_fix),
943
+ surviving_artifacts=self.repro_surviving_artifacts,
944
+ )
945
+ self.latest_repro_assessment = assessment.as_payload()
946
+ return assessment
947
+
948
+ def compute_expectation_assessment(
949
+ self, *, enabled: bool, turn_intent: str
950
+ ) -> ExpectationAssessment:
951
+ """Assess task expectations mechanically at the gate (turn-contract v2).
952
+
953
+ Confirmed = an expected-output literal observed in a post-edit run, or a
954
+ named locus that was edited, or an explicit recorded disposition; the rest
955
+ are unaddressed. With the feature disabled, on a non-execute turn, or when
956
+ the contract names no expectations, returns the empty assessment.
957
+ """
958
+ contract = self.acceptance_contract
959
+ expectations = list(contract.expectations) if contract is not None else []
960
+ if not enabled or str(turn_intent or "") != "execute" or not expectations:
961
+ self.latest_expectation_assessment = {}
962
+ self.latest_expectation_evidence = []
963
+ return ExpectationAssessment()
964
+ evidence = self.current_expectation_evidence(expectations)
965
+ assessment = assess_expectations(
966
+ expectations=expectations,
967
+ evidence=evidence,
968
+ edited_loci=self.touched_repo_paths,
969
+ dispositions=self.recorded_expectation_dispositions,
970
+ )
971
+ self.latest_expectation_assessment = assessment.as_payload()
972
+ self.latest_expectation_evidence = [item.as_payload() for item in evidence]
973
+ return assessment
974
+
975
+ def missing_verification_commands(self) -> set[str]:
976
+ return self.expected_verification_commands - self.covered_verification_commands
977
+
978
+ def failed_verification_commands(self) -> set[str]:
979
+ return set(self.failed_verification_command_snippets) & self.expected_verification_commands
980
+
981
+ def first_failed_verification_snippet(self) -> str:
982
+ for command in sorted(self.failed_verification_commands()):
983
+ snippet = self.failed_verification_command_snippets.get(command, "")
984
+ if snippet:
985
+ return snippet
986
+ return ""
987
+
988
+ def verification_coverage_is_stale(self) -> bool:
989
+ return (
990
+ bool(self.expected_verification_commands)
991
+ and self.last_successful_verification_generation is not None
992
+ and self.last_successful_verification_generation
993
+ < self.verification_relevant_edit_generation
994
+ )
995
+
996
+ def repair_attempts_for_stage(self, stage: str) -> int:
997
+ if stage == "no_material_edits":
998
+ return self.completion_gate_no_material_edits_repair_attempts
999
+ if stage == "verification_not_attempted":
1000
+ return self.completion_gate_missing_verify_repair_attempts
1001
+ if stage == "verification_incomplete":
1002
+ return self.completion_gate_missing_verify_repair_attempts
1003
+ if stage == "verification_failed":
1004
+ return self.completion_gate_failed_verify_repair_attempts
1005
+ if stage == "regressions_detected":
1006
+ return self.completion_gate_regression_repair_attempts
1007
+ if stage == "unattributed_failures":
1008
+ return self.completion_gate_unattributed_repair_attempts
1009
+ if stage == "expectations_unaddressed":
1010
+ return self.completion_gate_expectations_repair_attempts
1011
+ if stage == "repro_unconfirmed":
1012
+ return self.completion_gate_repro_repair_attempts
1013
+ # Both blast-radius stages share one repair budget: they are two faces of the
1014
+ # same protocol, and a run that alternates between them must not get double
1015
+ # the rounds.
1016
+ if stage in {"blast_radius_regressions", "blast_radius_unverified"}:
1017
+ return self.completion_gate_blast_radius_repair_attempts
1018
+ return self.completion_gate_repair_attempts
1019
+
1020
+ def increment_repair_attempts_for_stage(self, stage: str) -> None:
1021
+ self.completion_gate_repair_attempts += 1
1022
+ if stage == "no_material_edits":
1023
+ self.completion_gate_no_material_edits_repair_attempts += 1
1024
+ elif stage == "verification_not_attempted":
1025
+ self.completion_gate_missing_verify_repair_attempts += 1
1026
+ elif stage == "verification_incomplete":
1027
+ self.completion_gate_missing_verify_repair_attempts += 1
1028
+ elif stage == "verification_failed":
1029
+ self.completion_gate_failed_verify_repair_attempts += 1
1030
+ elif stage == "regressions_detected":
1031
+ self.completion_gate_regression_repair_attempts += 1
1032
+ elif stage == "unattributed_failures":
1033
+ self.completion_gate_unattributed_repair_attempts += 1
1034
+ elif stage == "expectations_unaddressed":
1035
+ self.completion_gate_expectations_repair_attempts += 1
1036
+ elif stage == "repro_unconfirmed":
1037
+ self.completion_gate_repro_repair_attempts += 1
1038
+ elif stage in {"blast_radius_regressions", "blast_radius_unverified"}:
1039
+ self.completion_gate_blast_radius_repair_attempts += 1
1040
+
1041
+ def as_payload(self) -> dict[str, Any]:
1042
+ return {
1043
+ "execution_requested": self.execution_requested,
1044
+ "expected_verification_commands": sorted(self.expected_verification_commands),
1045
+ "covered_verification_commands": sorted(self.covered_verification_commands),
1046
+ "missing_verification_commands": sorted(self.missing_verification_commands()),
1047
+ "material_edit_count": self.material_edit_count,
1048
+ "material_edit_generation": self.material_edit_generation,
1049
+ "material_edit_tools": sorted(self.material_edit_tools),
1050
+ "touched_repo_paths": sorted(self.touched_repo_paths),
1051
+ "last_diff_review_generation": self.last_diff_review_generation,
1052
+ "diff_review_stale": self.diff_review_is_stale(),
1053
+ "verification_attempt_count": self.verification_attempt_count,
1054
+ "verification_tools": sorted(self.verification_tools),
1055
+ "last_verification_passed": self.last_verification_passed,
1056
+ "last_verification_failure_snippet": self.last_verification_failure_snippet,
1057
+ "last_verification_failure_category": self.last_verification_failure_category,
1058
+ "failed_verification_commands": sorted(self.failed_verification_commands()),
1059
+ "verification_relevant_edit_generation": self.verification_relevant_edit_generation,
1060
+ "last_successful_verification_generation": self.last_successful_verification_generation,
1061
+ "verification_coverage_stale": self.verification_coverage_is_stale(),
1062
+ "verification_evidence_counts": dict(sorted(self.verification_evidence_counts.items())),
1063
+ "latest_verification_evidence_category": (self.latest_verification_evidence_category),
1064
+ "latest_verification_evidence_reason": self.latest_verification_evidence_reason,
1065
+ "accepted_verification_evidence": list(self.accepted_verification_evidence),
1066
+ "supplemental_verification_evidence": list(self.supplemental_verification_evidence),
1067
+ "rejected_verification_evidence": list(self.rejected_verification_evidence),
1068
+ "executed_verification_evidence": list(self.executed_verification_evidence),
1069
+ "verification_evidence_generation": self.verification_evidence_generation,
1070
+ "last_post_edit_execution_generation": self.last_post_edit_execution_generation,
1071
+ "post_edit_execution_evidence_present": self.has_post_edit_execution_evidence(),
1072
+ "completion_gate_repair_attempts": self.completion_gate_repair_attempts,
1073
+ "completion_gate_no_material_edits_repair_attempts": self.completion_gate_no_material_edits_repair_attempts,
1074
+ "completion_gate_missing_verify_repair_attempts": self.completion_gate_missing_verify_repair_attempts,
1075
+ "completion_gate_failed_verify_repair_attempts": self.completion_gate_failed_verify_repair_attempts,
1076
+ "completion_gate_regression_repair_attempts": self.completion_gate_regression_repair_attempts,
1077
+ "completion_gate_unattributed_repair_attempts": self.completion_gate_unattributed_repair_attempts,
1078
+ "completion_gate_expectations_repair_attempts": self.completion_gate_expectations_repair_attempts,
1079
+ "completion_gate_repro_repair_attempts": self.completion_gate_repro_repair_attempts,
1080
+ "test_baselines": {
1081
+ key: record.as_payload() for key, record in sorted(self.test_baselines.items())
1082
+ },
1083
+ "post_edit_test_runs": [run.as_payload() for run in self.post_edit_test_runs],
1084
+ "agent_created_paths": sorted(self.agent_created_paths),
1085
+ "last_verification_attempt_was_test_run": (self.last_verification_attempt_was_test_run),
1086
+ "regression_diff": dict(self.latest_regression_diff),
1087
+ "expectation_assessment": dict(self.latest_expectation_assessment),
1088
+ "expectation_evidence": list(self.latest_expectation_evidence),
1089
+ "repro_task_shape": self.repro_task_shape.value,
1090
+ "repro_runs": [run.as_payload() for run in self.repro_runs],
1091
+ "repro_artifact_paths": sorted(self.repro_artifact_paths),
1092
+ "repro_revision_rounds": self.repro_revision_rounds,
1093
+ "repro_artifacts_edited_after_fix": sorted(self.repro_artifacts_edited_after_fix),
1094
+ "repro_surviving_artifacts": list(self.repro_surviving_artifacts),
1095
+ "repro_assessment": dict(self.latest_repro_assessment),
1096
+ "completion_gate_blast_radius_repair_attempts": (
1097
+ self.completion_gate_blast_radius_repair_attempts
1098
+ ),
1099
+ "blast_radius_scope": self.blast_radius_scope.as_payload(),
1100
+ "blast_radius_runs": [run.as_payload() for run in self.blast_radius_runs],
1101
+ "blast_radius_assessment": dict(self.latest_blast_radius_assessment),
1102
+ "advisory_completion": (
1103
+ self.recorded_advisory_completion.as_payload()
1104
+ if self.recorded_advisory_completion is not None
1105
+ else None
1106
+ ),
1107
+ "completion_gate_controller": self.completion_gate_controller_state.as_payload(),
1108
+ "completion_gate_last_decision_kind": self.completion_gate_controller_state.last_decision_kind,
1109
+ "completion_certificate": dict(self.latest_completion_certificate),
1110
+ **acceptance_contract_problem_payload(self.acceptance_contract),
1111
+ }
1112
+
1113
+ def acceptance_problem_names(self) -> list[str]:
1114
+ if self.acceptance_contract is None:
1115
+ return []
1116
+ return self.acceptance_contract.problem_names()
1117
+
1118
+ def acceptance_requires_execution(self) -> bool:
1119
+ if self.acceptance_contract is None:
1120
+ return False
1121
+ return any(
1122
+ criterion.required_for_finalization
1123
+ and criterion.required
1124
+ and (
1125
+ criterion.commands
1126
+ or criterion.thresholds
1127
+ or criterion.ports
1128
+ or criterion.kind.value
1129
+ in {
1130
+ "explicit_command_io",
1131
+ "functional_api_protocol",
1132
+ "persistent_service",
1133
+ "explicit_host_user_verification_command",
1134
+ }
1135
+ )
1136
+ for criterion in self.acceptance_contract.criteria
1137
+ )
1138
+
1139
+
1140
+ def _successful_verification_claim_kind(final_text: str) -> str | None:
1141
+ text = str(final_text or "")
1142
+ for kind, pattern in (
1143
+ ("tests", _TEST_SUCCESS_CLAIM_RE),
1144
+ ("verification", _GENERIC_VERIFICATION_SUCCESS_CLAIM_RE),
1145
+ ):
1146
+ for match in pattern.finditer(text):
1147
+ prefix = text[max(0, match.start() - 48) : match.start()]
1148
+ if _NEGATED_CLAIM_PREFIX_RE.search(prefix):
1149
+ continue
1150
+ return kind
1151
+ return None
1152
+
1153
+
1154
+ def _fresh_executed_evidence_for_claim(
1155
+ state: TurnExecutionState,
1156
+ *,
1157
+ claim_kind: str,
1158
+ ) -> list[dict[str, Any]]:
1159
+ required_generation = state.verification_relevant_edit_generation
1160
+ evidence: list[dict[str, Any]] = []
1161
+ for raw_item in state.executed_verification_evidence:
1162
+ if not isinstance(raw_item, dict):
1163
+ continue
1164
+ item = dict(raw_item)
1165
+ try:
1166
+ generation = int(item.get("generation"))
1167
+ except (TypeError, ValueError):
1168
+ continue
1169
+ if generation < required_generation:
1170
+ continue
1171
+ if item.get("real_execution") is not True:
1172
+ continue
1173
+ if item.get("reason") == "mutated_material_paths":
1174
+ continue
1175
+ if item.get("observed_exit_code") != 0 or item.get("observed_output") is not True:
1176
+ continue
1177
+ command = str(item.get("normalized_command") or "").strip()
1178
+ if not command:
1179
+ continue
1180
+ if claim_kind == "tests":
1181
+ analysis = analyze_verification_command(command, trusted=True)
1182
+ family = str(analysis.command_family or "").casefold()
1183
+ if "test" not in family and _TEST_EXECUTION_COMMAND_RE.search(command) is None:
1184
+ continue
1185
+ evidence.append(item)
1186
+ return evidence
1187
+
1188
+
1189
+ def _runtime_message_locale(
1190
+ *,
1191
+ language: str = "",
1192
+ explicit_language_override: bool = False,
1193
+ ) -> str:
1194
+ if not explicit_language_override:
1195
+ return _RUNTIME_DEFAULT_LANGUAGE
1196
+ normalized = normalize_language_name(language).casefold()
1197
+ if normalized in _RUNTIME_MESSAGE_CATALOG:
1198
+ return normalized
1199
+ return _RUNTIME_DEFAULT_LANGUAGE
1200
+
1201
+
1202
+ def _runtime_message(
1203
+ key: str,
1204
+ *,
1205
+ language: str = "",
1206
+ explicit_language_override: bool = False,
1207
+ **kwargs: Any,
1208
+ ) -> str:
1209
+ locale = _runtime_message_locale(
1210
+ language=language,
1211
+ explicit_language_override=explicit_language_override,
1212
+ )
1213
+ template = _RUNTIME_MESSAGE_CATALOG.get(locale, {}).get(key)
1214
+ if template is None:
1215
+ template = _RUNTIME_MESSAGE_CATALOG[_RUNTIME_DEFAULT_LANGUAGE].get(key, key)
1216
+ try:
1217
+ return template.format(**kwargs)
1218
+ except Exception: # noqa: BLE001
1219
+ return template
1220
+
1221
+
1222
+ def _extract_touched_repo_paths(
1223
+ *,
1224
+ root: Path,
1225
+ tool_name: str,
1226
+ arguments: dict[str, Any],
1227
+ result: dict[str, Any],
1228
+ ) -> set[str]:
1229
+ normalized_tool = tool_name.strip().lower()
1230
+ raw_paths: list[str] = []
1231
+
1232
+ if normalized_tool in {"fs_write", "fs_edit", "fs_delete", "fs_mkdir"}:
1233
+ raw_path = result.get("path", arguments.get("path"))
1234
+ if isinstance(raw_path, str):
1235
+ raw_paths.append(raw_path)
1236
+ elif normalized_tool in {"fs_move", "fs_copy"}:
1237
+ for key in ("source_path", "destination_path"):
1238
+ raw_path = result.get(key, arguments.get(key))
1239
+ if isinstance(raw_path, str):
1240
+ raw_paths.append(raw_path)
1241
+ elif normalized_tool == "git_apply_patch":
1242
+ patch = str(arguments.get("patch") or "")
1243
+ raw_paths.extend(iter_patch_paths(patch))
1244
+ elif normalized_tool == "subagent_run":
1245
+ touched_paths = result.get(
1246
+ "material_touched_repo_paths",
1247
+ result.get("touched_repo_paths"),
1248
+ )
1249
+ if isinstance(touched_paths, list):
1250
+ raw_paths.extend(str(item) for item in touched_paths if isinstance(item, str))
1251
+ elif normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
1252
+ touched_paths = result.get("touched_repo_paths")
1253
+ if isinstance(touched_paths, list):
1254
+ raw_paths.extend(str(item) for item in touched_paths if isinstance(item, str))
1255
+
1256
+ touched: set[str] = set()
1257
+ for raw_path in raw_paths:
1258
+ normalized = _normalize_repo_relative_hint_path(root=root, raw=raw_path)
1259
+ if normalized:
1260
+ touched.add(normalized)
1261
+ if normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
1262
+ return set(material_mutation_paths(touched, root=root))
1263
+ return touched
1264
+
1265
+
1266
+ def _verification_attempt_passed(
1267
+ *,
1268
+ tool_name: str,
1269
+ status: str,
1270
+ result: dict[str, Any],
1271
+ evidence: VerificationEvidence | None = None,
1272
+ ) -> bool:
1273
+ if status == "failed":
1274
+ return False
1275
+ if evidence is not None and not evidence.allowed_to_satisfy_contract:
1276
+ return False
1277
+ normalized_tool = tool_name.strip().lower()
1278
+ touched_repo_paths = result.get("material_touched_repo_paths", result.get("touched_repo_paths"))
1279
+ normalized_touched = (
1280
+ {str(item) for item in touched_repo_paths if isinstance(item, str) and str(item).strip()}
1281
+ if isinstance(touched_repo_paths, list)
1282
+ else set()
1283
+ )
1284
+ if normalized_tool == "verify_run":
1285
+ if normalized_touched and _paths_require_verification(normalized_touched):
1286
+ return False
1287
+ all_passed = result.get("all_passed")
1288
+ if isinstance(all_passed, bool):
1289
+ return all_passed
1290
+ command_results = result.get("command_results")
1291
+ if isinstance(command_results, list):
1292
+ checks: list[bool] = []
1293
+ for item in command_results:
1294
+ if not isinstance(item, dict):
1295
+ checks.append(False)
1296
+ continue
1297
+ real_execution = item.get("real_execution")
1298
+ if real_execution is not True:
1299
+ checks.append(False)
1300
+ continue
1301
+ ok = item.get("ok")
1302
+ if isinstance(ok, bool):
1303
+ checks.append(ok)
1304
+ continue
1305
+ exit_code = item.get("exit_code")
1306
+ checks.append(isinstance(exit_code, int) and exit_code == 0)
1307
+ return bool(checks) and all(checks)
1308
+ return False
1309
+ if normalized_tool == "shell_run":
1310
+ exit_code = result.get("exit_code")
1311
+ if not (isinstance(exit_code, int) and exit_code == 0):
1312
+ return False
1313
+ if normalized_touched and _paths_require_verification(normalized_touched):
1314
+ return False
1315
+ output = "\n".join(
1316
+ [
1317
+ str(result.get("stdout") or "").strip(),
1318
+ str(result.get("stderr") or "").strip(),
1319
+ ]
1320
+ ).strip()
1321
+ assessment = assess_verification_command_execution(
1322
+ command=str(result.get("effective_cmd") or result.get("cmd") or ""),
1323
+ exit_code=exit_code,
1324
+ output=output,
1325
+ )
1326
+ return assessment.real_execution is True
1327
+ return False
1328
+
1329
+
1330
+ def _verification_relevant_material_paths(paths: set[str]) -> set[str]:
1331
+ if not paths or not _paths_require_verification(paths):
1332
+ return set()
1333
+ return set(paths)
1334
+
1335
+
1336
+ def _verification_command_result_is_benign_skip(item: dict[str, Any]) -> bool:
1337
+ return (
1338
+ item.get("status") == "skipped"
1339
+ and item.get("ok") is True
1340
+ and is_benign_non_execution_reason(str(item.get("non_execution_reason") or ""))
1341
+ )
1342
+
1343
+
1344
+ def _verification_command_result_passed(item: dict[str, Any]) -> bool:
1345
+ if _verification_command_result_is_benign_skip(item):
1346
+ return True
1347
+ real_execution = item.get("real_execution")
1348
+ if real_execution is not True:
1349
+ return False
1350
+ ok = item.get("ok")
1351
+ if isinstance(ok, bool):
1352
+ return ok
1353
+ exit_code = item.get("exit_code")
1354
+ return isinstance(exit_code, int) and exit_code == 0
1355
+
1356
+
1357
+ def _verification_command_result_snippet(item: dict[str, Any]) -> str:
1358
+ parts = [
1359
+ str(item.get("output_preview") or "").strip(),
1360
+ str(item.get("output") or "").strip(),
1361
+ str(item.get("stderr") or "").strip(),
1362
+ str(item.get("stdout") or "").strip(),
1363
+ ]
1364
+ text = "\n".join(part for part in parts if part)
1365
+ snippet = extract_actionable_failure_snippet(text)
1366
+ return snippet or (text[:240].rstrip() if text else "")
1367
+
1368
+
1369
+ def _verification_failure_category_for_tool_result(
1370
+ *,
1371
+ tool_name: str,
1372
+ arguments: dict[str, Any],
1373
+ result: dict[str, Any],
1374
+ ) -> str:
1375
+ normalized_tool = tool_name.strip().lower()
1376
+ if normalized_tool == "verify_run":
1377
+ category = str(result.get("failure_category") or "").strip()
1378
+ return category or FailureCategory.VERIFICATION_FAILED.value
1379
+
1380
+ if normalized_tool == "shell_run":
1381
+ output = "\n".join(
1382
+ [
1383
+ str(result.get("stdout") or "").strip(),
1384
+ str(result.get("stderr") or "").strip(),
1385
+ ]
1386
+ ).strip()
1387
+ command = str(
1388
+ result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
1389
+ )
1390
+ exit_code_raw = result.get("exit_code")
1391
+ exit_code = exit_code_raw if isinstance(exit_code_raw, int) else 1
1392
+ assessment = assess_verification_command_execution(
1393
+ command=command,
1394
+ exit_code=exit_code,
1395
+ output=output,
1396
+ )
1397
+ if (
1398
+ assessment.non_execution_reason == "execution_layer_failure"
1399
+ or is_infra_unavailable_error(output)
1400
+ or is_toolchain_unavailable_verification_output(output)
1401
+ ):
1402
+ return FailureCategory.INFRA_UNAVAILABLE.value
1403
+
1404
+ return FailureCategory.VERIFICATION_FAILED.value
1405
+
1406
+
1407
+ def _record_verify_run_command_outcomes(
1408
+ *,
1409
+ state: TurnExecutionState,
1410
+ result: dict[str, Any],
1411
+ known_verification_commands: list[str] | None,
1412
+ ) -> None:
1413
+ command_results = result.get("command_results")
1414
+ if not isinstance(command_results, list):
1415
+ if result.get("all_passed") is True:
1416
+ commands = result.get("commands")
1417
+ if isinstance(commands, list):
1418
+ covered: set[str] = set()
1419
+ for command in commands:
1420
+ covered.update(
1421
+ _matching_effective_verification_commands(
1422
+ observed_command=str(command),
1423
+ effective_verification_commands=known_verification_commands,
1424
+ )
1425
+ )
1426
+ state.record_verification_coverage(covered)
1427
+ return
1428
+
1429
+ covered: set[str] = set()
1430
+ failures: dict[str, str] = {}
1431
+ for item in command_results:
1432
+ if not isinstance(item, dict):
1433
+ continue
1434
+ matches: set[str] = set()
1435
+ observed_candidates = [
1436
+ str(item.get("command") or ""),
1437
+ str(item.get("effective_command") or ""),
1438
+ ]
1439
+ for observed in observed_candidates:
1440
+ if not observed:
1441
+ continue
1442
+ matches.update(
1443
+ _matching_effective_verification_commands(
1444
+ observed_command=observed,
1445
+ effective_verification_commands=known_verification_commands,
1446
+ )
1447
+ )
1448
+ if not matches:
1449
+ continue
1450
+ if _verification_command_result_passed(item):
1451
+ covered.update(matches)
1452
+ continue
1453
+ snippet = _verification_command_result_snippet(item)
1454
+ for command in matches:
1455
+ failures[command] = f"{command}: {snippet}" if snippet else command
1456
+
1457
+ state.record_verification_coverage(covered)
1458
+ state.record_verification_failures(failures)
1459
+
1460
+
1461
+ def _record_shell_verification_command_outcome(
1462
+ *,
1463
+ state: TurnExecutionState,
1464
+ arguments: dict[str, Any],
1465
+ result: dict[str, Any],
1466
+ known_verification_commands: list[str] | None,
1467
+ passed: bool,
1468
+ evidence: VerificationEvidence | None = None,
1469
+ ) -> None:
1470
+ matches = (
1471
+ set(evidence.covered_verification_commands)
1472
+ if evidence is not None and evidence.allowed_to_satisfy_contract
1473
+ else set()
1474
+ )
1475
+ if not matches:
1476
+ matches = _matching_effective_verification_commands(
1477
+ observed_command=str(result.get("effective_cmd") or arguments.get("cmd") or ""),
1478
+ effective_verification_commands=known_verification_commands,
1479
+ )
1480
+ if not matches:
1481
+ return
1482
+ if passed:
1483
+ state.record_verification_coverage(matches)
1484
+ return
1485
+ output = "\n".join(
1486
+ [
1487
+ str(result.get("stdout") or "").strip(),
1488
+ str(result.get("stderr") or "").strip(),
1489
+ ]
1490
+ ).strip()
1491
+ snippet = extract_actionable_failure_snippet(output) or output[:240].rstrip()
1492
+ state.record_verification_failures(
1493
+ {command: f"{command}: {snippet}" if snippet else command for command in matches}
1494
+ )
1495
+
1496
+
1497
+ def _verification_output_text(result: dict[str, Any]) -> str:
1498
+ return "\n".join(
1499
+ [
1500
+ str(result.get("stdout") or "").strip(),
1501
+ str(result.get("stderr") or "").strip(),
1502
+ str(result.get("output") or "").strip(),
1503
+ str(result.get("output_preview") or "").strip(),
1504
+ ]
1505
+ ).strip()
1506
+
1507
+
1508
+ def _aggregate_verification_evidence(
1509
+ records: list[VerificationEvidence],
1510
+ *,
1511
+ fallback_command: str = "",
1512
+ ) -> VerificationEvidence:
1513
+ if not records:
1514
+ return VerificationEvidence(
1515
+ category=VerificationEvidenceCategory.NOT_VERIFICATION,
1516
+ normalized_command=fallback_command,
1517
+ reason="no_verification_evidence",
1518
+ )
1519
+ priority = {
1520
+ VerificationEvidenceCategory.AUTHORITATIVE: 0,
1521
+ VerificationEvidenceCategory.REPO_NATIVE: 1,
1522
+ VerificationEvidenceCategory.TASK_ACCEPTANCE: 2,
1523
+ VerificationEvidenceCategory.NOT_VERIFICATION: 3,
1524
+ }
1525
+ primary = sorted(records, key=lambda item: priority[item.category])[0]
1526
+ covered = sorted(
1527
+ {command for item in records for command in item.covered_verification_commands if command}
1528
+ )
1529
+ allowed = bool(records) and all(
1530
+ item.allowed_to_satisfy_contract
1531
+ for item in records
1532
+ if item.category != VerificationEvidenceCategory.NOT_VERIFICATION
1533
+ )
1534
+ if any(item.category == VerificationEvidenceCategory.NOT_VERIFICATION for item in records):
1535
+ allowed = False
1536
+ return VerificationEvidence(
1537
+ category=primary.category,
1538
+ normalized_command=primary.normalized_command,
1539
+ matched_command=primary.matched_command,
1540
+ real_execution=primary.real_execution,
1541
+ allowed_to_satisfy_contract=allowed,
1542
+ reason=primary.reason
1543
+ if allowed
1544
+ else next(
1545
+ (item.reason for item in records if not item.allowed_to_satisfy_contract),
1546
+ primary.reason,
1547
+ ),
1548
+ covered_verification_commands=tuple(covered),
1549
+ supplemental_only=all(item.supplemental_only for item in records),
1550
+ )
1551
+
1552
+
1553
+ def _verification_evidence_note(
1554
+ evidence: VerificationEvidence,
1555
+ *,
1556
+ result: dict[str, Any] | None = None,
1557
+ ) -> str:
1558
+ if evidence.category == VerificationEvidenceCategory.NOT_VERIFICATION:
1559
+ return ""
1560
+ if evidence.supplemental_only:
1561
+ return (
1562
+ "evidence origin: SELF_AUTHORED "
1563
+ "(supplemental - cannot independently confirm spec compliance)"
1564
+ )
1565
+ result_payload = result if isinstance(result, dict) else {}
1566
+ command_specs = result_payload.get("verification_command_specs")
1567
+ if isinstance(command_specs, list) and any(
1568
+ isinstance(item, dict) and item.get("provenance") == "PREEXISTING_REPO_NATIVE"
1569
+ for item in command_specs
1570
+ ):
1571
+ return "evidence origin: PREEXISTING_REPO_NATIVE (independent)"
1572
+ if result_payload.get("verification_contract_type") == "repo_native":
1573
+ return "evidence origin: PREEXISTING_REPO_NATIVE (independent)"
1574
+ if evidence.category == VerificationEvidenceCategory.AUTHORITATIVE:
1575
+ return "evidence origin: USER_EXPLICIT (independent)"
1576
+ if evidence.category == VerificationEvidenceCategory.REPO_NATIVE:
1577
+ return "evidence origin: PREEXISTING_REPO_NATIVE (independent)"
1578
+ if evidence.category == VerificationEvidenceCategory.TASK_ACCEPTANCE:
1579
+ return "evidence origin: DIRECT_BLACK_BOX (independent)"
1580
+ return ""
1581
+
1582
+
1583
+ def _verify_run_evidence_records(
1584
+ *,
1585
+ result: dict[str, Any],
1586
+ known_verification_commands: list[str] | None,
1587
+ verification_authoritative: bool,
1588
+ material_touched_paths: set[str],
1589
+ root: Path,
1590
+ evidence_v2: bool = True,
1591
+ ) -> list[VerificationEvidence]:
1592
+ command_results = result.get("command_results")
1593
+ verification_relevant_touched_paths = _verification_relevant_material_paths(
1594
+ material_touched_paths
1595
+ )
1596
+ records: list[VerificationEvidence] = []
1597
+ if isinstance(command_results, list):
1598
+ for item in command_results:
1599
+ if not isinstance(item, dict):
1600
+ continue
1601
+ command = str(item.get("command") or item.get("effective_command") or "")
1602
+ if not command:
1603
+ continue
1604
+ exit_code_raw = item.get("exit_code")
1605
+ exit_code = exit_code_raw if isinstance(exit_code_raw, int) else None
1606
+ record = classify_verification_evidence(
1607
+ command,
1608
+ known_verification_commands=known_verification_commands,
1609
+ authoritative=verification_authoritative,
1610
+ material_touched_paths=verification_relevant_touched_paths,
1611
+ exit_code=exit_code,
1612
+ output=_verification_output_text(item),
1613
+ real_execution=(
1614
+ item.get("real_execution")
1615
+ if isinstance(item.get("real_execution"), bool)
1616
+ or item.get("real_execution") is None
1617
+ else None
1618
+ ),
1619
+ root=root,
1620
+ evidence_v2=evidence_v2,
1621
+ )
1622
+ if (
1623
+ _verification_command_result_is_benign_skip(item)
1624
+ and record.category != VerificationEvidenceCategory.NOT_VERIFICATION
1625
+ and record.covered_verification_commands
1626
+ ):
1627
+ record = replace(
1628
+ record,
1629
+ allowed_to_satisfy_contract=True,
1630
+ reason=str(item.get("non_execution_reason") or "verification_skipped"),
1631
+ )
1632
+ records.append(record)
1633
+ return records
1634
+
1635
+ commands = result.get("commands")
1636
+ if isinstance(commands, list):
1637
+ all_passed = result.get("all_passed")
1638
+ exit_code = 0 if all_passed is True else 1 if all_passed is False else None
1639
+ for command in commands:
1640
+ records.append(
1641
+ classify_verification_evidence(
1642
+ str(command),
1643
+ known_verification_commands=known_verification_commands,
1644
+ authoritative=verification_authoritative,
1645
+ material_touched_paths=verification_relevant_touched_paths,
1646
+ exit_code=exit_code,
1647
+ output=_verification_output_text(result),
1648
+ root=root,
1649
+ evidence_v2=evidence_v2,
1650
+ )
1651
+ )
1652
+ return records
1653
+
1654
+
1655
+ def _shell_verification_evidence(
1656
+ *,
1657
+ root: Path,
1658
+ state: TurnExecutionState,
1659
+ arguments: dict[str, Any],
1660
+ result: dict[str, Any],
1661
+ known_verification_commands: list[str] | None,
1662
+ verification_authoritative: bool,
1663
+ material_touched_paths: set[str],
1664
+ evidence_v2: bool = True,
1665
+ ) -> VerificationEvidence:
1666
+ exit_code_raw = result.get("exit_code")
1667
+ exit_code = exit_code_raw if isinstance(exit_code_raw, int) else None
1668
+ command = str(result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or "")
1669
+ stage_status_raw = result.get("pipeline_stage_status")
1670
+ stage_status = (
1671
+ [int(item) for item in stage_status_raw]
1672
+ if isinstance(stage_status_raw, list)
1673
+ and all(isinstance(item, int) for item in stage_status_raw)
1674
+ else None
1675
+ )
1676
+ return classify_verification_evidence(
1677
+ command,
1678
+ known_verification_commands=known_verification_commands,
1679
+ authoritative=verification_authoritative,
1680
+ changed_paths=state.touched_repo_paths,
1681
+ material_touched_paths=_verification_relevant_material_paths(material_touched_paths),
1682
+ exit_code=exit_code,
1683
+ output=_verification_output_text(result),
1684
+ root=root,
1685
+ stage_status=stage_status,
1686
+ evidence_v2=evidence_v2,
1687
+ )
1688
+
1689
+
1690
+ def _verification_evidence_observation(
1691
+ *,
1692
+ tool_name: str,
1693
+ evidence: VerificationEvidence,
1694
+ result: dict[str, Any],
1695
+ ) -> tuple[int | None, bool]:
1696
+ def _has_output(payload: dict[str, Any]) -> bool:
1697
+ if any(
1698
+ str(payload.get(key) or "").strip()
1699
+ for key in ("output", "output_preview", "stdout", "stderr")
1700
+ ):
1701
+ return True
1702
+ output_chars = payload.get("output_chars")
1703
+ return isinstance(output_chars, int) and output_chars > 0
1704
+
1705
+ normalized_tool = tool_name.strip().casefold()
1706
+ if normalized_tool == "shell_run":
1707
+ exit_code = result.get("exit_code")
1708
+ return (
1709
+ exit_code if isinstance(exit_code, int) else None,
1710
+ _has_output(result),
1711
+ )
1712
+
1713
+ if normalized_tool == "verify_run":
1714
+ command_results = result.get("command_results")
1715
+ if isinstance(command_results, list):
1716
+ evidence_command = _normalize_shell_command_for_match(evidence.normalized_command)
1717
+ for raw_item in command_results:
1718
+ if not isinstance(raw_item, dict):
1719
+ continue
1720
+ command = str(raw_item.get("command") or raw_item.get("effective_command") or "")
1721
+ effective_command = str(
1722
+ raw_item.get("effective_command") or raw_item.get("command") or ""
1723
+ )
1724
+ normalized_candidates = {
1725
+ _normalize_shell_command_for_match(command),
1726
+ _normalize_shell_command_for_match(effective_command),
1727
+ }
1728
+ if evidence_command not in normalized_candidates:
1729
+ continue
1730
+ exit_code = raw_item.get("exit_code")
1731
+ return (
1732
+ exit_code if isinstance(exit_code, int) else None,
1733
+ _has_output(raw_item),
1734
+ )
1735
+ all_passed = result.get("all_passed")
1736
+ exit_code = 0 if all_passed is True else 1 if all_passed is False else None
1737
+ return (
1738
+ exit_code,
1739
+ _has_output(result),
1740
+ )
1741
+
1742
+ return None, False
1743
+
1744
+
1745
+ def _unmasked_shell_verification_command(command: str) -> str:
1746
+ candidate = str(command or "").strip()
1747
+ while match := _SAFE_LEADING_CD_RE.match(candidate):
1748
+ candidate = candidate[match.end() :].strip()
1749
+ if not candidate or _UNSAFE_CLAIM_EVIDENCE_SHELL_RE.search(candidate):
1750
+ return ""
1751
+ analysis_candidate = _SHELL_REDIRECTION_RE.sub("", candidate).strip()
1752
+ analysis = analyze_verification_command(analysis_candidate, trusted=True)
1753
+ if analysis.command_family is None:
1754
+ return ""
1755
+ return _normalize_shell_command_for_match(analysis_candidate)
1756
+
1757
+
1758
+ def _tool_effect_has_qualifying_execution(
1759
+ *,
1760
+ tool_name: str,
1761
+ evidence_records: list[VerificationEvidence],
1762
+ result: dict[str, Any],
1763
+ ) -> bool:
1764
+ """True when a real test/execution run (pass or fail) is observed.
1765
+
1766
+ Qualifying = a recognized test/execution program that actually executed the
1767
+ code — a passing run (``real_execution is True``) or a genuine failing run
1768
+ (a non-zero exit of a recognized test command). Excludes syntax-only and
1769
+ static checks (ast.parse, py_compile, mypy, ruff check) and non-executions
1770
+ (no-tests collected, vacuous commands). Used only for the ordering rule.
1771
+ """
1772
+ for record in evidence_records:
1773
+ if record.category == VerificationEvidenceCategory.NOT_VERIFICATION:
1774
+ continue
1775
+ if record.real_execution is False:
1776
+ continue
1777
+ if not command_is_qualifying_execution_evidence(record.normalized_command or ""):
1778
+ continue
1779
+ if record.real_execution is True:
1780
+ return True
1781
+ observed_exit_code, _observed_output = _verification_evidence_observation(
1782
+ tool_name=tool_name,
1783
+ evidence=record,
1784
+ result=result,
1785
+ )
1786
+ if observed_exit_code is not None and observed_exit_code != 0:
1787
+ return True
1788
+ return False
1789
+
1790
+
1791
+ def _regression_capture_timestamp() -> str:
1792
+ try:
1793
+ return datetime.now(timezone.utc).isoformat()
1794
+ except Exception: # noqa: BLE001 - a telemetry timestamp must never crash a turn
1795
+ return ""
1796
+
1797
+
1798
+ def _iter_executed_test_commands(
1799
+ *,
1800
+ tool_name: str,
1801
+ arguments: dict[str, Any],
1802
+ result: dict[str, Any],
1803
+ ) -> list[tuple[str, str]]:
1804
+ """Yield ``(command, output)`` pairs for executed test-runner commands.
1805
+
1806
+ Only commands whose meaningful first stage is pytest or unittest/Django are
1807
+ returned — the runners the parsers understand. Other qualifying executions
1808
+ (validation scripts, linters) emit no per-test ids and are out of scope.
1809
+ """
1810
+ pairs: list[tuple[str, str]] = []
1811
+ if tool_name == "shell_run":
1812
+ command = str(
1813
+ result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
1814
+ )
1815
+ if command and command_is_test_runner(command):
1816
+ pairs.append((command, _verification_output_text(result)))
1817
+ return pairs
1818
+ if tool_name == "verify_run":
1819
+ command_results = result.get("command_results")
1820
+ if isinstance(command_results, list):
1821
+ for item in command_results:
1822
+ if not isinstance(item, dict):
1823
+ continue
1824
+ command = str(item.get("command") or item.get("effective_command") or "")
1825
+ if command and command_is_test_runner(command):
1826
+ pairs.append((command, _verification_output_text(item)))
1827
+ return pairs
1828
+
1829
+
1830
+ def _iter_executed_commands_with_outcome(
1831
+ *,
1832
+ tool_name: str,
1833
+ arguments: dict[str, Any],
1834
+ status: str,
1835
+ result: dict[str, Any],
1836
+ ) -> list[tuple[str, int | None]]:
1837
+ """Yield ``(command, exit_code)`` for every command this tool actually ran.
1838
+
1839
+ ``exit_code`` is ``None`` when the runner reported none; a failed tool status
1840
+ with no exit code is reported as a non-zero sentinel so a crashed run is never
1841
+ mistaken for a passing one.
1842
+ """
1843
+
1844
+ def _exit_code(payload: dict[str, Any]) -> int | None:
1845
+ raw = payload.get("exit_code")
1846
+ if raw is None:
1847
+ raw = payload.get("returncode")
1848
+ if raw is None:
1849
+ return 1 if status == "failed" else None
1850
+ try:
1851
+ return int(raw)
1852
+ except (TypeError, ValueError):
1853
+ return 1 if status == "failed" else None
1854
+
1855
+ pairs: list[tuple[str, int | None]] = []
1856
+ if tool_name == "shell_run":
1857
+ command = str(
1858
+ result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
1859
+ )
1860
+ if command:
1861
+ pairs.append((command, _exit_code(result)))
1862
+ return pairs
1863
+ if tool_name == "verify_run":
1864
+ command_results = result.get("command_results")
1865
+ if isinstance(command_results, list):
1866
+ for item in command_results:
1867
+ if not isinstance(item, dict):
1868
+ continue
1869
+ command = str(item.get("command") or item.get("effective_command") or "")
1870
+ if command:
1871
+ pairs.append((command, _exit_code(item)))
1872
+ return pairs
1873
+
1874
+
1875
+ def _capture_repro_runs(
1876
+ *,
1877
+ state: TurnExecutionState,
1878
+ tool_name: str,
1879
+ arguments: dict[str, Any],
1880
+ status: str,
1881
+ result: dict[str, Any],
1882
+ ) -> None:
1883
+ """Record executions of files the agent created this turn (step 5).
1884
+
1885
+ A command "runs a reproduction" when one of its path tokens is a path the
1886
+ agent created this turn — a fact, not an inference about the file's purpose.
1887
+ A run with no resolvable exit code is not recorded at all: an unobservable
1888
+ outcome can neither confirm nor refute the reproduction.
1889
+ """
1890
+ if tool_name not in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
1891
+ return
1892
+ if not state.agent_created_paths:
1893
+ return
1894
+ for command, exit_code in _iter_executed_commands_with_outcome(
1895
+ tool_name=tool_name,
1896
+ arguments=arguments,
1897
+ status=status,
1898
+ result=result,
1899
+ ):
1900
+ artifacts = match_repro_artifacts(command, state.agent_created_paths)
1901
+ if not artifacts or exit_code is None:
1902
+ continue
1903
+ state.note_repro_run(
1904
+ command=command,
1905
+ artifact_paths=artifacts,
1906
+ exit_code=exit_code,
1907
+ passed=exit_code == 0,
1908
+ )
1909
+
1910
+
1911
+ def _capture_repro_artifact_edits(
1912
+ *,
1913
+ state: TurnExecutionState,
1914
+ touched_paths: set[str],
1915
+ ) -> None:
1916
+ """Record edits to a recorded reproduction artifact made after a product edit.
1917
+
1918
+ Called before ``touched_repo_paths`` absorbs this edit, so the "product code
1919
+ already changed" test reads only prior edits.
1920
+ """
1921
+ if not state.repro_artifact_paths or not touched_paths:
1922
+ return
1923
+ edited_artifacts = touched_paths & state.repro_artifact_paths
1924
+ if not edited_artifacts:
1925
+ return
1926
+ if not (state.touched_repo_paths - state.agent_created_paths):
1927
+ return
1928
+ state.note_repro_artifact_edited_after_fix(edited_artifacts)
1929
+
1930
+
1931
+ def _capture_regression_test_runs(
1932
+ *,
1933
+ state: TurnExecutionState,
1934
+ tool_name: str,
1935
+ arguments: dict[str, Any],
1936
+ result: dict[str, Any],
1937
+ elapsed_ms: int | None = None,
1938
+ ) -> None:
1939
+ timestamp = _regression_capture_timestamp()
1940
+ pairs = _iter_executed_test_commands(
1941
+ tool_name=tool_name,
1942
+ arguments=arguments,
1943
+ result=result,
1944
+ )
1945
+ # A tool call's elapsed time covers everything it ran, so attributing it to a
1946
+ # single command is only honest when that call ran exactly one test command.
1947
+ duration_seconds: float | None = None
1948
+ if elapsed_ms is not None and len(pairs) == 1:
1949
+ try:
1950
+ duration_seconds = max(0.0, float(elapsed_ms) / 1000.0)
1951
+ except (TypeError, ValueError):
1952
+ duration_seconds = None
1953
+ for command, output in pairs:
1954
+ report = parse_test_report(output)
1955
+ if tool_name == "verify_run":
1956
+ report = _structured_verify_test_report(
1957
+ result=result,
1958
+ command=command,
1959
+ parsed_report=report,
1960
+ )
1961
+ state.note_test_execution(command=command, report=report, timestamp=timestamp)
1962
+ # Blast radius (step 6) reads the same parsed reports but keys them by what
1963
+ # each run selected rather than by command identity, so a clean whole-suite
1964
+ # run can baseline a scope the agent never named.
1965
+ state.note_blast_radius_run(
1966
+ command=command,
1967
+ report=report,
1968
+ duration_seconds=duration_seconds,
1969
+ )
1970
+
1971
+
1972
+ def _structured_verify_test_report(
1973
+ *,
1974
+ result: dict[str, Any],
1975
+ command: str,
1976
+ parsed_report: TestReport,
1977
+ ) -> TestReport:
1978
+ """Prefer host-recorded verify success; raw output only adds parsed detail."""
1979
+ command_key = baseline_command_key(command)
1980
+ command_results = result.get("command_results")
1981
+ if not isinstance(command_results, list):
1982
+ return parsed_report
1983
+ for raw_item in command_results:
1984
+ if not isinstance(raw_item, dict):
1985
+ continue
1986
+ observed_command = str(raw_item.get("effective_command") or raw_item.get("command") or "")
1987
+ if baseline_command_key(observed_command) != command_key:
1988
+ continue
1989
+ exit_code = raw_item.get("exit_code")
1990
+ structured_passed = raw_item.get("ok") is True or (
1991
+ result.get("all_passed") is True and exit_code == 0
1992
+ )
1993
+ if not structured_passed or raw_item.get("real_execution") is False:
1994
+ return parsed_report
1995
+ runner = parsed_report.runner
1996
+ if runner == "unknown":
1997
+ lowered = observed_command.casefold()
1998
+ runner = "pytest" if "pytest" in lowered else "unittest"
1999
+ return TestReport(
2000
+ runner=runner,
2001
+ passed=parsed_report.passed,
2002
+ failed=0,
2003
+ skipped=parsed_report.skipped,
2004
+ errors=0,
2005
+ counts_known=True,
2006
+ )
2007
+ return parsed_report
2008
+
2009
+
2010
+ def _capture_expectation_run_outputs(
2011
+ *,
2012
+ state: TurnExecutionState,
2013
+ tool_name: str,
2014
+ arguments: dict[str, Any],
2015
+ result: dict[str, Any],
2016
+ ) -> None:
2017
+ """Capture bounded observed outputs of post-edit runs (turn-contract v2).
2018
+
2019
+ The expected-output evidence linker substring-matches contract literals against
2020
+ these outputs. Capture is unconditional telemetry (never kill-switched) and
2021
+ only records runs after a verification-relevant edit — pre-edit output can never
2022
+ confirm a post-edit expectation.
2023
+ """
2024
+ generation = state.verification_relevant_edit_generation
2025
+ if generation <= 0:
2026
+ return
2027
+ if tool_name == "shell_run":
2028
+ command = str(
2029
+ result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
2030
+ )
2031
+ output = _verification_output_text(result)
2032
+ if command and output:
2033
+ state.note_post_edit_run_output(command=command, output=output, generation=generation)
2034
+ elif tool_name == "verify_run":
2035
+ command_results = result.get("command_results")
2036
+ if isinstance(command_results, list):
2037
+ for item in command_results:
2038
+ if not isinstance(item, dict):
2039
+ continue
2040
+ command = str(item.get("command") or item.get("effective_command") or "")
2041
+ output = _verification_output_text(item)
2042
+ if command and output:
2043
+ state.note_post_edit_run_output(
2044
+ command=command, output=output, generation=generation
2045
+ )
2046
+ else:
2047
+ output = _verification_output_text(result)
2048
+ commands = result.get("commands")
2049
+ command = (
2050
+ ", ".join(str(item) for item in commands if item)
2051
+ if isinstance(commands, list)
2052
+ else ""
2053
+ )
2054
+ if output:
2055
+ state.note_post_edit_run_output(
2056
+ command=command or "verify_run", output=output, generation=generation
2057
+ )
2058
+
2059
+
2060
+ def _verification_attempt_executed_test_runner(
2061
+ *,
2062
+ tool_name: str,
2063
+ arguments: dict[str, Any],
2064
+ result: dict[str, Any],
2065
+ ) -> bool:
2066
+ """True when the recorded verification attempt ran a test-runner command."""
2067
+ return bool(
2068
+ _iter_executed_test_commands(
2069
+ tool_name=tool_name,
2070
+ arguments=arguments,
2071
+ result=result,
2072
+ )
2073
+ )
2074
+
2075
+
2076
+ def _record_tool_effect(
2077
+ *,
2078
+ root: Path,
2079
+ state: TurnExecutionState,
2080
+ tool_name: str,
2081
+ arguments: dict[str, Any],
2082
+ status: str,
2083
+ result: dict[str, Any],
2084
+ known_verification_commands: list[str] | None,
2085
+ verification_authoritative: bool = False,
2086
+ evidence_v2: bool = True,
2087
+ elapsed_ms: int | None = None,
2088
+ ) -> None:
2089
+ if is_tool_unavailable_result(result):
2090
+ return
2091
+ normalized_tool = tool_name.strip().lower()
2092
+ touched_paths: set[str] = set()
2093
+ benign_runtime_paths: set[str] = set()
2094
+ if normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
2095
+ raw_touched_paths = result.get("touched_repo_paths")
2096
+ if isinstance(raw_touched_paths, list):
2097
+ classifications = classify_mutation_paths(
2098
+ [str(item) for item in raw_touched_paths if isinstance(item, str)],
2099
+ root=root,
2100
+ command_was_verification=normalized_tool == "verify_run",
2101
+ )
2102
+ touched_paths = {item.path for item in classifications if item.is_material}
2103
+ benign_runtime_paths = {item.path for item in classifications if not item.is_material}
2104
+ if touched_paths:
2105
+ result["material_touched_repo_paths"] = sorted(touched_paths)
2106
+ if benign_runtime_paths:
2107
+ result["benign_runtime_paths"] = sorted(benign_runtime_paths)
2108
+ else:
2109
+ touched_paths = _extract_touched_repo_paths(
2110
+ root=root,
2111
+ tool_name=normalized_tool,
2112
+ arguments=arguments,
2113
+ result=result,
2114
+ )
2115
+ elif status != "failed" and normalized_tool in _MATERIAL_EDIT_TOOL_NAMES:
2116
+ touched_paths = _extract_touched_repo_paths(
2117
+ root=root,
2118
+ tool_name=normalized_tool,
2119
+ arguments=arguments,
2120
+ result=result,
2121
+ )
2122
+ elif normalized_tool == "subagent_run":
2123
+ touched_paths = _extract_touched_repo_paths(
2124
+ root=root,
2125
+ tool_name=normalized_tool,
2126
+ arguments=arguments,
2127
+ result=result,
2128
+ )
2129
+
2130
+ if status != "failed" and normalized_tool == "fs_write" and result.get("created") is True:
2131
+ # A brand-new file the agent authored this turn: a failing test in it is
2132
+ # signal (agent_authored), not a regression. touched_paths already holds
2133
+ # the normalized repo-relative path for fs_write.
2134
+ for created_path in touched_paths:
2135
+ state.note_agent_created_path(created_path)
2136
+
2137
+ # Reproduction-first guardrail (step 5): the reproduction is only evidence
2138
+ # while it stays the one that failed before the fix. Editing a recorded
2139
+ # artifact once product code has already changed is recorded and surfaced.
2140
+ if status != "failed" and normalized_tool in _MATERIAL_EDIT_TOOL_NAMES:
2141
+ _capture_repro_artifact_edits(state=state, touched_paths=touched_paths)
2142
+
2143
+ if (status != "failed" and normalized_tool in _MATERIAL_EDIT_TOOL_NAMES) or (
2144
+ normalized_tool == "subagent_run" and touched_paths
2145
+ ):
2146
+ state.note_material_edit()
2147
+ state.material_edit_tools.add(normalized_tool)
2148
+ state.touched_repo_paths.update(touched_paths)
2149
+ if _paths_require_verification(touched_paths):
2150
+ state.note_verification_relevant_edit()
2151
+ elif normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES and touched_paths:
2152
+ state.note_material_edit()
2153
+ state.material_edit_tools.add(normalized_tool)
2154
+ state.touched_repo_paths.update(touched_paths)
2155
+ if _paths_require_verification(touched_paths):
2156
+ state.note_verification_relevant_edit()
2157
+ elif status != "failed" and normalized_tool == "git_diff":
2158
+ state.record_diff_review()
2159
+
2160
+ verification_attempt = False
2161
+ evidence_records: list[VerificationEvidence] = []
2162
+ evidence = VerificationEvidence(
2163
+ category=VerificationEvidenceCategory.NOT_VERIFICATION,
2164
+ normalized_command=str(arguments.get("cmd") or ""),
2165
+ reason="not_checked",
2166
+ )
2167
+ if normalized_tool == "verify_run":
2168
+ verification_attempt = True
2169
+ evidence_records = _verify_run_evidence_records(
2170
+ result=result,
2171
+ known_verification_commands=known_verification_commands,
2172
+ verification_authoritative=verification_authoritative,
2173
+ material_touched_paths=touched_paths,
2174
+ root=root,
2175
+ evidence_v2=evidence_v2,
2176
+ )
2177
+ evidence = _aggregate_verification_evidence(evidence_records)
2178
+ elif normalized_tool == "shell_run":
2179
+ evidence = _shell_verification_evidence(
2180
+ root=root,
2181
+ state=state,
2182
+ arguments=arguments,
2183
+ result=result,
2184
+ known_verification_commands=known_verification_commands,
2185
+ verification_authoritative=verification_authoritative,
2186
+ material_touched_paths=touched_paths,
2187
+ evidence_v2=evidence_v2,
2188
+ )
2189
+ evidence_records = [evidence]
2190
+ verification_attempt = evidence.category != VerificationEvidenceCategory.NOT_VERIFICATION
2191
+ if normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
2192
+ result["verification_evidence_category"] = evidence.category.value
2193
+ result["verification_evidence_reason"] = evidence.reason
2194
+ result["verification_evidence_allowed"] = evidence.allowed_to_satisfy_contract
2195
+ result["verification_evidence_supplemental_only"] = evidence.supplemental_only
2196
+ result["evidence_verdict"] = evidence.evidence_verdict
2197
+ verification_note = _verification_evidence_note(evidence, result=result)
2198
+ if verification_note:
2199
+ result["verification_note"] = verification_note
2200
+ if evidence.supplemental_only:
2201
+ result["verification_supplemental_only_note"] = SUPPLEMENTAL_VERIFICATION_ADVISORY
2202
+ record_acceptance_tool_effect(
2203
+ contract=state.acceptance_contract,
2204
+ root=root,
2205
+ tool_name=normalized_tool,
2206
+ arguments=arguments,
2207
+ status=status,
2208
+ result=result,
2209
+ touched_paths=touched_paths,
2210
+ known_verification_commands=known_verification_commands,
2211
+ verification_authoritative=verification_authoritative,
2212
+ evidence_category=evidence.category.value,
2213
+ evidence_allowed=evidence.allowed_to_satisfy_contract,
2214
+ )
2215
+ # Baseline-first regression protocol (step 3): capture parsed per-test
2216
+ # outcomes for baseline/attribution. Runs for every executed test-runner
2217
+ # command regardless of the evidence classifier's verdict (so an
2218
+ # unobservable-pipeline run still contributes what its output shows), and
2219
+ # regardless of the kill-switch (capture is telemetry; only the gate policy
2220
+ # is gated).
2221
+ _capture_regression_test_runs(
2222
+ state=state,
2223
+ tool_name=normalized_tool,
2224
+ arguments=arguments,
2225
+ result=result,
2226
+ elapsed_ms=elapsed_ms,
2227
+ )
2228
+ # Turn-contract v2 (step 4): capture post-edit run output for the expected-output
2229
+ # evidence linker. Like the regression capture above, this is unconditional
2230
+ # telemetry (only the gate policy is kill-switched).
2231
+ _capture_expectation_run_outputs(
2232
+ state=state,
2233
+ tool_name=normalized_tool,
2234
+ arguments=arguments,
2235
+ result=result,
2236
+ )
2237
+ # Reproduction-first (step 5): capture every executed command that runs a file
2238
+ # the agent created this turn, phase-classified against the product edits
2239
+ # recorded so far. Unconditional telemetry, like the captures above — only the
2240
+ # gate policy and the turn directives are kill-switched.
2241
+ _capture_repro_runs(
2242
+ state=state,
2243
+ tool_name=normalized_tool,
2244
+ arguments=arguments,
2245
+ status=status,
2246
+ result=result,
2247
+ )
2248
+ if normalized_tool == "shell_run" and not verification_attempt:
2249
+ raw_command = str(
2250
+ result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
2251
+ )
2252
+ normalized_command = _unmasked_shell_verification_command(raw_command)
2253
+ observed_exit_code, observed_output = _verification_evidence_observation(
2254
+ tool_name=normalized_tool,
2255
+ evidence=evidence,
2256
+ result=result,
2257
+ )
2258
+ if normalized_command and observed_exit_code == 0 and observed_output:
2259
+ state.record_executed_command_evidence(
2260
+ normalized_command=normalized_command,
2261
+ observed_exit_code=observed_exit_code,
2262
+ observed_output=observed_output,
2263
+ )
2264
+ if not verification_attempt:
2265
+ return
2266
+
2267
+ state.verification_attempt_count += 1
2268
+ state.verification_tools.add(normalized_tool)
2269
+ state.last_verification_passed = _verification_attempt_passed(
2270
+ tool_name=normalized_tool,
2271
+ status=status,
2272
+ result=result,
2273
+ evidence=evidence,
2274
+ )
2275
+ for record in evidence_records:
2276
+ observed_exit_code, observed_output = _verification_evidence_observation(
2277
+ tool_name=normalized_tool,
2278
+ evidence=record,
2279
+ result=result,
2280
+ )
2281
+ state.record_verification_evidence(
2282
+ record,
2283
+ accepted=(
2284
+ state.last_verification_passed is True and record.allowed_to_satisfy_contract
2285
+ ),
2286
+ observed_exit_code=observed_exit_code,
2287
+ observed_output=observed_output,
2288
+ )
2289
+ if evidence_v2 and _tool_effect_has_qualifying_execution(
2290
+ tool_name=normalized_tool,
2291
+ evidence_records=evidence_records,
2292
+ result=result,
2293
+ ):
2294
+ # Ordering rule: stamp that a real execution run happened after the most
2295
+ # recent material edit, so finalization can require post-edit evidence.
2296
+ state.note_qualifying_execution_evidence()
2297
+ # Baseline-first regression protocol (step 3): remember whether this
2298
+ # verification attempt ran a test-runner command, so the gate can clear a
2299
+ # non-contract all-pre-existing test failure without masking a non-test one.
2300
+ state.last_verification_attempt_was_test_run = _verification_attempt_executed_test_runner(
2301
+ tool_name=normalized_tool,
2302
+ arguments=arguments,
2303
+ result=result,
2304
+ )
2305
+ if normalized_tool == "verify_run":
2306
+ _record_verify_run_command_outcomes(
2307
+ state=state,
2308
+ result=result,
2309
+ known_verification_commands=known_verification_commands,
2310
+ )
2311
+ elif normalized_tool == "shell_run":
2312
+ _record_shell_verification_command_outcome(
2313
+ state=state,
2314
+ arguments=arguments,
2315
+ result=result,
2316
+ known_verification_commands=known_verification_commands,
2317
+ passed=state.last_verification_passed is True,
2318
+ evidence=evidence,
2319
+ )
2320
+
2321
+ if state.last_verification_passed is True:
2322
+ state.last_verification_failure_category = ""
2323
+ if not state.failed_verification_commands():
2324
+ state.last_verification_failure_snippet = ""
2325
+ else:
2326
+ state.last_verification_failure_category = _verification_failure_category_for_tool_result(
2327
+ tool_name=normalized_tool,
2328
+ arguments=arguments,
2329
+ result=result,
2330
+ )
2331
+ state.last_verification_failure_snippet = (
2332
+ extract_verification_failure_snippet(
2333
+ tool_name=normalized_tool,
2334
+ result=result,
2335
+ )
2336
+ or state.first_failed_verification_snippet()
2337
+ )
2338
+
2339
+
2340
+ def _verification_expected_for_turn(
2341
+ *,
2342
+ turn_intent: _OneShotRepoTurnIntent,
2343
+ blocked: bool,
2344
+ touched_repo_paths: set[str],
2345
+ verification_contract_requires_execution: bool = False,
2346
+ verification_contract_available: bool = True,
2347
+ effective_verification_commands: list[str] | tuple[str, ...] | set[str] | None = None,
2348
+ ) -> bool:
2349
+ if turn_intent != "execute":
2350
+ return False
2351
+ if verification_contract_requires_execution:
2352
+ return True
2353
+ if blocked:
2354
+ return False
2355
+ if not verification_contract_available:
2356
+ return False
2357
+ return _verification_commands_apply_to_paths(
2358
+ touched_repo_paths,
2359
+ effective_verification_commands,
2360
+ )
2361
+
2362
+
2363
+ def _completion_gate_blocker_allows_final(
2364
+ *,
2365
+ state: TurnExecutionState,
2366
+ blocked_response: bool,
2367
+ ) -> bool:
2368
+ if not blocked_response:
2369
+ return False
2370
+ if not state.touched_repo_paths or not _paths_require_verification(state.touched_repo_paths):
2371
+ return True
2372
+ if state.verification_attempt_count <= 0:
2373
+ return False
2374
+ if state.last_verification_passed is True:
2375
+ return True
2376
+ return state.last_verification_failure_category == FailureCategory.INFRA_UNAVAILABLE.value
2377
+
2378
+
2379
+ def _execution_evidence_required_for_turn(
2380
+ *,
2381
+ state: TurnExecutionState,
2382
+ turn_intent: str,
2383
+ blocked: bool,
2384
+ evidence_v2: bool,
2385
+ verification_expected: bool,
2386
+ ) -> bool:
2387
+ """Ordering rule trigger: an execute turn that mutated a verifiable surface.
2388
+
2389
+ The point of the rule is to catch "edited source, then only ran a syntax
2390
+ check (or nothing), then finalized". It requires that verification is
2391
+ actually applicable for this turn (``verification_expected``) so greenfield
2392
+ workspaces with no test surface are not harassed. Turns with no mutating
2393
+ edits (pure Q&A/analysis/advisory) are exempt, as are non-execute turns and
2394
+ blocker finalizations.
2395
+ """
2396
+ return bool(
2397
+ evidence_v2
2398
+ and verification_expected
2399
+ and str(turn_intent or "") == "execute"
2400
+ and not blocked
2401
+ and state.material_edit_count > 0
2402
+ and _paths_require_verification(state.touched_repo_paths)
2403
+ )
2404
+
2405
+
2406
+ def _completion_gate_problems(
2407
+ *,
2408
+ state: TurnExecutionState,
2409
+ final_text: str,
2410
+ blocked: bool,
2411
+ verification_expected: bool,
2412
+ require_material_edit_evidence: bool = True,
2413
+ evidence_v2: bool = False,
2414
+ turn_intent: str = "",
2415
+ regression_baseline_enabled: bool = False,
2416
+ turn_contract_v2_enabled: bool = False,
2417
+ reproduction_first_enabled: bool = False,
2418
+ repro_engagement_based: bool = False,
2419
+ blast_radius_enabled: bool = False,
2420
+ ) -> list[str]:
2421
+ expectation_assessment = state.compute_expectation_assessment(
2422
+ enabled=turn_contract_v2_enabled,
2423
+ turn_intent=turn_intent,
2424
+ )
2425
+ repro_assessment = state.compute_repro_assessment(
2426
+ enabled=reproduction_first_enabled,
2427
+ turn_intent=turn_intent,
2428
+ engagement_based=repro_engagement_based,
2429
+ )
2430
+ blast_radius_assessment = state.compute_blast_radius_assessment(
2431
+ enabled=blast_radius_enabled,
2432
+ turn_intent=turn_intent,
2433
+ )
2434
+ execution_evidence_required = _execution_evidence_required_for_turn(
2435
+ state=state,
2436
+ turn_intent=turn_intent,
2437
+ blocked=blocked,
2438
+ evidence_v2=evidence_v2,
2439
+ verification_expected=verification_expected,
2440
+ )
2441
+ regression_diff = state.compute_regression_diff(enabled=regression_baseline_enabled)
2442
+ # Let attribution supersede a non-contract "last attempt failed" block only
2443
+ # when that last attempt was itself a test run AND the diff attributes at
2444
+ # least one failure as pre-existing/regression/unattributed. The test-run
2445
+ # guard stops an all-benign earlier run from masking a failing non-test
2446
+ # command; the "not agent-authored-only" guard keeps a failing repro the agent
2447
+ # just wrote (agent-authored only) blocking as a generic verification failure.
2448
+ regression_attribution_supersedes_last_failure = bool(
2449
+ regression_baseline_enabled
2450
+ and state.last_verification_attempt_was_test_run
2451
+ and (
2452
+ regression_diff.regressions
2453
+ or regression_diff.unattributed
2454
+ or regression_diff.pre_existing
2455
+ )
2456
+ )
2457
+ certificate = evaluate_completion_certificate(
2458
+ CompletionCertificateInput(
2459
+ contract=state.acceptance_contract,
2460
+ final_text=final_text,
2461
+ blocked=blocked,
2462
+ blocker_valid=blocked,
2463
+ material_edit_count=state.material_edit_count,
2464
+ require_material_result=require_material_edit_evidence,
2465
+ verification_expected=verification_expected,
2466
+ verification_attempt_count=state.verification_attempt_count,
2467
+ last_verification_passed=state.last_verification_passed,
2468
+ failed_verification_commands=state.failed_verification_commands(),
2469
+ expected_verification_commands=set(state.expected_verification_commands),
2470
+ missing_verification_commands=state.missing_verification_commands(),
2471
+ verification_coverage_stale=state.verification_coverage_is_stale(),
2472
+ accepted_verification_evidence=list(state.accepted_verification_evidence),
2473
+ execution_evidence_required=execution_evidence_required,
2474
+ post_edit_execution_evidence_present=state.has_post_edit_execution_evidence(),
2475
+ regression_baseline_enabled=regression_baseline_enabled,
2476
+ regressions=regression_diff.regressions,
2477
+ unattributed_failures=regression_diff.unattributed,
2478
+ pre_existing_failures=regression_diff.pre_existing,
2479
+ agent_authored_failures=regression_diff.agent_authored,
2480
+ regression_attribution_supersedes_last_failure=(
2481
+ regression_attribution_supersedes_last_failure
2482
+ ),
2483
+ turn_contract_v2_enabled=turn_contract_v2_enabled,
2484
+ expectations_unaddressed=expectation_assessment.unaddressed,
2485
+ reproduction_first_enabled=reproduction_first_enabled,
2486
+ repro_unconfirmed=repro_blocks_finalization(
2487
+ repro_assessment,
2488
+ material_edit_count=state.material_edit_count,
2489
+ ),
2490
+ repro_failing_after_fix=repro_assessment.contradicted,
2491
+ repro_status=repro_assessment.status.value if repro_assessment.applicable else "",
2492
+ repro_artifacts_present=repro_assessment.surviving_artifacts,
2493
+ blast_radius_enabled=blast_radius_enabled,
2494
+ # Failures step 3 already reports as regressions of the same command are
2495
+ # dropped here: one fact, one blocker. What remains is the breakage only
2496
+ # the selected scope saw — the tests the agent never chose to run.
2497
+ blast_radius_new_failures=tuple(
2498
+ test_id
2499
+ for test_id in blast_radius_assessment.new_failures
2500
+ if test_id not in set(regression_diff.regressions)
2501
+ ),
2502
+ # Only the "never measured" state feeds the weaker problem; a REGRESSED
2503
+ # assessment whose ids step 3 already owns must not resurface here as a
2504
+ # coverage complaint about a scope the agent demonstrably ran.
2505
+ blast_radius_unverified=(
2506
+ blast_radius_assessment.status == BlastRadiusStatus.GATE_MISSING
2507
+ and blast_radius_blocks_finalization(
2508
+ blast_radius_assessment,
2509
+ material_edit_count=state.material_edit_count,
2510
+ )
2511
+ ),
2512
+ blast_radius_status=(
2513
+ blast_radius_assessment.status.value if blast_radius_assessment.applicable else ""
2514
+ ),
2515
+ )
2516
+ )
2517
+ state.latest_completion_certificate = certificate.as_payload()
2518
+ return list(certificate.problems)
2519
+
2520
+
2521
+ def _sorted_missing_verification_commands(state: TurnExecutionState) -> list[str]:
2522
+ return sorted(state.missing_verification_commands())
2523
+
2524
+
2525
+ def _completion_gate_problem_summary(problems: list[str]) -> str:
2526
+ labels = [_COMPLETION_GATE_PROBLEM_LABELS.get(item, item) for item in problems]
2527
+ return ", ".join(labels) if labels else "unknown completion gate failure"
2528
+
2529
+
2530
+ def _completion_gate_repair_stage(problems: list[str]) -> str:
2531
+ if "no_material_edits" in problems:
2532
+ return "no_material_edits"
2533
+ # Regressions are the most specific, most actionable verification failure:
2534
+ # named tests that passed pre-edit and now fail. Rank them ahead of the
2535
+ # generic verification_failed so the repair nudge names them concretely.
2536
+ if "regressions_detected" in problems:
2537
+ return "regressions_detected"
2538
+ # Proven collateral damage ranks with the other regression stages and above the
2539
+ # generic verification failure: it names concrete tests, and its repair is a
2540
+ # different action (narrow the change) than "make your own check pass".
2541
+ if "blast_radius_regressions" in problems:
2542
+ return "blast_radius_regressions"
2543
+ if "verification_failed" in problems:
2544
+ return "verification_failed"
2545
+ if "verification_incomplete" in problems:
2546
+ return "verification_incomplete"
2547
+ if "verification_not_attempted" in problems:
2548
+ return "verification_not_attempted"
2549
+ if "unattributed_failures" in problems:
2550
+ return "unattributed_failures"
2551
+ # An unmeasured blast radius ranks *below* the verification stages: when nothing
2552
+ # has been run at all, "you ran no tests" is the more fundamental complaint and
2553
+ # owns the repair loop. This stage takes over once that is satisfied and only the
2554
+ # neighbouring tests are still unrun.
2555
+ if "blast_radius_unverified" in problems:
2556
+ return "blast_radius_unverified"
2557
+ # Reproduction-first: an unvalidated reported symptom ranks above the
2558
+ # task-expectation stage — a reproduction is the most direct evidence that the
2559
+ # delivered change addresses what was reported, not the agent's reading of it.
2560
+ # Scaffolding cleanup shares the stage; the nudge names whichever applies.
2561
+ if "repro_unconfirmed" in problems or "repro_artifacts_present" in problems:
2562
+ return "repro_unconfirmed"
2563
+ # Turn-contract v2: task-named expectations rank below verification/regression
2564
+ # deficits (broken behavior is more urgent) but above acceptance-criteria stages.
2565
+ if "expectations_unaddressed" in problems:
2566
+ return "expectations_unaddressed"
2567
+ if "acceptance_criteria_failed" in problems or "unexpected_scope_changes" in problems:
2568
+ return "acceptance_failed"
2569
+ if (
2570
+ "acceptance_criteria_unverified" in problems
2571
+ or "acceptance_evidence_insufficient" in problems
2572
+ ):
2573
+ return "acceptance_unverified"
2574
+ if "empty_final_response" in problems:
2575
+ return "empty_final_response"
2576
+ return "generic"
2577
+
2578
+
2579
+ _LIVE_BACKGROUND_PROCESS_FINALIZATION_LINE = (
2580
+ "- You have {n} background process(es) started with shell_background; they are "
2581
+ "terminated when this run ends. If the task requires a server/daemon to still "
2582
+ "be running after you finish, start it with shell_service_start (durable) instead, "
2583
+ "and re-verify."
2584
+ )
2585
+
2586
+
2587
+ def _live_background_process_finalization_advisory_line(
2588
+ *,
2589
+ one_shot_execution: bool,
2590
+ live_background_processes: int = 0,
2591
+ ) -> str:
2592
+ try:
2593
+ count = int(live_background_processes)
2594
+ except (TypeError, ValueError):
2595
+ count = 0
2596
+ if not one_shot_execution or count <= 0:
2597
+ return ""
2598
+ return _LIVE_BACKGROUND_PROCESS_FINALIZATION_LINE.format(n=count)
2599
+
2600
+
2601
+ def _completion_gate_nudge_message(
2602
+ problems: list[str],
2603
+ *,
2604
+ prefix_key: str = "completion_gate_nudge_prefix",
2605
+ verification_failure_snippet: str = "",
2606
+ missing_verification_commands: list[str] | None = None,
2607
+ verification_coverage_stale: bool = False,
2608
+ anchor_paths: list[str] | None = None,
2609
+ has_material_edits: bool = False,
2610
+ all_verification_evidence_self_authored: bool = False,
2611
+ diff_review_stale: bool = False,
2612
+ language: str = "",
2613
+ explicit_language_override: bool = False,
2614
+ one_shot_execution: bool = False,
2615
+ live_background_processes: int = 0,
2616
+ execution_evidence_missing_detail: str = "",
2617
+ regression_ids: list[str] | None = None,
2618
+ regression_baseline_command: str = "",
2619
+ unattributed_ids: list[str] | None = None,
2620
+ expectation_details: list[str] | None = None,
2621
+ repro_assessment: ReproAssessment | None = None,
2622
+ blast_radius_assessment: BlastRadiusAssessment | None = None,
2623
+ ) -> str:
2624
+ _ = (
2625
+ prefix_key,
2626
+ verification_coverage_stale,
2627
+ anchor_paths,
2628
+ language,
2629
+ explicit_language_override,
2630
+ )
2631
+ problem_set = set(problems)
2632
+ # A post-edit execution-evidence deficit is action-only: the model must run
2633
+ # the tests; a written explanation cannot clear it. Naming the concrete
2634
+ # missing fact keeps successive nudges specific rather than repetitive.
2635
+ evidence_deficit = bool(execution_evidence_missing_detail) and bool(
2636
+ problem_set & {"verification_not_attempted", "verification_incomplete"}
2637
+ )
2638
+ # Regressions are action-only in the same way: only making the named tests
2639
+ # pass again clears the deficit; prose cannot. Unattributed failures need a
2640
+ # fact (a rerun of the baseline-known command) to be attributed.
2641
+ regression_deficit = "regressions_detected" in problem_set
2642
+ unattributed_deficit = "unattributed_failures" in problem_set
2643
+ # Turn-contract v2: task-named expectations neither confirmed nor disposed. Each
2644
+ # is addressed by editing the named locus or producing (and running) the expected
2645
+ # output — prose alone cannot clear it.
2646
+ expectation_deficit = "expectations_unaddressed" in problem_set
2647
+ # Reproduction-first: an unvalidated reported symptom is action-only too — only
2648
+ # a reproduction that failed before the fix and passes after it clears it.
2649
+ repro_deficit = "repro_unconfirmed" in problem_set
2650
+ repro_artifacts_deficit = "repro_artifacts_present" in problem_set
2651
+ # Blast radius: both states are action-only. Only running the scope measures it,
2652
+ # and only making the broken tests pass again (by narrowing the change) clears a
2653
+ # regression; neither can be talked away.
2654
+ blast_radius_deficit = bool(
2655
+ problem_set & {"blast_radius_regressions", "blast_radius_unverified"}
2656
+ )
2657
+ lines = ["Finalization check - one pass before you finish:"]
2658
+ if "no_material_edits" in problem_set:
2659
+ lines.append(
2660
+ "- No file changes are recorded yet. If the task required creating/modifying "
2661
+ "something, do it now; if you concluded no change is needed, say so explicitly "
2662
+ "with your reasoning."
2663
+ )
2664
+ snippet = extract_actionable_failure_snippet(verification_failure_snippet)
2665
+ if "verification_failed" in problem_set:
2666
+ failure_detail = snippet or "the latest verification attempt did not pass"
2667
+ lines.append(
2668
+ f"- Your last verification failed: {failure_detail}. Fix and re-run, or explain "
2669
+ "why the failure is expected/out of scope."
2670
+ )
2671
+ if missing_verification_commands and (
2672
+ "verification_not_attempted" in problem_set or "verification_incomplete" in problem_set
2673
+ ):
2674
+ lines.append(
2675
+ "- Expected verification not yet run: "
2676
+ + ", ".join(missing_verification_commands)
2677
+ + ". Run them, or state why they don't apply."
2678
+ )
2679
+ elif "verification_not_attempted" in problem_set or "verification_incomplete" in problem_set:
2680
+ lines.append(
2681
+ "- Expected verification has not been completed. Run it, or state why it does not apply."
2682
+ )
2683
+ if evidence_deficit:
2684
+ lines.append(
2685
+ f"- No test execution recorded {execution_evidence_missing_detail}. Run the relevant "
2686
+ "tests now and observe their output and exit code. A written explanation cannot "
2687
+ "clear this - only a new test run can."
2688
+ )
2689
+ if regression_deficit and regression_ids:
2690
+ baseline = str(regression_baseline_command or "").strip() or "the baseline command"
2691
+ lines.append(
2692
+ "- Regressions your change introduced: "
2693
+ + ", ".join(str(item) for item in regression_ids)
2694
+ + f". These tests passed in the pre-edit baseline of `{baseline}` and now fail. "
2695
+ "Fix them and re-run so they pass again. A written explanation cannot clear this - "
2696
+ "only making the tests pass can."
2697
+ )
2698
+ if unattributed_deficit and unattributed_ids:
2699
+ lines.append(
2700
+ "- Failures with no comparable pre-edit baseline (cannot tell if your change caused "
2701
+ "them): "
2702
+ + ", ".join(str(item) for item in unattributed_ids)
2703
+ + ". Re-run the exact command you have a baseline for (or run it now to establish "
2704
+ "one) so these can be attributed, or state their relationship to your change with "
2705
+ "evidence."
2706
+ )
2707
+ if expectation_deficit and expectation_details:
2708
+ lines.append(
2709
+ "- Task expectations not yet addressed: "
2710
+ + "; ".join(str(item) for item in expectation_details)
2711
+ + ". The task named these concretely. Either make your change satisfy each one "
2712
+ "(edit the named locus, or produce the expected output and run the command so it "
2713
+ "is observed), or explicitly state why the expectation no longer applies."
2714
+ )
2715
+ if repro_deficit and repro_assessment is not None:
2716
+ repro_line = build_repro_nudge_line(repro_assessment)
2717
+ if repro_line:
2718
+ lines.append(repro_line)
2719
+ if repro_artifacts_deficit and repro_assessment is not None:
2720
+ artifacts_line = build_repro_artifacts_nudge_line(repro_assessment.surviving_artifacts)
2721
+ if artifacts_line:
2722
+ lines.append(artifacts_line)
2723
+ if blast_radius_deficit and blast_radius_assessment is not None:
2724
+ blast_radius_line = build_blast_radius_nudge_line(blast_radius_assessment)
2725
+ if blast_radius_line:
2726
+ lines.append(blast_radius_line)
2727
+ if all_verification_evidence_self_authored:
2728
+ lines.append(f"- {SUPPLEMENTAL_VERIFICATION_ADVISORY}")
2729
+ if has_material_edits and diff_review_stale:
2730
+ lines.append(
2731
+ "- Consider reviewing the current diff for accidental scope or quality issues before "
2732
+ "finalizing."
2733
+ )
2734
+ live_background_process_line = _live_background_process_finalization_advisory_line(
2735
+ one_shot_execution=one_shot_execution,
2736
+ live_background_processes=live_background_processes,
2737
+ )
2738
+ if live_background_process_line:
2739
+ lines.append(live_background_process_line)
2740
+ lines.append(
2741
+ "- Re-read the task statement once and confirm every explicitly named output "
2742
+ "(paths, formats, values) exists exactly as requested."
2743
+ )
2744
+ if (
2745
+ evidence_deficit
2746
+ or regression_deficit
2747
+ or unattributed_deficit
2748
+ or expectation_deficit
2749
+ or repro_deficit
2750
+ or blast_radius_deficit
2751
+ ):
2752
+ lines.append(
2753
+ "Run the relevant tests now, then give your final answer once you have observed "
2754
+ "the result."
2755
+ )
2756
+ else:
2757
+ lines.append(
2758
+ "Then give your final answer. If you are confident the work is complete as-is, "
2759
+ "finalize - this checklist is advisory."
2760
+ )
2761
+ return "\n".join(lines)
2762
+
2763
+
2764
+ def _build_interactive_turn_verify_task(
2765
+ *,
2766
+ session: Any,
2767
+ instruction: str,
2768
+ ) -> tuple[dict[str, Any] | None, list[str]]:
2769
+ task_paths = _extract_workspace_relation_paths_from_text(root=session.root, text=instruction)
2770
+ task_brief = _session_task_brief_content(session)
2771
+ if task_brief:
2772
+ for path in _extract_workspace_relation_paths_from_text(root=session.root, text=task_brief):
2773
+ if path not in task_paths:
2774
+ task_paths.append(path)
2775
+ task_texts = [str(instruction or "").strip()]
2776
+ if task_brief:
2777
+ task_texts.extend(_task_brief_lines_from_text(task_brief, max_lines=6))
2778
+ task_texts = [text for text in task_texts if text]
2779
+ if not task_paths and not task_texts:
2780
+ return None, []
2781
+ task: dict[str, Any] = {}
2782
+ if task_paths:
2783
+ task["estimated_files"] = list(task_paths)
2784
+ task["write_scope"] = list(task_paths)
2785
+ if task_texts:
2786
+ task["acceptance_criteria"] = list(task_texts)
2787
+ return task, task_texts
2788
+
2789
+
2790
+ def _refresh_execute_turn_verification_selection(
2791
+ session: Any,
2792
+ *,
2793
+ instruction: str,
2794
+ route_execution_posture: str,
2795
+ ) -> None:
2796
+ if not bool(getattr(session, "verification_enabled", True)):
2797
+ return
2798
+ runtime_kind = getattr(session, "runtime_kind", RuntimeKind.INTERACTIVE_CHAT)
2799
+ one_shot_execution = bool(getattr(session, "one_shot_execution", False))
2800
+ if runtime_kind != RuntimeKind.INTERACTIVE_CHAT and not one_shot_execution:
2801
+ return
2802
+ if getattr(session, "authoritative_verification_commands", None) is not None:
2803
+ return
2804
+ if str(route_execution_posture or "").strip().lower() != "execute":
2805
+ return
2806
+
2807
+ repo_scan = _session_repo_scan(session)
2808
+ task, plan_requirements = _build_interactive_turn_verify_task(
2809
+ session=session,
2810
+ instruction=instruction,
2811
+ )
2812
+ current = _session_verify_command_selection(session)
2813
+ resolved = resolve_task_aware_verify_command_selection(
2814
+ cfg=session.cfg,
2815
+ verify_cmd=None,
2816
+ task=task,
2817
+ root=session.root,
2818
+ repo_scan=repo_scan,
2819
+ plan_requirements=plan_requirements,
2820
+ selection=current,
2821
+ )
2822
+ explicit_commands = extract_explicit_acceptance_commands(
2823
+ instruction,
2824
+ *[str(item) for item in plan_requirements],
2825
+ )
2826
+ if (
2827
+ explicit_commands
2828
+ and not is_authoritative_verify_command_selection(current)
2829
+ and resolved.contract_type in {"generic_fallback", "unavailable", ""}
2830
+ ):
2831
+ resolved = ResolvedVerifyCommands(
2832
+ commands=tuple(explicit_commands),
2833
+ source="task_refinement.explicit_user_command",
2834
+ reason="explicit user command is the task-native verification contract",
2835
+ contract_type="task_acceptance",
2836
+ )
2837
+ if (
2838
+ current is not None
2839
+ and current.commands == resolved.commands
2840
+ and current.source == resolved.source
2841
+ and current.reason == resolved.reason
2842
+ and current.contract_type == resolved.contract_type
2843
+ ):
2844
+ return
2845
+
2846
+ previous_payload = (
2847
+ verification_selection_payload(
2848
+ current,
2849
+ authoritative=is_authoritative_verify_command_selection(current),
2850
+ )
2851
+ if current is not None
2852
+ else None
2853
+ )
2854
+ session.effective_verification_commands = list(resolved.commands)
2855
+ session.verification_selection_source = resolved.source
2856
+ session.verification_selection_reason = resolved.reason
2857
+ session.verification_contract_type = resolved.contract_type
2858
+ session.verification_authoritative = is_authoritative_verify_command_selection(resolved)
2859
+ refresh_session_environment_context_message(session)
2860
+ payload: dict[str, Any] = {
2861
+ "instruction_paths": list(task.get("estimated_files", []))
2862
+ if isinstance(task, dict)
2863
+ else [],
2864
+ "route_execution_posture": route_execution_posture,
2865
+ **verification_selection_payload(
2866
+ resolved,
2867
+ authoritative=is_authoritative_verify_command_selection(resolved),
2868
+ ),
2869
+ }
2870
+ if previous_payload is not None:
2871
+ payload["previous"] = previous_payload
2872
+ session.store.append("verification_contract_updated", payload)
2873
+
2874
+
2875
+ def _refresh_interactive_turn_verification_selection(
2876
+ session: Any,
2877
+ *,
2878
+ instruction: str,
2879
+ route_execution_posture: str,
2880
+ ) -> None:
2881
+ _refresh_execute_turn_verification_selection(
2882
+ session,
2883
+ instruction=instruction,
2884
+ route_execution_posture=route_execution_posture,
2885
+ )