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,227 @@
1
+ """Best-effort prompt-cache affinity while a parent waits on synchronous children."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import threading
7
+ from collections.abc import Callable, Iterator
8
+ from contextlib import contextmanager
9
+ from dataclasses import dataclass
10
+ from time import monotonic
11
+ from typing import Any
12
+
13
+ from ..execution_deadline import DeadlinePhase, ExecutionDeadline
14
+
15
+
16
+ def cache_keepalive_unsupported_reason(client: Any) -> str | None:
17
+ """Return why replay keepalive cannot preserve this client's cache stream."""
18
+ if getattr(client, "provider_auth", None) is not None:
19
+ return "subscription_gateway"
20
+ transport = str(getattr(client, "cache_keepalive_transport", "") or "").strip()
21
+ if transport == "response_continuation":
22
+ return transport
23
+ return None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class CacheKeepaliveRequest:
28
+ """Immutable copy of the last parent request prefix sent to the provider."""
29
+
30
+ messages: list[dict[str, Any]]
31
+ tools: list[dict[str, Any]] | None
32
+ tool_choice: Any | None
33
+
34
+
35
+ class ParentCacheKeepalive:
36
+ """Refresh an idle parent prefix without changing its durable transcript."""
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ enabled: bool,
42
+ idle_threshold_s: float,
43
+ send_ping: Callable[[CacheKeepaliveRequest, threading.Event], bool],
44
+ on_disabled: Callable[[int], None] | None = None,
45
+ on_unsupported: Callable[[str], None] | None = None,
46
+ unsupported_reason: str | None = None,
47
+ deadline: ExecutionDeadline | None,
48
+ clock: Callable[[], float] = monotonic,
49
+ ) -> None:
50
+ requested_enabled = bool(enabled)
51
+ normalized_unsupported_reason = str(unsupported_reason or "").strip()
52
+ self.enabled = requested_enabled and not normalized_unsupported_reason
53
+ self.idle_threshold_s = max(0.001, float(idle_threshold_s))
54
+ self._send_ping = send_ping
55
+ self._on_disabled = on_disabled
56
+ self._deadline = deadline
57
+ self._clock = clock
58
+ self._lock = threading.RLock()
59
+ self._snapshot: CacheKeepaliveRequest | None = None
60
+ self._last_parent_request_at: float | None = None
61
+ self._last_ping_at: float | None = None
62
+ self._wait_depth = 0
63
+ self._cancelled: Callable[[], bool] | None = None
64
+ self._stop_event: threading.Event | None = None
65
+ self._worker: threading.Thread | None = None
66
+ self._consecutive_failures = 0
67
+ self._failure_disable_reported = False
68
+ if requested_enabled and normalized_unsupported_reason and on_unsupported is not None:
69
+ try:
70
+ on_unsupported(normalized_unsupported_reason)
71
+ except Exception: # noqa: BLE001 - warning emission is best-effort
72
+ pass
73
+
74
+ def note_parent_request(
75
+ self,
76
+ *,
77
+ messages: list[dict[str, Any]],
78
+ tools: list[dict[str, Any]] | None,
79
+ tool_choice: Any | None = None,
80
+ ) -> None:
81
+ """Capture the exact prefix inputs before a real parent request is sent."""
82
+ if not self.enabled:
83
+ return
84
+ with self._lock:
85
+ self._snapshot = CacheKeepaliveRequest(
86
+ messages=copy.deepcopy(messages),
87
+ tools=copy.deepcopy(tools),
88
+ tool_choice=copy.deepcopy(tool_choice),
89
+ )
90
+ self._last_parent_request_at = self._clock()
91
+ self._last_ping_at = None
92
+
93
+ def forget_parent_request(self) -> None:
94
+ """Drop a snapshot that must not be retained or replayed."""
95
+ with self._lock:
96
+ self._snapshot = None
97
+ self._last_parent_request_at = None
98
+ self._last_ping_at = None
99
+
100
+ @contextmanager
101
+ def synchronous_child_wait(
102
+ self,
103
+ *,
104
+ cancelled: Callable[[], bool] | None = None,
105
+ ) -> Iterator[None]:
106
+ """Mark time in which the parent is blocked on synchronous child work."""
107
+ self._begin_wait(cancelled=cancelled)
108
+ try:
109
+ yield
110
+ finally:
111
+ self._end_wait()
112
+
113
+ def _begin_wait(self, *, cancelled: Callable[[], bool] | None) -> None:
114
+ if not self.enabled:
115
+ return
116
+ with self._lock:
117
+ self._wait_depth += 1
118
+ self._cancelled = cancelled
119
+ if self._wait_depth != 1 or self._snapshot is None:
120
+ return
121
+ stop_event = threading.Event()
122
+ worker = threading.Thread(
123
+ target=self._run,
124
+ args=(stop_event,),
125
+ name="parent-cache-keepalive",
126
+ daemon=True,
127
+ )
128
+ self._stop_event = stop_event
129
+ self._worker = worker
130
+ worker.start()
131
+
132
+ def _end_wait(self) -> None:
133
+ with self._lock:
134
+ self._wait_depth = max(0, self._wait_depth - 1)
135
+ if self._wait_depth != 0:
136
+ return
137
+ self._cancelled = None
138
+ if self._stop_event is not None:
139
+ self._stop_event.set()
140
+ self._stop_event = None
141
+ self._worker = None
142
+ # Never join here: a provider that ignores cooperative cancellation must
143
+ # not add latency to the child result the parent is waiting for.
144
+
145
+ def close(self) -> None:
146
+ """Stop refreshes during session teardown."""
147
+ worker: threading.Thread | None = None
148
+ with self._lock:
149
+ self._wait_depth = 0
150
+ self._cancelled = None
151
+ if self._stop_event is not None:
152
+ self._stop_event.set()
153
+ worker = self._worker
154
+ self._stop_event = None
155
+ self._worker = None
156
+ if worker is not None and worker is not threading.current_thread():
157
+ worker.join(timeout=1.0)
158
+
159
+ def _run(self, stop_event: threading.Event) -> None:
160
+ while not stop_event.is_set():
161
+ with self._lock:
162
+ anchor = self._last_ping_at or self._last_parent_request_at
163
+ active = self._wait_depth > 0
164
+ if not active or anchor is None:
165
+ return
166
+ wait_s = max(0.0, anchor + self.idle_threshold_s - self._clock())
167
+ if stop_event.wait(wait_s):
168
+ return
169
+ if not self._send_if_due(stop_event=stop_event):
170
+ return
171
+
172
+ def _send_if_due(self, *, stop_event: threading.Event) -> bool:
173
+ """Send one due ping; return whether the periodic loop may continue."""
174
+ with self._lock:
175
+ snapshot = self._snapshot
176
+ anchor = self._last_ping_at or self._last_parent_request_at
177
+ cancelled = self._cancelled
178
+ if (
179
+ self._wait_depth <= 0
180
+ or snapshot is None
181
+ or anchor is None
182
+ or self._clock() < anchor + self.idle_threshold_s
183
+ ):
184
+ return True
185
+ if stop_event.is_set() or (cancelled is not None and cancelled()):
186
+ return False
187
+ if not self._deadline_allows_ping():
188
+ return False
189
+ # Reserve the interval before sending so a provider error cannot create
190
+ # a tight retry loop. The next attempt is never earlier than one full
191
+ # configured threshold.
192
+ self._last_ping_at = self._clock()
193
+ request = copy.deepcopy(snapshot)
194
+ try:
195
+ succeeded = bool(self._send_ping(request, stop_event))
196
+ except Exception: # noqa: BLE001 - affinity can never crash child work
197
+ succeeded = False
198
+ disable_callback: Callable[[int], None] | None = None
199
+ failures = 0
200
+ with self._lock:
201
+ if succeeded:
202
+ self._consecutive_failures = 0
203
+ else:
204
+ self._consecutive_failures += 1
205
+ failures = self._consecutive_failures
206
+ if failures >= 2:
207
+ self.enabled = False
208
+ stop_event.set()
209
+ if not self._failure_disable_reported:
210
+ self._failure_disable_reported = True
211
+ disable_callback = self._on_disabled
212
+ if disable_callback is not None:
213
+ try:
214
+ disable_callback(failures)
215
+ except Exception: # noqa: BLE001 - warning emission is best-effort
216
+ pass
217
+ return failures < 2 and not stop_event.is_set()
218
+
219
+ def _deadline_allows_ping(self) -> bool:
220
+ deadline = self._deadline
221
+ if deadline is None:
222
+ return True
223
+ phase = deadline.phase()
224
+ if phase in {DeadlinePhase.FINALIZATION_WINDOW, DeadlinePhase.EXHAUSTED}:
225
+ return False
226
+ remaining = deadline.remaining_seconds()
227
+ return remaining is None or remaining >= self.idle_threshold_s
@@ -0,0 +1,366 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import StrEnum
5
+ from typing import Any
6
+
7
+ from .acceptance_contract import (
8
+ AcceptanceContract,
9
+ AcceptanceCriterion,
10
+ AcceptanceCriterionEnforcement,
11
+ AcceptanceCriterionKind,
12
+ AcceptanceCriterionStatus,
13
+ )
14
+
15
+
16
+ class CompletionCertificateStatus(StrEnum):
17
+ SUFFICIENT = "SUFFICIENT"
18
+ INSUFFICIENT = "INSUFFICIENT"
19
+ CONTRADICTED = "CONTRADICTED"
20
+
21
+
22
+ _EVIDENCE_RANK = {
23
+ "HOST_AUTHORITATIVE": 0,
24
+ "USER_EXPLICIT": 1,
25
+ "PREEXISTING_TASK_CHECKER": 2,
26
+ "DIRECT_BLACK_BOX": 3,
27
+ "PREEXISTING_REPO_NATIVE": 4,
28
+ "SELF_AUTHORED": 5,
29
+ "AD_HOC_OBSERVATION": 6,
30
+ }
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class CompletionCertificate:
35
+ status: CompletionCertificateStatus
36
+ problems: tuple[str, ...] = tuple()
37
+ hard_criterion_ids: tuple[str, ...] = tuple()
38
+ covered_hard_criterion_ids: tuple[str, ...] = tuple()
39
+ failed_hard_criterion_ids: tuple[str, ...] = tuple()
40
+ evidence_hierarchy: tuple[str, ...] = tuple()
41
+ reason: str = ""
42
+ # Baseline-first regression attribution (step 3): failing test ids mechanically
43
+ # split against the pre-edit same-command baseline. pre_existing/agent_authored
44
+ # do not block; regressions/unattributed do (see gate policy).
45
+ regressions: tuple[str, ...] = tuple()
46
+ unattributed_failures: tuple[str, ...] = tuple()
47
+ pre_existing_failures: tuple[str, ...] = tuple()
48
+ agent_authored_failures: tuple[str, ...] = tuple()
49
+ # Turn-contract v2 (step 4): expectation ids left unaddressed at the gate.
50
+ expectations_unaddressed: tuple[str, ...] = tuple()
51
+ # Reproduction-first (step 5): the protocol status for a bug-fix-shaped turn,
52
+ # plus any repro scaffolding still present in the working tree.
53
+ repro_status: str = ""
54
+ repro_artifacts_present: tuple[str, ...] = tuple()
55
+ # Blast radius (step 6): tests that passed in the clean-tree baseline of the
56
+ # selected scope and fail after the change, plus the protocol status.
57
+ blast_radius_status: str = ""
58
+ blast_radius_new_failures: tuple[str, ...] = tuple()
59
+
60
+ def as_payload(self) -> dict[str, Any]:
61
+ return {
62
+ "status": self.status.value,
63
+ "problems": list(self.problems),
64
+ "hard_criterion_ids": list(self.hard_criterion_ids),
65
+ "covered_hard_criterion_ids": list(self.covered_hard_criterion_ids),
66
+ "failed_hard_criterion_ids": list(self.failed_hard_criterion_ids),
67
+ "evidence_hierarchy": list(self.evidence_hierarchy),
68
+ "reason": self.reason,
69
+ "regressions": list(self.regressions),
70
+ "unattributed_failures": list(self.unattributed_failures),
71
+ "pre_existing_failures": list(self.pre_existing_failures),
72
+ "agent_authored_failures": list(self.agent_authored_failures),
73
+ "expectations_unaddressed": list(self.expectations_unaddressed),
74
+ "repro_status": self.repro_status,
75
+ "repro_artifacts_present": list(self.repro_artifacts_present),
76
+ "blast_radius_status": self.blast_radius_status,
77
+ "blast_radius_new_failures": list(self.blast_radius_new_failures),
78
+ }
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class CompletionCertificateInput:
83
+ contract: AcceptanceContract | None
84
+ final_text: str
85
+ blocked: bool
86
+ blocker_valid: bool
87
+ material_edit_count: int
88
+ require_material_result: bool
89
+ verification_expected: bool
90
+ verification_attempt_count: int
91
+ last_verification_passed: bool | None
92
+ failed_verification_commands: set[str] = field(default_factory=set)
93
+ expected_verification_commands: set[str] = field(default_factory=set)
94
+ missing_verification_commands: set[str] = field(default_factory=set)
95
+ verification_coverage_stale: bool = False
96
+ accepted_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
97
+ # Ordering rule (evidence v2): an execute-posture turn that made mutating
98
+ # edits to a verifiable surface requires a qualifying execution-evidence
99
+ # event that ran AFTER the last such edit. Independent of whether any named
100
+ # verification contract exists.
101
+ execution_evidence_required: bool = False
102
+ post_edit_execution_evidence_present: bool = False
103
+ # Baseline-first regression attribution (step 3). The four lists are the
104
+ # aggregated diff of post-edit test failures vs the same-command pre-edit
105
+ # baseline. ``regression_attribution_supersedes_last_failure`` is True when the
106
+ # last verification attempt was a test run and the diff attributes at least one
107
+ # failure as pre-existing/regression/unattributed — so the specific attribution
108
+ # (below) replaces the generic ``verification_failed``: pre-existing-only does
109
+ # not block (sympy-12489), regressions/unattributed get their own problem.
110
+ regression_baseline_enabled: bool = False
111
+ regressions: tuple[str, ...] = tuple()
112
+ unattributed_failures: tuple[str, ...] = tuple()
113
+ pre_existing_failures: tuple[str, ...] = tuple()
114
+ agent_authored_failures: tuple[str, ...] = tuple()
115
+ regression_attribution_supersedes_last_failure: bool = False
116
+ # Turn-contract v2 (step 4). ``expectations_unaddressed`` are the ids of task
117
+ # expectations (expected-output literals / named loci) that were neither
118
+ # mechanically confirmed nor explicitly disposed. They block as a repairable
119
+ # INSUFFICIENT problem (a nudge, bounded, then an honest UNCONFIRMED marker) —
120
+ # never CONTRADICTED, since a missing disposition is not a proven failure.
121
+ turn_contract_v2_enabled: bool = False
122
+ expectations_unaddressed: tuple[str, ...] = tuple()
123
+ # Reproduction-first (step 5). ``repro_unconfirmed`` is True when the turn is
124
+ # bug-fix-shaped, changed product code, and no reproduction of the *reported*
125
+ # symptom was observed failing before the fix and passing after it. It blocks
126
+ # as a repairable INSUFFICIENT problem — except when the reproduction actively
127
+ # still fails (``repro_failing_after_fix``), which is a proven CONTRADICTED
128
+ # result, the same rank as a detected regression. ``repro_artifacts_present``
129
+ # are recorded scaffolding paths still in the tree at finalization.
130
+ reproduction_first_enabled: bool = False
131
+ repro_unconfirmed: bool = False
132
+ repro_failing_after_fix: bool = False
133
+ repro_status: str = ""
134
+ repro_artifacts_present: tuple[str, ...] = tuple()
135
+ # Blast radius (step 6). ``blast_radius_new_failures`` are tests that passed in
136
+ # the clean-tree baseline of the selected scope and fail after the change: proven
137
+ # collateral damage, so they rank CONTRADICTED alongside a step-3 regression.
138
+ # ``blast_radius_unverified`` is the weaker deficit — the scope around the change
139
+ # was never run after it — and blocks as a repairable INSUFFICIENT problem.
140
+ blast_radius_enabled: bool = False
141
+ blast_radius_new_failures: tuple[str, ...] = tuple()
142
+ blast_radius_unverified: bool = False
143
+ blast_radius_status: str = ""
144
+
145
+
146
+ def evaluate_completion_certificate(
147
+ certificate_input: CompletionCertificateInput,
148
+ ) -> CompletionCertificate:
149
+ problems: list[str] = []
150
+ failed_hard: list[str] = []
151
+ covered_hard: list[str] = []
152
+
153
+ if not str(certificate_input.final_text or "").strip():
154
+ problems.append("empty_final_response")
155
+ if certificate_input.blocked:
156
+ if not certificate_input.blocker_valid:
157
+ problems.append("acceptance_evidence_insufficient")
158
+ elif certificate_input.require_material_result and certificate_input.material_edit_count <= 0:
159
+ problems.append("no_material_edits")
160
+
161
+ regression_enabled = certificate_input.regression_baseline_enabled
162
+ has_regressions = regression_enabled and bool(certificate_input.regressions)
163
+ has_unattributed = regression_enabled and bool(certificate_input.unattributed_failures)
164
+
165
+ # When attribution supersedes a failed non-contract run (its failures are all
166
+ # pre-existing / regression / unattributed), the generic verification_failed is
167
+ # replaced by the specific attribution below — but the missing/stale coverage
168
+ # checks must still run, so this only suppresses the failed-run branch itself.
169
+ supersede_last_failure = regression_enabled and bool(
170
+ certificate_input.regression_attribution_supersedes_last_failure
171
+ )
172
+ if certificate_input.verification_expected:
173
+ if certificate_input.verification_attempt_count <= 0:
174
+ problems.append("verification_not_attempted")
175
+ elif certificate_input.failed_verification_commands:
176
+ # A named contract command failed: this always blocks. Regression
177
+ # attribution never clears a contract failure (step 2 stays intact).
178
+ problems.append("verification_failed")
179
+ elif certificate_input.last_verification_passed is not True and not supersede_last_failure:
180
+ problems.append("verification_failed")
181
+ elif certificate_input.missing_verification_commands:
182
+ problems.append("verification_incomplete")
183
+ elif certificate_input.verification_coverage_stale:
184
+ problems.append("verification_incomplete")
185
+
186
+ # Regression attribution problems are independent of which attempt ran last AND
187
+ # of whether a named verify contract resolved: a regression proven by the
188
+ # agent's own before/after runs must block even when verification_expected is
189
+ # False (e.g. no resolvable verify command). No-edit/advisory turns are exempt
190
+ # structurally (the diff is empty without post-edit runs); blocked
191
+ # finalizations are exempt to stay consistent with step 2.
192
+ if regression_enabled and not certificate_input.blocked:
193
+ if has_regressions:
194
+ problems.append("regressions_detected")
195
+ elif has_unattributed and certificate_input.verification_expected:
196
+ problems.append("unattributed_failures")
197
+
198
+ # Turn-contract v2: task-named expectations left unaddressed (neither confirmed
199
+ # by observed evidence nor explicitly disposed) block this execute turn until
200
+ # resolved. Blocked finalizations are exempt (consistent with steps 2-3).
201
+ if (
202
+ certificate_input.turn_contract_v2_enabled
203
+ and not certificate_input.blocked
204
+ and certificate_input.expectations_unaddressed
205
+ ):
206
+ problems.append("expectations_unaddressed")
207
+
208
+ # Reproduction-first: a bug-fix-shaped turn that changed product code without a
209
+ # reproduction that failed before the fix and passes after it has not validated
210
+ # the *reported* symptom, only its own reading of it. Blocked finalizations are
211
+ # exempt (consistent with steps 2-4).
212
+ repro_enabled = certificate_input.reproduction_first_enabled
213
+ repro_unconfirmed = repro_enabled and certificate_input.repro_unconfirmed
214
+ if repro_unconfirmed and not certificate_input.blocked:
215
+ problems.append("repro_unconfirmed")
216
+ if (
217
+ repro_enabled
218
+ and not certificate_input.blocked
219
+ and certificate_input.repro_artifacts_present
220
+ ):
221
+ problems.append("repro_artifacts_present")
222
+
223
+ # Blast radius: a change that broke tests it was not aiming at has failed the
224
+ # task even when its own verification passes. New failures are proven collateral
225
+ # damage; a scope that was never re-run is an unmeasured blast radius. Blocked
226
+ # finalizations are exempt (consistent with steps 2-5).
227
+ blast_radius_enabled = certificate_input.blast_radius_enabled
228
+ has_blast_radius_regressions = blast_radius_enabled and bool(
229
+ certificate_input.blast_radius_new_failures
230
+ )
231
+ if not certificate_input.blocked and blast_radius_enabled:
232
+ if has_blast_radius_regressions:
233
+ problems.append("blast_radius_regressions")
234
+ elif certificate_input.blast_radius_unverified:
235
+ problems.append("blast_radius_unverified")
236
+
237
+ # Ordering rule: post-edit execution evidence. Applies even when no named
238
+ # verification contract exists, catching "edited source, then only ran a
239
+ # syntax check, then finalized". Deduped against the block above.
240
+ if (
241
+ certificate_input.execution_evidence_required
242
+ and not certificate_input.post_edit_execution_evidence_present
243
+ ):
244
+ if certificate_input.verification_attempt_count <= 0:
245
+ problems.append("verification_not_attempted")
246
+ else:
247
+ problems.append("verification_incomplete")
248
+
249
+ hard_criteria = _hard_criteria(certificate_input.contract)
250
+ blocker_covers_missing = certificate_input.blocked and certificate_input.blocker_valid
251
+ for criterion in hard_criteria:
252
+ if criterion.status == AcceptanceCriterionStatus.PASSED:
253
+ covered_hard.append(criterion.criterion_id)
254
+ continue
255
+ if criterion.status == AcceptanceCriterionStatus.FAILED:
256
+ failed_hard.append(criterion.criterion_id)
257
+ problems.append("acceptance_criteria_failed")
258
+ if criterion.kind == AcceptanceCriterionKind.PRESERVATION_UNCHANGED_PATH:
259
+ problems.append("unexpected_scope_changes")
260
+ continue
261
+ if criterion.status == AcceptanceCriterionStatus.BLOCKED:
262
+ if blocker_covers_missing:
263
+ continue
264
+ problems.append("acceptance_evidence_insufficient")
265
+ continue
266
+ if criterion.status == AcceptanceCriterionStatus.UNVERIFIED:
267
+ if blocker_covers_missing:
268
+ continue
269
+ problems.append("acceptance_criteria_unverified")
270
+
271
+ status = CompletionCertificateStatus.SUFFICIENT
272
+ reason = "requirements_satisfied"
273
+ deduped_problems = tuple(dict.fromkeys(problems))
274
+ repro_contradicted = bool(
275
+ repro_enabled
276
+ and certificate_input.repro_failing_after_fix
277
+ and "repro_unconfirmed" in deduped_problems
278
+ )
279
+ if (
280
+ failed_hard
281
+ or "verification_failed" in deduped_problems
282
+ or "regressions_detected" in deduped_problems
283
+ or "blast_radius_regressions" in deduped_problems
284
+ or repro_contradicted
285
+ ):
286
+ status = CompletionCertificateStatus.CONTRADICTED
287
+ reason = "hard_requirement_failed"
288
+ elif deduped_problems:
289
+ status = CompletionCertificateStatus.INSUFFICIENT
290
+ reason = "requirements_missing"
291
+
292
+ return CompletionCertificate(
293
+ status=status,
294
+ problems=deduped_problems,
295
+ hard_criterion_ids=tuple(criterion.criterion_id for criterion in hard_criteria),
296
+ covered_hard_criterion_ids=tuple(covered_hard),
297
+ failed_hard_criterion_ids=tuple(failed_hard),
298
+ evidence_hierarchy=_evidence_hierarchy(certificate_input.accepted_verification_evidence),
299
+ reason=reason,
300
+ regressions=tuple(certificate_input.regressions) if regression_enabled else tuple(),
301
+ unattributed_failures=(
302
+ tuple(certificate_input.unattributed_failures) if regression_enabled else tuple()
303
+ ),
304
+ pre_existing_failures=(
305
+ tuple(certificate_input.pre_existing_failures) if regression_enabled else tuple()
306
+ ),
307
+ agent_authored_failures=(
308
+ tuple(certificate_input.agent_authored_failures) if regression_enabled else tuple()
309
+ ),
310
+ expectations_unaddressed=(
311
+ tuple(certificate_input.expectations_unaddressed)
312
+ if certificate_input.turn_contract_v2_enabled
313
+ else tuple()
314
+ ),
315
+ repro_status=str(certificate_input.repro_status or "") if repro_enabled else "",
316
+ repro_artifacts_present=(
317
+ tuple(certificate_input.repro_artifacts_present) if repro_enabled else tuple()
318
+ ),
319
+ blast_radius_status=(
320
+ str(certificate_input.blast_radius_status or "") if blast_radius_enabled else ""
321
+ ),
322
+ blast_radius_new_failures=(
323
+ tuple(certificate_input.blast_radius_new_failures) if blast_radius_enabled else tuple()
324
+ ),
325
+ )
326
+
327
+
328
+ def _hard_criteria(contract: AcceptanceContract | None) -> list[AcceptanceCriterion]:
329
+ if contract is None:
330
+ return []
331
+ return [
332
+ criterion
333
+ for criterion in contract.criteria
334
+ if criterion.enforcement == AcceptanceCriterionEnforcement.HARD
335
+ and criterion.required
336
+ and criterion.required_for_finalization
337
+ ]
338
+
339
+
340
+ def _evidence_hierarchy(evidence: list[dict[str, Any]]) -> tuple[str, ...]:
341
+ origins = set()
342
+ for item in evidence:
343
+ if not isinstance(item, dict):
344
+ continue
345
+ raw = str(
346
+ item.get("origin")
347
+ or item.get("evidence_origin")
348
+ or item.get("evidence_category")
349
+ or item.get("category")
350
+ or ""
351
+ )
352
+ if not raw:
353
+ continue
354
+ origins.add(
355
+ {
356
+ "AUTHORITATIVE": "HOST_AUTHORITATIVE",
357
+ "TASK_ACCEPTANCE": "DIRECT_BLACK_BOX",
358
+ "REPO_NATIVE": "PREEXISTING_REPO_NATIVE",
359
+ }.get(raw, raw)
360
+ )
361
+ return tuple(
362
+ sorted(
363
+ (origin for origin in origins if origin),
364
+ key=lambda origin: (_EVIDENCE_RANK.get(origin, 99), origin),
365
+ )
366
+ )