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,729 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import subprocess
6
+ import warnings
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .config import (
12
+ AppConfig,
13
+ ConfigError,
14
+ get_api_key,
15
+ resolve_llm_timeout_s,
16
+ resolve_model_access_api_key,
17
+ resolve_prompt_cache_key,
18
+ resolve_prompt_cache_retention,
19
+ resolve_role_temperature,
20
+ )
21
+ from .execution_shared import safe_task_file_component
22
+ from .forge import RunPaths, ensure_execution_dirs, now_iso
23
+ from .git_safe import build_git_cmd
24
+ from .llm.base import ChatClient
25
+ from .llm.factory import _resolve_base_url, make_llm_client
26
+ from .llm.openai_compat import OpenAICompatClient as _OpenAICompatClient
27
+ from .llm.types import LLMError
28
+ from .model_metadata_policy import (
29
+ ActiveModelRef,
30
+ ModelMetadataPolicyError,
31
+ evaluate_active_model_metadata_policy,
32
+ )
33
+ from .model_registry import ModelRegistry
34
+ from .model_router import ROLE_CONFLICT_REVIEW, resolve_model_for_role
35
+ from .profiles import get_active_profile
36
+
37
+ _ALLOWED_RESOLUTION = {"take_ours", "take_theirs", "manual_merge"}
38
+ _ALLOWED_CONFIDENCE = {"high", "medium", "low"}
39
+ _ALLOWED_RISK = {"low", "medium", "high"}
40
+ _CONFLICT_REVIEW_TRANSIENT_REQUEST_MAX_ATTEMPTS = 2
41
+ _RETRYABLE_LLM_HTTP_STATUSES = {408, 429, 500, 502, 503, 504}
42
+ _RETRYABLE_LLM_REQUEST_ERROR_MARKERS = (
43
+ "connection aborted",
44
+ "connection reset",
45
+ "connect timeout",
46
+ "connecttimeout",
47
+ "eof",
48
+ "pool timeout",
49
+ "read error",
50
+ "read timeout",
51
+ "readtimeout",
52
+ "remote protocol error",
53
+ "temporarily unavailable",
54
+ "timed out",
55
+ )
56
+ _REQUEST_RETRY_STATE_NONE = "none"
57
+ _REQUEST_RETRY_STATE_RECOVERED = "recovered"
58
+ _REQUEST_RETRY_STATE_EXHAUSTED = "exhausted"
59
+ _REQUEST_RETRY_STATE_FINAL_FAILURE_AFTER_RETRY = "final_failure_after_retry"
60
+
61
+ # Compatibility surface for tests and downstream monkeypatches that predate the LLM factory.
62
+ OpenAICompatClient = _OpenAICompatClient
63
+
64
+ MERGE_CONFLICT_REVIEWER_SYSTEM_PROMPT = """You are a strict merge-conflict reviewer.
65
+
66
+ Goal
67
+ - Analyze the merge conflict context (base/ours/theirs/working excerpts) and recommend the safest resolution strategy.
68
+ - Be conservative: when uncertain, recommend manual_merge and explain why.
69
+
70
+ Guidelines
71
+ - Prefer minimal, low-risk merges that preserve intended behavior.
72
+ - Consider task acceptance criteria and write scope constraints if provided.
73
+ - Call out risks (build breaks, semantic changes, missing tests, formatting traps).
74
+
75
+ Output
76
+ - Return valid JSON only, strictly matching the schema requested by the user prompt.
77
+ """
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class ConflictReviewOutcome:
82
+ review_json: dict[str, Any] | None
83
+ review_markdown: str
84
+ skipped_reason: str | None
85
+ request_retry_count: int = 0
86
+ request_retry_state: str = _REQUEST_RETRY_STATE_NONE
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class ConflictArtifactPaths:
91
+ conflict_dir: Path
92
+ context_json_path: Path
93
+ review_json_path: Path | None
94
+ review_md_path: Path
95
+ cleanup_log_path: Path
96
+
97
+
98
+ def _run_git(root: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
99
+ return subprocess.run(
100
+ build_git_cmd(root, args),
101
+ check=False,
102
+ capture_output=True,
103
+ text=True,
104
+ )
105
+
106
+
107
+ def _truncate_head_tail(text: str, *, max_chars: int = 8000) -> str:
108
+ if len(text) <= max_chars:
109
+ return text
110
+ half = max_chars // 2
111
+ omitted = len(text) - max_chars
112
+ return text[:half] + f"\n\n... [truncated {omitted} chars] ...\n\n" + text[-half:]
113
+
114
+
115
+ def _read_text_excerpt(path: Path, *, max_chars: int = 8000) -> str:
116
+ if not path.exists() or not path.is_file():
117
+ return ""
118
+ try:
119
+ content = path.read_text(encoding="utf-8", errors="replace")
120
+ except OSError:
121
+ return ""
122
+ return _truncate_head_tail(content, max_chars=max_chars)
123
+
124
+
125
+ def _extract_json_object(raw: str) -> dict[str, Any]:
126
+ text = raw.strip()
127
+ try:
128
+ parsed = json.loads(text)
129
+ except json.JSONDecodeError:
130
+ parsed = None
131
+ if isinstance(parsed, dict):
132
+ return parsed
133
+
134
+ start = text.find("{")
135
+ end = text.rfind("}")
136
+ if start != -1 and end != -1 and end > start:
137
+ sliced = text[start : end + 1]
138
+ parsed = json.loads(sliced)
139
+ if isinstance(parsed, dict):
140
+ return parsed
141
+ raise ValueError("model response is not a valid JSON object")
142
+
143
+
144
+ def _normalize_per_file(value: Any) -> list[dict[str, str]]:
145
+ if not isinstance(value, list):
146
+ raise ValueError("per_file must be a list")
147
+ out: list[dict[str, str]] = []
148
+ for idx, item in enumerate(value, start=1):
149
+ if not isinstance(item, dict):
150
+ raise ValueError(f"per_file[{idx}] must be an object")
151
+ path = str(item.get("path") or "").strip()
152
+ recommended_resolution = str(item.get("recommended_resolution") or "").strip().lower()
153
+ notes = str(item.get("notes") or "").strip()
154
+ risk = str(item.get("risk") or "").strip().lower()
155
+ if recommended_resolution not in _ALLOWED_RESOLUTION:
156
+ raise ValueError(
157
+ f"per_file[{idx}].recommended_resolution must be one of "
158
+ "take_ours|take_theirs|manual_merge"
159
+ )
160
+ if risk not in _ALLOWED_RISK:
161
+ raise ValueError(f"per_file[{idx}].risk must be one of low|medium|high")
162
+ out.append(
163
+ {
164
+ "path": path or "(unknown)",
165
+ "recommended_resolution": recommended_resolution,
166
+ "notes": notes or "(no notes)",
167
+ "risk": risk,
168
+ }
169
+ )
170
+ return out
171
+
172
+
173
+ def _normalize_review_json(raw: dict[str, Any], *, task_id: str) -> dict[str, Any]:
174
+ confidence = str(raw.get("confidence") or "").strip().lower()
175
+ summary = str(raw.get("summary") or "").strip()
176
+ root_cause = str(raw.get("root_cause") or "").strip()
177
+ strategy = str(raw.get("recommended_strategy") or "").strip()
178
+
179
+ if confidence not in _ALLOWED_CONFIDENCE:
180
+ raise ValueError("confidence must be high|medium|low")
181
+ if not summary:
182
+ raise ValueError("summary is required")
183
+ if not root_cause:
184
+ raise ValueError("root_cause is required")
185
+ if not strategy:
186
+ raise ValueError("recommended_strategy is required")
187
+
188
+ next_steps_raw = raw.get("next_steps")
189
+ if not isinstance(next_steps_raw, list):
190
+ raise ValueError("next_steps must be a list")
191
+ next_steps = [str(item).strip() for item in next_steps_raw if str(item).strip()]
192
+
193
+ return {
194
+ "task_id": task_id,
195
+ "confidence": confidence,
196
+ "summary": summary,
197
+ "root_cause": root_cause,
198
+ "recommended_strategy": strategy,
199
+ "per_file": _normalize_per_file(raw.get("per_file")),
200
+ "next_steps": next_steps,
201
+ "generated_at": now_iso(),
202
+ }
203
+
204
+
205
+ def _llm_error_status_code(err: LLMError) -> int | None:
206
+ match = re.match(r"LLM error (\d{3}):", str(err or "").strip())
207
+ if match is None:
208
+ return None
209
+ try:
210
+ return int(match.group(1))
211
+ except ValueError:
212
+ return None
213
+
214
+
215
+ def _is_retryable_conflict_review_request_error(err: LLMError) -> bool:
216
+ status_code = _llm_error_status_code(err)
217
+ if status_code is not None:
218
+ return status_code in _RETRYABLE_LLM_HTTP_STATUSES
219
+
220
+ message = str(err or "").casefold().strip()
221
+ if not message.startswith("llm request failed:"):
222
+ return False
223
+ return any(marker in message for marker in _RETRYABLE_LLM_REQUEST_ERROR_MARKERS)
224
+
225
+
226
+ def _attach_conflict_review_retry_details(
227
+ err: LLMError,
228
+ *,
229
+ retry_count: int,
230
+ exhausted: bool,
231
+ ) -> LLMError:
232
+ err.conflict_review_request_retry_count = int(retry_count) # type: ignore[attr-defined]
233
+ err.conflict_review_request_retry_exhausted = bool(exhausted) # type: ignore[attr-defined]
234
+ return err
235
+
236
+
237
+ def _request_retry_state_for_terminal_outcome(
238
+ *,
239
+ request_retry_count: int,
240
+ request_retry_exhausted: bool = False,
241
+ skipped_reason: str | None,
242
+ ) -> str:
243
+ if request_retry_count <= 0:
244
+ return _REQUEST_RETRY_STATE_NONE
245
+ if not skipped_reason:
246
+ return _REQUEST_RETRY_STATE_RECOVERED
247
+ if request_retry_exhausted:
248
+ return _REQUEST_RETRY_STATE_EXHAUSTED
249
+ return _REQUEST_RETRY_STATE_FINAL_FAILURE_AFTER_RETRY
250
+
251
+
252
+ def _request_retry_line(*, retry_count: int, retry_state: str) -> str | None:
253
+ if retry_count <= 0:
254
+ return None
255
+ retry_word = "retry" if retry_count == 1 else "retries"
256
+ if retry_state == _REQUEST_RETRY_STATE_RECOVERED:
257
+ return f"- Request Retries: {retry_count} transient {retry_word} before successful review."
258
+ if retry_state == _REQUEST_RETRY_STATE_EXHAUSTED:
259
+ return (
260
+ f"- Request Retries: {retry_count} transient {retry_word} exhausted before review "
261
+ "was skipped."
262
+ )
263
+ if retry_state == _REQUEST_RETRY_STATE_FINAL_FAILURE_AFTER_RETRY:
264
+ return (
265
+ f"- Request Retries: {retry_count} transient {retry_word} before final review failure."
266
+ )
267
+ return None
268
+
269
+
270
+ def _request_conflict_review_response(
271
+ *,
272
+ client: ChatClient,
273
+ messages: list[dict[str, str]],
274
+ ) -> tuple[Any, int]:
275
+ retry_count = 0
276
+ for attempt in range(1, _CONFLICT_REVIEW_TRANSIENT_REQUEST_MAX_ATTEMPTS + 1):
277
+ try:
278
+ return (
279
+ client.chat(
280
+ messages=messages,
281
+ stream=False,
282
+ ),
283
+ retry_count,
284
+ )
285
+ except LLMError as e:
286
+ retryable = _is_retryable_conflict_review_request_error(e)
287
+ if not retryable or attempt >= _CONFLICT_REVIEW_TRANSIENT_REQUEST_MAX_ATTEMPTS:
288
+ # TODO(failure-category-audit): default=implementation_failed,
289
+ # site=merge conflict reviewer retry exhaustion outside Forge result trend, see failure_category.py
290
+ raise _attach_conflict_review_retry_details(
291
+ e,
292
+ retry_count=retry_count,
293
+ exhausted=retryable
294
+ and attempt >= _CONFLICT_REVIEW_TRANSIENT_REQUEST_MAX_ATTEMPTS,
295
+ ) from e
296
+ retry_count += 1
297
+ raise AssertionError("unreachable")
298
+
299
+
300
+ def _make_conflict_review_llm_client(
301
+ *,
302
+ cfg: AppConfig,
303
+ api_key: str,
304
+ model_name: str,
305
+ ) -> ChatClient:
306
+ temperature = resolve_role_temperature(cfg, role="conflict_review")
307
+ if OpenAICompatClient is _OpenAICompatClient:
308
+ return make_llm_client(
309
+ cfg=cfg,
310
+ api_key=api_key,
311
+ model=model_name,
312
+ timeout_s=resolve_llm_timeout_s(cfg),
313
+ temperature=temperature,
314
+ prompt_cache_key=resolve_prompt_cache_key(cfg),
315
+ prompt_cache_retention=resolve_prompt_cache_retention(cfg),
316
+ )
317
+
318
+ profile = get_active_profile(cfg)
319
+ return OpenAICompatClient(
320
+ base_url=_resolve_base_url(cfg=cfg, profile=profile),
321
+ api_key=api_key,
322
+ model=model_name,
323
+ timeout_s=resolve_llm_timeout_s(cfg),
324
+ temperature=temperature,
325
+ prompt_cache_key=resolve_prompt_cache_key(cfg),
326
+ prompt_cache_retention=resolve_prompt_cache_retention(cfg),
327
+ extra_headers=profile.extra_headers,
328
+ )
329
+
330
+
331
+ def _review_markdown(
332
+ *,
333
+ task: dict[str, Any],
334
+ context: dict[str, Any],
335
+ review: dict[str, Any] | None,
336
+ skipped_reason: str | None,
337
+ request_retry_count: int = 0,
338
+ request_retry_state: str = _REQUEST_RETRY_STATE_NONE,
339
+ ) -> str:
340
+ task_id = str(task.get("id") or "")
341
+ title = str(task.get("title") or "")
342
+ lines: list[str] = [
343
+ f"# Merge Conflict Review: {task_id}",
344
+ "",
345
+ f"- Task Title: {title}",
346
+ f"- Generated At: {now_iso()}",
347
+ ]
348
+ retry_line = _request_retry_line(
349
+ retry_count=request_retry_count,
350
+ retry_state=request_retry_state,
351
+ )
352
+ if retry_line is not None:
353
+ lines.append(retry_line)
354
+ lines.append("")
355
+
356
+ unmerged_files = context.get("unmerged_files")
357
+ if isinstance(unmerged_files, list) and unmerged_files:
358
+ lines.append("## Unmerged Files")
359
+ lines.append("")
360
+ for item in unmerged_files:
361
+ lines.append(f"- `{item}`")
362
+ lines.append("")
363
+
364
+ if skipped_reason:
365
+ lines.extend(
366
+ [
367
+ "## Review Status",
368
+ "",
369
+ f"LLM conflict review skipped: {skipped_reason}",
370
+ "",
371
+ "## Suggested Next Steps",
372
+ "",
373
+ "- Resolve conflicts manually in listed files.",
374
+ "- Commit the resolution on the task branch.",
375
+ "- Retry merge after resolution.",
376
+ ]
377
+ )
378
+ return "\n".join(lines).rstrip() + "\n"
379
+
380
+ assert review is not None
381
+ lines.extend(
382
+ [
383
+ "## Summary",
384
+ "",
385
+ review["summary"],
386
+ "",
387
+ "## Root Cause",
388
+ "",
389
+ review["root_cause"],
390
+ "",
391
+ "## Recommended Strategy",
392
+ "",
393
+ review["recommended_strategy"],
394
+ "",
395
+ f"- Confidence: {review['confidence']}",
396
+ "",
397
+ "## Per-file Resolution",
398
+ "",
399
+ ]
400
+ )
401
+ if review["per_file"]:
402
+ for entry in review["per_file"]:
403
+ lines.append(
404
+ f"- `{entry['path']}` -> {entry['recommended_resolution']} "
405
+ f"(risk={entry['risk']}): {entry['notes']}"
406
+ )
407
+ else:
408
+ lines.append("- (none)")
409
+
410
+ lines.extend(["", "## Next Steps", ""])
411
+ if review["next_steps"]:
412
+ for step in review["next_steps"]:
413
+ lines.append(f"- {step}")
414
+ else:
415
+ lines.append("- Resolve conflicts manually and retry merge.")
416
+
417
+ return "\n".join(lines).rstrip() + "\n"
418
+
419
+
420
+ def list_unmerged_files(root: Path) -> list[str]:
421
+ cp = _run_git(root, ["diff", "--name-only", "--diff-filter=U"])
422
+ if cp.returncode != 0:
423
+ return []
424
+ return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
425
+
426
+
427
+ def read_stage_blob(root: Path, stage: int, path: str) -> str:
428
+ if stage not in {1, 2, 3}:
429
+ raise ValueError("stage must be one of: 1, 2, 3")
430
+ cp = _run_git(root, ["show", f":{stage}:{path}"])
431
+ if cp.returncode != 0:
432
+ return ""
433
+ return cp.stdout
434
+
435
+
436
+ def capture_merge_conflict_context(
437
+ root: Path,
438
+ *,
439
+ base_branch: str,
440
+ task_branch: str,
441
+ merge_error: str,
442
+ ) -> dict[str, Any]:
443
+ status_cp = _run_git(root, ["status", "--porcelain=v1"])
444
+ git_status = status_cp.stdout if status_cp.returncode == 0 else (status_cp.stderr or "")
445
+
446
+ files = list_unmerged_files(root)
447
+ per_file: list[dict[str, str]] = []
448
+ for path in files:
449
+ working_excerpt = _read_text_excerpt(root / path)
450
+ per_file.append(
451
+ {
452
+ "path": path,
453
+ "base_version_excerpt": _truncate_head_tail(read_stage_blob(root, 1, path)),
454
+ "ours_version_excerpt": _truncate_head_tail(read_stage_blob(root, 2, path)),
455
+ "theirs_version_excerpt": _truncate_head_tail(read_stage_blob(root, 3, path)),
456
+ "working_excerpt": working_excerpt,
457
+ }
458
+ )
459
+
460
+ return {
461
+ "base_branch": base_branch,
462
+ "task_branch": task_branch,
463
+ "merge_error": merge_error,
464
+ "git_status_porcelain": _truncate_head_tail(git_status, max_chars=12000),
465
+ "unmerged_files": files,
466
+ "files": per_file,
467
+ "captured_at": now_iso(),
468
+ }
469
+
470
+
471
+ def try_abort_merge(root: Path, *, base_branch: str) -> tuple[bool, str]:
472
+ logs: list[str] = []
473
+
474
+ def _record(cp: subprocess.CompletedProcess[str], cmd: str) -> None:
475
+ out = (cp.stdout or "").strip()
476
+ err = (cp.stderr or "").strip()
477
+ logs.append(f"$ {cmd}")
478
+ logs.append(f"returncode={cp.returncode}")
479
+ if out:
480
+ logs.append("stdout:")
481
+ logs.append(out)
482
+ if err:
483
+ logs.append("stderr:")
484
+ logs.append(err)
485
+ logs.append("")
486
+
487
+ abort_cp = _run_git(root, ["merge", "--abort"])
488
+ _record(abort_cp, f"git -C {root} merge --abort")
489
+ abort_ok = abort_cp.returncode == 0
490
+
491
+ reset_ok = False
492
+ if not abort_ok:
493
+ reset_cp = _run_git(root, ["reset", "--hard"])
494
+ _record(reset_cp, f"git -C {root} reset --hard")
495
+ reset_ok = reset_cp.returncode == 0
496
+
497
+ checkout_cp = _run_git(root, ["checkout", base_branch])
498
+ _record(checkout_cp, f"git -C {root} checkout {base_branch}")
499
+ checkout_ok = checkout_cp.returncode == 0
500
+
501
+ ok = (abort_ok or reset_ok) and checkout_ok
502
+ return ok, "\n".join(logs).rstrip() + "\n"
503
+
504
+
505
+ def review_merge_conflict(
506
+ *,
507
+ paths: RunPaths,
508
+ task: dict[str, Any],
509
+ cfg: AppConfig,
510
+ api_key_override: str | None,
511
+ context: dict[str, Any],
512
+ plan: dict[str, Any] | None = None,
513
+ ) -> ConflictReviewOutcome:
514
+ task_id = str(task.get("id") or "").strip() or "(unknown)"
515
+
516
+ try:
517
+ api_key = resolve_model_access_api_key(
518
+ cfg,
519
+ override=api_key_override,
520
+ legacy_resolver=get_api_key,
521
+ )
522
+ except ConfigError as exc:
523
+ reason = str(exc)
524
+ review_md = _review_markdown(
525
+ task=task,
526
+ context=context,
527
+ review=None,
528
+ skipped_reason=reason,
529
+ request_retry_count=0,
530
+ request_retry_state=_REQUEST_RETRY_STATE_NONE,
531
+ )
532
+ return ConflictReviewOutcome(
533
+ review_json=None,
534
+ review_markdown=review_md,
535
+ skipped_reason=reason,
536
+ request_retry_count=0,
537
+ request_retry_state=_REQUEST_RETRY_STATE_NONE,
538
+ )
539
+
540
+ prompt_payload = {
541
+ "task": {
542
+ "id": task.get("id", ""),
543
+ "title": task.get("title", ""),
544
+ "description": task.get("description", ""),
545
+ "acceptance_criteria": task.get("acceptance_criteria", []),
546
+ "write_scope": task.get("write_scope", []),
547
+ "estimated_files": task.get("estimated_files", []),
548
+ "branch": task.get("branch", ""),
549
+ },
550
+ "conflict_context": context,
551
+ }
552
+ schema_hint = {
553
+ "task_id": task_id,
554
+ "confidence": "high|medium|low",
555
+ "summary": "string",
556
+ "root_cause": "string",
557
+ "recommended_strategy": "string",
558
+ "per_file": [
559
+ {
560
+ "path": "src/x.py",
561
+ "recommended_resolution": "take_ours|take_theirs|manual_merge",
562
+ "notes": "string",
563
+ "risk": "low|medium|high",
564
+ }
565
+ ],
566
+ "next_steps": ["string"],
567
+ }
568
+
569
+ prompt = (
570
+ "Analyze this git merge conflict and return STRICT JSON only.\n\n"
571
+ "Rubric:\n"
572
+ "- root cause of conflict\n"
573
+ "- safest resolution strategy per file\n"
574
+ "- risks of taking ours/theirs\n"
575
+ "- concrete next steps\n\n"
576
+ f"Input:\n{json.dumps(prompt_payload, ensure_ascii=True, indent=2)}\n\n"
577
+ f"Required schema:\n{json.dumps(schema_hint, ensure_ascii=True, indent=2)}\n"
578
+ )
579
+ model_name = resolve_model_for_role(
580
+ cfg=cfg,
581
+ role=ROLE_CONFLICT_REVIEW,
582
+ plan=plan,
583
+ prefer_context="forge",
584
+ )
585
+ registry = ModelRegistry(cfg=cfg, api_key=api_key)
586
+ try:
587
+ metadata_policy_result = evaluate_active_model_metadata_policy(
588
+ cfg=cfg,
589
+ registry=registry,
590
+ active_models=[ActiveModelRef(role=ROLE_CONFLICT_REVIEW, model_name=model_name)],
591
+ )
592
+ except ModelMetadataPolicyError as e:
593
+ raise ConfigError(str(e)) from e
594
+ for warning_message in metadata_policy_result.warning_messages:
595
+ warnings.warn(warning_message, stacklevel=2)
596
+
597
+ client = _make_conflict_review_llm_client(
598
+ cfg=cfg,
599
+ api_key=api_key,
600
+ model_name=model_name,
601
+ )
602
+ request_retry_count = 0
603
+ try:
604
+ response, request_retry_count = _request_conflict_review_response(
605
+ client=client,
606
+ messages=[
607
+ {
608
+ "role": "system",
609
+ "content": MERGE_CONFLICT_REVIEWER_SYSTEM_PROMPT,
610
+ },
611
+ {"role": "user", "content": prompt},
612
+ ],
613
+ )
614
+ raw = _extract_json_object(response.content)
615
+ review_json = _normalize_review_json(raw, task_id=task_id)
616
+ review_md = _review_markdown(
617
+ task=task,
618
+ context=context,
619
+ review=review_json,
620
+ skipped_reason=None,
621
+ request_retry_count=request_retry_count,
622
+ request_retry_state=_request_retry_state_for_terminal_outcome(
623
+ request_retry_count=request_retry_count,
624
+ skipped_reason=None,
625
+ ),
626
+ )
627
+ return ConflictReviewOutcome(
628
+ review_json=review_json,
629
+ review_markdown=review_md,
630
+ skipped_reason=None,
631
+ request_retry_count=request_retry_count,
632
+ request_retry_state=_request_retry_state_for_terminal_outcome(
633
+ request_retry_count=request_retry_count,
634
+ skipped_reason=None,
635
+ ),
636
+ )
637
+ except LLMError as e:
638
+ request_retry_count = int(getattr(e, "conflict_review_request_retry_count", 0) or 0)
639
+ request_retry_exhausted = bool(
640
+ getattr(e, "conflict_review_request_retry_exhausted", False) or False
641
+ )
642
+ retry_word = "retry" if request_retry_count == 1 else "retries"
643
+ reason = f"review failed: {e}"
644
+ if request_retry_count > 0:
645
+ reason = f"review failed after {request_retry_count} transient {retry_word}: {e}"
646
+ retry_state = _request_retry_state_for_terminal_outcome(
647
+ request_retry_count=request_retry_count,
648
+ request_retry_exhausted=request_retry_exhausted,
649
+ skipped_reason=reason,
650
+ )
651
+ review_md = _review_markdown(
652
+ task=task,
653
+ context=context,
654
+ review=None,
655
+ skipped_reason=reason,
656
+ request_retry_count=request_retry_count,
657
+ request_retry_state=retry_state,
658
+ )
659
+ return ConflictReviewOutcome(
660
+ review_json=None,
661
+ review_markdown=review_md,
662
+ skipped_reason=reason,
663
+ request_retry_count=request_retry_count,
664
+ request_retry_state=retry_state,
665
+ )
666
+ except (ValueError, json.JSONDecodeError) as e:
667
+ reason = f"review failed: {e}"
668
+ retry_state = _request_retry_state_for_terminal_outcome(
669
+ request_retry_count=request_retry_count,
670
+ skipped_reason=reason,
671
+ )
672
+ review_md = _review_markdown(
673
+ task=task,
674
+ context=context,
675
+ review=None,
676
+ skipped_reason=reason,
677
+ request_retry_count=request_retry_count,
678
+ request_retry_state=retry_state,
679
+ )
680
+ return ConflictReviewOutcome(
681
+ review_json=None,
682
+ review_markdown=review_md,
683
+ skipped_reason=reason,
684
+ request_retry_count=request_retry_count,
685
+ request_retry_state=retry_state,
686
+ )
687
+
688
+
689
+ def write_conflict_artifacts(
690
+ *,
691
+ paths: RunPaths,
692
+ task_id: str,
693
+ context: dict[str, Any],
694
+ review_json: dict[str, Any] | None,
695
+ review_md: str,
696
+ cleanup_log: str,
697
+ ) -> ConflictArtifactPaths:
698
+ ensure_execution_dirs(paths)
699
+ conflict_root = paths.execution_dir / "conflicts"
700
+ conflict_root.mkdir(parents=True, exist_ok=True)
701
+
702
+ safe = safe_task_file_component(task_id)
703
+ out_dir = conflict_root / safe
704
+ out_dir.mkdir(parents=True, exist_ok=True)
705
+
706
+ context_json_path = out_dir / "conflict_context.json"
707
+ review_json_path = out_dir / "conflict_review.json" if review_json is not None else None
708
+ review_md_path = out_dir / "conflict_review.md"
709
+ cleanup_log_path = out_dir / "merge_cleanup.log"
710
+
711
+ context_json_path.write_text(
712
+ json.dumps(context, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
713
+ encoding="utf-8",
714
+ )
715
+ if review_json_path is not None:
716
+ review_json_path.write_text(
717
+ json.dumps(review_json, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
718
+ encoding="utf-8",
719
+ )
720
+ review_md_path.write_text(review_md.rstrip() + "\n", encoding="utf-8")
721
+ cleanup_log_path.write_text(cleanup_log.rstrip() + "\n", encoding="utf-8")
722
+
723
+ return ConflictArtifactPaths(
724
+ conflict_dir=out_dir,
725
+ context_json_path=context_json_path,
726
+ review_json_path=review_json_path,
727
+ review_md_path=review_md_path,
728
+ cleanup_log_path=cleanup_log_path,
729
+ )