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,1099 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import warnings
5
+ from dataclasses import dataclass
6
+ from fnmatch import fnmatchcase
7
+ from pathlib import PurePosixPath
8
+ from typing import Any, Literal
9
+
10
+ from .assets.models import AssetError
11
+ from .assets.plan_binding import task_asset_briefing
12
+ from .direction_change import filter_obsolete_direction_paths
13
+ from .failure_category import FailureCategory
14
+ from .file_classification import (
15
+ CONTENT_SURFACE_EXTENSIONS,
16
+ FRONTEND_SURFACE_EXTENSIONS,
17
+ describe_path_kinds,
18
+ is_code_implementation_path,
19
+ is_test_path,
20
+ )
21
+ from .mcp.forge_scope import normalize_task_mcp_scope
22
+ from .planning_constraints import (
23
+ planning_constraints_from_plan,
24
+ task_scope_constraint_violations,
25
+ )
26
+ from .swarm_scheduler import canonical_task_status
27
+ from .task_readiness import (
28
+ EXECUTION_UNREADY_SCOPE_WARNING,
29
+ TASK_KIND_ANALYSIS_ONLY,
30
+ classify_task_lifecycle,
31
+ normalize_existing_task_scope_fields,
32
+ normalized_text_list,
33
+ status_is_execution_candidate,
34
+ task_is_missing_runnable_scope,
35
+ task_readiness_warning,
36
+ task_requires_runnable_file_scope,
37
+ )
38
+ from .task_scope import (
39
+ extract_forbidden_repo_path_hints,
40
+ extract_repo_path_hints,
41
+ is_agent_internal_scope_path,
42
+ split_normalized_repo_path_list,
43
+ )
44
+
45
+ _NON_EXECUTABLE_OBSOLETE_STATUSES = frozenset({"superseded", "invalidated"})
46
+ PlanAcceptanceRuleId = Literal["R1", "R2", "R3", "R4", "R5"]
47
+
48
+ # Attached to every R3/R4 scope issue. Execution triages out-of-scope changes rather than
49
+ # rejecting them outright -- adjacent changes are amended into write_scope automatically --
50
+ # so the planner's job is to name the directories the work lives in, not to guess the exact
51
+ # filenames. An exact file list that misses one file costs a round trip; a directory glob
52
+ # does not.
53
+ SCOPE_GLOB_PREFERENCE_GUIDANCE = (
54
+ "Prefer directory-level globs over exact file lists in write_scope: write "
55
+ "`src/pkg/**` or `tests/**` rather than enumerating every file you expect to touch. "
56
+ "Execution auto-amends adjacent changes (a new file in a declared directory, a sibling "
57
+ "test file, a generated artifact of a declared file) into write_scope, but an unrelated "
58
+ "path still blocks the task, so declare the directories the work belongs to. Keep the "
59
+ "globs as narrow as the work: scope a package directory, not the repository root."
60
+ )
61
+
62
+
63
+ def _with_scope_guidance(detail: str) -> str:
64
+ text = str(detail or "").strip()
65
+ return f"{text} {SCOPE_GLOB_PREFERENCE_GUIDANCE}" if text else SCOPE_GLOB_PREFERENCE_GUIDANCE
66
+
67
+
68
+ class PlannerFailedError(RuntimeError):
69
+ failure_category = FailureCategory.PLANNER_FAILED
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class PlanAcceptanceIssue:
74
+ rule_id: PlanAcceptanceRuleId
75
+ observed: str
76
+ task_id: str | None = None
77
+ detail: str = ""
78
+
79
+
80
+ def _task_ids(tasks: list[dict[str, Any]]) -> list[str]:
81
+ ids: list[str] = []
82
+ seen: set[str] = set()
83
+ for task in tasks:
84
+ task_id = str(task.get("id") or "").strip()
85
+ if not task_id or task_id in seen:
86
+ continue
87
+ seen.add(task_id)
88
+ ids.append(task_id)
89
+ return ids
90
+
91
+
92
+ def _string_list(value: Any) -> list[str]:
93
+ if not isinstance(value, list):
94
+ return []
95
+ out: list[str] = []
96
+ for item in value:
97
+ text = str(item).strip()
98
+ if text:
99
+ out.append(text)
100
+ return out
101
+
102
+
103
+ def _raw_text_list(value: Any) -> list[str]:
104
+ if not isinstance(value, list):
105
+ return []
106
+ out: list[str] = []
107
+ for item in value:
108
+ text = str(item).strip()
109
+ if text:
110
+ out.append(text)
111
+ return out
112
+
113
+
114
+ def _parse_only_ids(only: str | None) -> set[str] | None:
115
+ if only is None:
116
+ return None
117
+ ids = {part.strip() for part in only.split(",") if part.strip()}
118
+ return ids or None
119
+
120
+
121
+ def _task_id_for_issue(task: dict[str, Any], index: int) -> str:
122
+ return str(task.get("id") or "").strip() or f"task[{index}]"
123
+
124
+
125
+ def _candidate_execution_tasks(
126
+ plan: dict[str, Any],
127
+ *,
128
+ retry_failed: bool,
129
+ retry_changes_requested: bool,
130
+ retry_merge_conflicts: bool,
131
+ only: str | None,
132
+ ) -> list[tuple[int, dict[str, Any], str, str]]:
133
+ only_ids = _parse_only_ids(only)
134
+ tasks_raw = plan.get("tasks")
135
+ if not isinstance(tasks_raw, list):
136
+ return []
137
+
138
+ candidates: list[tuple[int, dict[str, Any], str, str]] = []
139
+ for index, task in enumerate(tasks_raw):
140
+ if not isinstance(task, dict):
141
+ continue
142
+ task_id = _task_id_for_issue(task, index)
143
+ status = canonical_task_status(str(task.get("status") or ""))
144
+ if (
145
+ only_ids is not None
146
+ and task_id not in only_ids
147
+ and status != "ready_for_merge"
148
+ and not (retry_merge_conflicts and status == "merge_conflict")
149
+ ):
150
+ continue
151
+ if not status_is_execution_candidate(
152
+ status,
153
+ retry_failed=retry_failed,
154
+ retry_changes_requested=retry_changes_requested,
155
+ retry_merge_conflicts=retry_merge_conflicts,
156
+ ):
157
+ continue
158
+ candidates.append((index, task, task_id, status))
159
+ return candidates
160
+
161
+
162
+ def _mutating_task_scope(task: dict[str, Any]) -> tuple[bool, list[str], list[str]]:
163
+ estimated_files, write_scope = normalize_existing_task_scope_fields(
164
+ estimated_files=task.get("estimated_files"),
165
+ write_scope=task.get("write_scope"),
166
+ )
167
+ requires_runnable_scope = task_requires_runnable_file_scope(
168
+ title=str(task.get("title") or "").strip(),
169
+ description=str(task.get("description") or "").strip(),
170
+ acceptance_criteria=normalized_text_list(task.get("acceptance_criteria")),
171
+ estimated_files=estimated_files,
172
+ write_scope=write_scope,
173
+ )
174
+ return requires_runnable_scope, estimated_files, write_scope
175
+
176
+
177
+ def _missing_field_issue(
178
+ *,
179
+ task_id: str,
180
+ field_name: str,
181
+ ) -> PlanAcceptanceIssue:
182
+ detail = (
183
+ _with_scope_guidance(EXECUTION_UNREADY_SCOPE_WARNING) if field_name == "write_scope" else ""
184
+ )
185
+ return PlanAcceptanceIssue(
186
+ rule_id="R4",
187
+ task_id=task_id,
188
+ observed=f"missing field: {field_name}",
189
+ detail=detail,
190
+ )
191
+
192
+
193
+ def _required_task_field_errors(
194
+ task: dict[str, Any],
195
+ *,
196
+ task_id: str,
197
+ ) -> list[PlanAcceptanceIssue]:
198
+ issues: list[PlanAcceptanceIssue] = []
199
+ if not str(task.get("id") or "").strip():
200
+ issues.append(_missing_field_issue(task_id=task_id, field_name="id"))
201
+ if not str(task.get("title") or "").strip():
202
+ issues.append(_missing_field_issue(task_id=task_id, field_name="title"))
203
+ raw_write_scope = task.get("write_scope")
204
+ if not isinstance(raw_write_scope, list) or not _raw_text_list(raw_write_scope):
205
+ issues.append(_missing_field_issue(task_id=task_id, field_name="write_scope"))
206
+ return issues
207
+
208
+
209
+ def _empty_write_scope_observed(raw_write_scope: list[str]) -> str:
210
+ normalized, _dropped = split_normalized_repo_path_list(raw_write_scope)
211
+ if normalized and all(is_agent_internal_scope_path(path) for path in normalized):
212
+ prefixes = sorted({path.split("/", 1)[0] for path in normalized})
213
+ if prefixes == [".alysis"]:
214
+ return "all write_scope paths under .alysis/"
215
+ if prefixes:
216
+ return "all write_scope paths under agent-internal dirs: " + ", ".join(prefixes)
217
+ return "write_scope has no runnable user-code paths after filtering"
218
+
219
+
220
+ def _metadata_only_observed(write_scope: list[str]) -> str:
221
+ lowered = [path.casefold() for path in write_scope]
222
+ if lowered and all(
223
+ path in {"readme", "readme.md"} or path.startswith("docs/") or path.endswith(".md")
224
+ for path in lowered
225
+ ):
226
+ return "write_scope is README/docs only"
227
+ return f"write_scope has no code implementation paths (detected: {describe_path_kinds(write_scope)})"
228
+
229
+
230
+ _PRIMARY_IMPLEMENTATION_FILENAMES = frozenset(
231
+ {
232
+ "cargo.toml",
233
+ "go.mod",
234
+ "go.sum",
235
+ "mix.exs",
236
+ "package-lock.json",
237
+ "package.json",
238
+ "pnpm-lock.yaml",
239
+ "poetry.lock",
240
+ "pom.xml",
241
+ "pyproject.toml",
242
+ "requirements-dev.txt",
243
+ "requirements.txt",
244
+ "setup.cfg",
245
+ "setup.py",
246
+ "tox.ini",
247
+ "yarn.lock",
248
+ }
249
+ )
250
+ _PRIMARY_IMPLEMENTATION_EXTENSIONS = frozenset(
251
+ {
252
+ ".cfg",
253
+ ".conf",
254
+ ".ini",
255
+ ".toml",
256
+ ".yaml",
257
+ ".yml",
258
+ }
259
+ )
260
+ _FRONTEND_SURFACE_EXTENSIONS = FRONTEND_SURFACE_EXTENSIONS
261
+ _CONTENT_SURFACE_EXTENSIONS = CONTENT_SURFACE_EXTENSIONS
262
+ _PRIMARY_IMPLEMENTATION_SCOPE_DIRS = frozenset(
263
+ {
264
+ "app",
265
+ "apps",
266
+ "bin",
267
+ "cmd",
268
+ "lib",
269
+ "libs",
270
+ "package",
271
+ "packages",
272
+ "server",
273
+ "src",
274
+ }
275
+ )
276
+ _BROAD_SUPPORT_SCOPE_DIRS = frozenset(
277
+ {
278
+ ".github",
279
+ ".alysis",
280
+ "__tests__",
281
+ "build",
282
+ "coverage",
283
+ "dist",
284
+ "doc",
285
+ "docs",
286
+ "example",
287
+ "examples",
288
+ "fixture",
289
+ "fixtures",
290
+ "note",
291
+ "notes",
292
+ "spec",
293
+ "specs",
294
+ "test",
295
+ "tests",
296
+ }
297
+ )
298
+ _SUPPORT_FIXTURE_SCOPE_DIRS = frozenset(
299
+ {
300
+ "__snapshots__",
301
+ "fixture",
302
+ "fixtures",
303
+ "golden",
304
+ "goldens",
305
+ "sample",
306
+ "samples",
307
+ "snapshot",
308
+ "snapshots",
309
+ "test_data",
310
+ "test-data",
311
+ "testdata",
312
+ }
313
+ )
314
+ _SUPPORT_FIXTURE_EXTENSIONS = frozenset(
315
+ {
316
+ ".csv",
317
+ ".golden",
318
+ ".ini",
319
+ ".json",
320
+ ".jsonl",
321
+ ".out",
322
+ ".snap",
323
+ ".toml",
324
+ ".txt",
325
+ ".xml",
326
+ ".yaml",
327
+ ".yml",
328
+ }
329
+ )
330
+ _SUPPORT_SCOPE_TEXT_RE = re.compile(
331
+ r"(?<![A-Za-z0-9_])"
332
+ r"(?:changelog|coverage|docs?|doctest|documentation|examples?|jest|markdown|"
333
+ r"pytest|readme|regression|specs?|tests?|unittest|verification|verify|vitest)"
334
+ r"(?![A-Za-z0-9_])",
335
+ re.IGNORECASE,
336
+ )
337
+ _PRIMARY_SUPPORT_SCOPE_TEXT_RE = re.compile(
338
+ r"^\s*(?:(?:add|create|write|update)\s+)?"
339
+ r"(?:(?:[\w./-]+)\s+){0,8}"
340
+ r"(?:tests?|test\s+case|coverage)\b"
341
+ r"|^\s*(?:verify|validate|run|check)\b"
342
+ r"|^\s*(?:(?:add|create|write|update|sync)\s+)?"
343
+ r"(?:readme|docs?|documentation|changelog|manual)\b"
344
+ r"|^\s*document\b",
345
+ re.IGNORECASE,
346
+ )
347
+ _CONTENT_SCOPE_TEXT_RE = re.compile(
348
+ r"(?<![A-Za-z0-9_])"
349
+ r"(?:adoc|article|checklist|chores?|content|copy|css|docs?|documentation|"
350
+ r"frontend|front-end|headings?|html|landing|markdown|mdx|notes?|page|readme|"
351
+ r"rst|sections?|style|styles|styling|todo|todos|ui|web(?:site)?|write[- ]?up)"
352
+ r"(?![A-Za-z0-9_])",
353
+ re.IGNORECASE,
354
+ )
355
+ _IMPLEMENTATION_INTENT_TEXT_RE = re.compile(
356
+ r"\b(?:build|change|configure|edit|enable|fix|implement|improve|modify|patch|"
357
+ r"refactor|rename|repair|support|wire)\b",
358
+ re.IGNORECASE,
359
+ )
360
+ _SUPPORT_FIXTURE_TEXT_RE = re.compile(
361
+ r"\b(?:fixtures?|test\s+data|sample(?:\s+data)?|golden(?:\s+files?)?|"
362
+ r"snapshots?|expected\s+(?:output|data|results?))\b",
363
+ re.IGNORECASE,
364
+ )
365
+
366
+
367
+ def _is_primary_implementation_path(path: str) -> bool:
368
+ cleaned = str(path or "").strip().replace("\\", "/").strip("/")
369
+ if cleaned.endswith("/**"):
370
+ first_part = cleaned.split("/", 1)[0].casefold()
371
+ if first_part in _PRIMARY_IMPLEMENTATION_SCOPE_DIRS:
372
+ return True
373
+ if is_code_implementation_path(path):
374
+ return True
375
+ pure = PurePosixPath(path)
376
+ name = pure.name.casefold()
377
+ if name in _PRIMARY_IMPLEMENTATION_FILENAMES:
378
+ return True
379
+ if pure.suffix.casefold() in _PRIMARY_IMPLEMENTATION_EXTENSIONS:
380
+ return True
381
+ return False
382
+
383
+
384
+ def _is_task_broad_primary_implementation_scope(task: dict[str, Any], path: str) -> bool:
385
+ cleaned = str(path or "").strip().replace("\\", "/").strip("/")
386
+ if not cleaned.endswith("/**"):
387
+ return False
388
+ base = cleaned[:-3].strip("/")
389
+ if not base:
390
+ return False
391
+ parts = [part.casefold() for part in base.split("/") if part]
392
+ if not parts:
393
+ return False
394
+ if any(part.startswith(".") or part in _BROAD_SUPPORT_SCOPE_DIRS for part in parts):
395
+ return False
396
+ text = _task_text(task)
397
+ if not _IMPLEMENTATION_INTENT_TEXT_RE.search(text):
398
+ return False
399
+ if _task_is_explicit_support_scope(task):
400
+ return False
401
+ return True
402
+
403
+
404
+ def _path_suffix(path: str) -> str:
405
+ return PurePosixPath(str(path or "").strip().replace("\\", "/")).suffix.casefold()
406
+
407
+
408
+ def _is_content_surface_path(path: str) -> bool:
409
+ lowered = str(path or "").strip().replace("\\", "/").casefold()
410
+ if lowered in {"readme", "readme.md", "todo", "todo.md"}:
411
+ return True
412
+ if lowered.startswith(("docs/", "notes/")):
413
+ return True
414
+ return _path_suffix(path) in _CONTENT_SURFACE_EXTENSIONS
415
+
416
+
417
+ def _is_frontend_surface_path(path: str) -> bool:
418
+ return _path_suffix(path) in _FRONTEND_SURFACE_EXTENSIONS
419
+
420
+
421
+ def _is_test_surface_path(path: str) -> bool:
422
+ return is_test_path(path)
423
+
424
+
425
+ def _is_support_surface_path(path: str) -> bool:
426
+ lowered = str(path or "").strip().replace("\\", "/").casefold()
427
+ name = PurePosixPath(lowered).name
428
+ if _is_content_surface_path(path) or _is_frontend_surface_path(path):
429
+ return True
430
+ if _is_test_surface_path(path):
431
+ return True
432
+ if lowered.startswith(("example/", "examples/")):
433
+ return True
434
+ return name in {
435
+ "changelog",
436
+ "changelog.md",
437
+ "license",
438
+ "license.md",
439
+ "licence",
440
+ "licence.md",
441
+ }
442
+
443
+
444
+ def _is_support_fixture_path(task: dict[str, Any], path: str) -> bool:
445
+ normalized = str(path or "").strip().replace("\\", "/").strip("/")
446
+ if not normalized:
447
+ return False
448
+ pure = PurePosixPath(normalized)
449
+ lowered_parts = tuple(part.casefold() for part in pure.parts)
450
+ if set(lowered_parts) & _SUPPORT_FIXTURE_SCOPE_DIRS:
451
+ return True
452
+ if len(lowered_parts) != 1:
453
+ if any(part in _PRIMARY_IMPLEMENTATION_SCOPE_DIRS for part in lowered_parts[:-1]):
454
+ return False
455
+ return False
456
+ if pure.suffix.casefold() not in _SUPPORT_FIXTURE_EXTENSIONS:
457
+ return False
458
+ return _SUPPORT_FIXTURE_TEXT_RE.search(_task_text(task)) is not None
459
+
460
+
461
+ def _task_has_explicit_support_surface_scope(
462
+ *,
463
+ estimated_files: list[str],
464
+ write_scope: list[str],
465
+ ) -> bool:
466
+ scoped_paths = [*estimated_files, *write_scope]
467
+ return bool(scoped_paths) and all(_is_support_surface_path(path) for path in scoped_paths)
468
+
469
+
470
+ def _task_has_explicit_content_surface_scope(
471
+ task: dict[str, Any],
472
+ *,
473
+ estimated_files: list[str],
474
+ write_scope: list[str],
475
+ ) -> bool:
476
+ scoped_paths = [*estimated_files, *write_scope]
477
+ if not scoped_paths:
478
+ return False
479
+ if not all(
480
+ _is_content_surface_path(path) or _is_frontend_surface_path(path) for path in scoped_paths
481
+ ):
482
+ return False
483
+ text = _task_text(task)
484
+ if _CONTENT_SCOPE_TEXT_RE.search(text):
485
+ return True
486
+ path_hints = _explicit_task_path_hints(task)
487
+ return bool(path_hints) and all(
488
+ _is_content_surface_path(path) or _is_frontend_surface_path(path) for path in path_hints
489
+ )
490
+
491
+
492
+ def _task_text(task: dict[str, Any]) -> str:
493
+ parts = [
494
+ str(task.get("title") or ""),
495
+ str(task.get("description") or ""),
496
+ *normalized_text_list(task.get("acceptance_criteria")),
497
+ ]
498
+ return " ".join(part for part in parts if part).casefold()
499
+
500
+
501
+ def _task_is_explicit_support_scope(task: dict[str, Any]) -> bool:
502
+ text = _task_text(task)
503
+ if not text:
504
+ return False
505
+ title = str(task.get("title") or "").strip()
506
+ if _PRIMARY_SUPPORT_SCOPE_TEXT_RE.search(title):
507
+ return True
508
+ intent_text = " ".join(
509
+ part
510
+ for part in (
511
+ title,
512
+ str(task.get("description") or "").strip(),
513
+ )
514
+ if part
515
+ )
516
+ if _IMPLEMENTATION_INTENT_TEXT_RE.search(intent_text):
517
+ return False
518
+ return _SUPPORT_SCOPE_TEXT_RE.search(text) is not None
519
+
520
+
521
+ def _path_identity_key(value: str) -> str:
522
+ normalized = str(value or "").strip().replace("\\", "/").rstrip("/")
523
+ if normalized.casefold() in {"readme", "readme.md"}:
524
+ return "__readme_alias__"
525
+ return normalized.casefold()
526
+
527
+
528
+ def _explicit_task_path_hints(task: dict[str, Any]) -> list[str]:
529
+ text = "\n".join(
530
+ [
531
+ str(task.get("title") or ""),
532
+ str(task.get("description") or ""),
533
+ *normalized_text_list(task.get("acceptance_criteria")),
534
+ ]
535
+ )
536
+ forbidden = {_path_identity_key(path) for path in extract_forbidden_repo_path_hints(text)}
537
+ hints, _obsolete = filter_obsolete_direction_paths(
538
+ extract_repo_path_hints(text),
539
+ latest_user_text=text,
540
+ task_text=text,
541
+ )
542
+ normalized, _dropped = split_normalized_repo_path_list(hints)
543
+ out: list[str] = []
544
+ for path in normalized:
545
+ if is_agent_internal_scope_path(path):
546
+ continue
547
+ if _path_identity_key(path) in forbidden:
548
+ continue
549
+ out.append(path)
550
+ return list(dict.fromkeys(out))
551
+
552
+
553
+ def _scope_entry_covers_path(scope_entry: str, path: str) -> bool:
554
+ normalized_scope = str(scope_entry or "").strip().replace("\\", "/").rstrip("/")
555
+ normalized_path = str(path or "").strip().replace("\\", "/").rstrip("/")
556
+ if not normalized_scope or not normalized_path:
557
+ return False
558
+ if _path_identity_key(normalized_scope) == _path_identity_key(normalized_path):
559
+ return True
560
+ if normalized_scope == normalized_path:
561
+ return True
562
+ if any(char in normalized_scope for char in "*?["):
563
+ return fnmatchcase(normalized_path, normalized_scope)
564
+ if normalized_scope.endswith("/"):
565
+ return normalized_path.startswith(normalized_scope)
566
+ return False
567
+
568
+
569
+ def _missing_explicit_scope_hints(
570
+ *,
571
+ task: dict[str, Any],
572
+ estimated_files: list[str],
573
+ write_scope: list[str],
574
+ ) -> list[str]:
575
+ explicit_paths = _explicit_task_path_hints(task)
576
+ if not explicit_paths:
577
+ return []
578
+ scope_entries = [*estimated_files, *write_scope]
579
+ missing = [
580
+ path
581
+ for path in explicit_paths
582
+ if not any(_scope_entry_covers_path(scope, path) for scope in scope_entries)
583
+ ]
584
+ return missing
585
+
586
+
587
+ def _task_needs_primary_implementation_scope(
588
+ task: dict[str, Any],
589
+ *,
590
+ estimated_files: list[str],
591
+ write_scope: list[str],
592
+ ) -> bool:
593
+ if _task_is_explicit_support_scope(task) and _task_has_explicit_support_surface_scope(
594
+ estimated_files=estimated_files,
595
+ write_scope=write_scope,
596
+ ):
597
+ return False
598
+ if _task_has_explicit_content_surface_scope(
599
+ task,
600
+ estimated_files=estimated_files,
601
+ write_scope=write_scope,
602
+ ):
603
+ return False
604
+ return True
605
+
606
+
607
+ def _task_has_primary_implementation_scope(
608
+ task: dict[str, Any],
609
+ ) -> bool:
610
+ estimated_files, write_scope = normalize_existing_task_scope_fields(
611
+ estimated_files=task.get("estimated_files"),
612
+ write_scope=task.get("write_scope"),
613
+ )
614
+ return any(
615
+ _is_primary_implementation_path(path)
616
+ or _is_task_broad_primary_implementation_scope(task, path)
617
+ for path in [*estimated_files, *write_scope]
618
+ )
619
+
620
+
621
+ def _task_has_implementation_intent(task: dict[str, Any]) -> bool:
622
+ if _task_is_explicit_support_scope(task):
623
+ return False
624
+ return _IMPLEMENTATION_INTENT_TEXT_RE.search(_task_text(task)) is not None
625
+
626
+
627
+ def _task_has_primary_implementation_dependency(
628
+ task: dict[str, Any],
629
+ *,
630
+ task_by_id: dict[str, dict[str, Any]],
631
+ seen: set[str] | None = None,
632
+ ) -> bool:
633
+ raw_deps = task.get("dependencies") or []
634
+ if not isinstance(raw_deps, list):
635
+ return False
636
+ seen = set(seen or ())
637
+ for raw_dep in raw_deps:
638
+ dep_id = str(raw_dep or "").strip()
639
+ if not dep_id or dep_id in seen:
640
+ continue
641
+ seen.add(dep_id)
642
+ dep_task = task_by_id.get(dep_id)
643
+ if dep_task is None:
644
+ continue
645
+ dep_status = canonical_task_status(str(dep_task.get("status") or ""))
646
+ if dep_status in _NON_EXECUTABLE_OBSOLETE_STATUSES:
647
+ continue
648
+ if _task_has_primary_implementation_scope(dep_task) or _task_has_implementation_intent(
649
+ dep_task
650
+ ):
651
+ return True
652
+ if _task_has_primary_implementation_dependency(
653
+ dep_task,
654
+ task_by_id=task_by_id,
655
+ seen=seen,
656
+ ):
657
+ return True
658
+ return False
659
+
660
+
661
+ def _support_surface_scope_is_covered_by_implementation_dependency(
662
+ task: dict[str, Any],
663
+ *,
664
+ estimated_files: list[str],
665
+ write_scope: list[str],
666
+ task_by_id: dict[str, dict[str, Any]],
667
+ ) -> bool:
668
+ scoped_paths = [*estimated_files, *write_scope]
669
+ if not scoped_paths:
670
+ return False
671
+ if not _task_is_explicit_support_scope(task):
672
+ return False
673
+ if not any(_is_support_surface_path(path) for path in scoped_paths):
674
+ return False
675
+ if any(
676
+ _is_primary_implementation_path(path)
677
+ or _is_task_broad_primary_implementation_scope(task, path)
678
+ for path in scoped_paths
679
+ ):
680
+ return False
681
+ if not all(
682
+ _is_support_surface_path(path) or _is_support_fixture_path(task, path)
683
+ for path in scoped_paths
684
+ ):
685
+ return False
686
+ return _task_has_primary_implementation_dependency(task, task_by_id=task_by_id)
687
+
688
+
689
+ def _task_is_report_only_execution_candidate(task: dict[str, Any]) -> bool:
690
+ if task.get("analysis_only") is True:
691
+ return True
692
+ lifecycle = classify_task_lifecycle(
693
+ title=str(task.get("title") or "").strip(),
694
+ description=str(task.get("description") or "").strip(),
695
+ acceptance_criteria=normalized_text_list(task.get("acceptance_criteria")),
696
+ estimated_files=_raw_text_list(task.get("estimated_files")),
697
+ write_scope=_raw_text_list(task.get("write_scope")),
698
+ )
699
+ return lifecycle.kind == TASK_KIND_ANALYSIS_ONLY
700
+
701
+
702
+ def find_plan_acceptance_issues(
703
+ plan: dict[str, Any],
704
+ *,
705
+ retry_failed: bool = False,
706
+ retry_changes_requested: bool = False,
707
+ retry_merge_conflicts: bool = False,
708
+ only: str | None = None,
709
+ ) -> list[PlanAcceptanceIssue]:
710
+ tasks_raw = plan.get("tasks")
711
+ if not isinstance(tasks_raw, list) or not tasks_raw:
712
+ return [
713
+ PlanAcceptanceIssue(
714
+ rule_id="R1",
715
+ observed="no mutating execution candidates",
716
+ detail="plan.tasks is missing or empty",
717
+ )
718
+ ]
719
+
720
+ candidates = _candidate_execution_tasks(
721
+ plan,
722
+ retry_failed=retry_failed,
723
+ retry_changes_requested=retry_changes_requested,
724
+ retry_merge_conflicts=retry_merge_conflicts,
725
+ only=only,
726
+ )
727
+ task_by_id = {
728
+ str(task.get("id") or "").strip(): task
729
+ for task in tasks_raw
730
+ if isinstance(task, dict) and str(task.get("id") or "").strip()
731
+ }
732
+ planning_constraints = planning_constraints_from_plan(plan)
733
+ mutating_seen = False
734
+ report_only_seen = False
735
+ issues: list[PlanAcceptanceIssue] = []
736
+ for _index, task, task_id, _status in candidates:
737
+ requires_runnable_scope, estimated_files, write_scope = _mutating_task_scope(task)
738
+ if not requires_runnable_scope:
739
+ if _task_is_report_only_execution_candidate(task):
740
+ report_only_seen = True
741
+ if not str(task.get("id") or "").strip():
742
+ issues.append(_missing_field_issue(task_id=task_id, field_name="id"))
743
+ if not str(task.get("title") or "").strip():
744
+ issues.append(_missing_field_issue(task_id=task_id, field_name="title"))
745
+ continue
746
+ mutating_seen = True
747
+ issues.extend(_required_task_field_errors(task, task_id=task_id))
748
+
749
+ raw_write_scope = _raw_text_list(task.get("write_scope"))
750
+ if raw_write_scope and not write_scope:
751
+ issues.append(
752
+ PlanAcceptanceIssue(
753
+ rule_id="R2",
754
+ task_id=task_id,
755
+ observed=_empty_write_scope_observed(raw_write_scope),
756
+ detail=EXECUTION_UNREADY_SCOPE_WARNING,
757
+ )
758
+ )
759
+ continue
760
+ missing_explicit_scope = _missing_explicit_scope_hints(
761
+ task=task,
762
+ estimated_files=estimated_files,
763
+ write_scope=write_scope,
764
+ )
765
+ if missing_explicit_scope:
766
+ issues.append(
767
+ PlanAcceptanceIssue(
768
+ rule_id="R3",
769
+ task_id=task_id,
770
+ observed=(
771
+ "scope omits explicit task path hints: "
772
+ + ", ".join(missing_explicit_scope[:8])
773
+ ),
774
+ detail=_with_scope_guidance("write_scope=" + ", ".join(write_scope[:8])),
775
+ )
776
+ )
777
+
778
+ if (
779
+ write_scope
780
+ and not any(
781
+ _is_primary_implementation_path(path)
782
+ or _is_task_broad_primary_implementation_scope(task, path)
783
+ for path in [*estimated_files, *write_scope]
784
+ )
785
+ and not _support_surface_scope_is_covered_by_implementation_dependency(
786
+ task,
787
+ estimated_files=estimated_files,
788
+ write_scope=write_scope,
789
+ task_by_id=task_by_id,
790
+ )
791
+ and _task_needs_primary_implementation_scope(
792
+ task,
793
+ estimated_files=estimated_files,
794
+ write_scope=write_scope,
795
+ )
796
+ ):
797
+ issues.append(
798
+ PlanAcceptanceIssue(
799
+ rule_id="R3",
800
+ task_id=task_id,
801
+ observed=_metadata_only_observed(write_scope),
802
+ detail=_with_scope_guidance("write_scope=" + ", ".join(write_scope[:8])),
803
+ )
804
+ )
805
+
806
+ constraint_violations = task_scope_constraint_violations(task, planning_constraints)
807
+ for violation in constraint_violations:
808
+ detail = (
809
+ f"constraint={violation.constraint_path}; evidence={violation.evidence}"
810
+ if violation.constraint_path
811
+ else f"evidence={violation.evidence}"
812
+ )
813
+ issues.append(
814
+ PlanAcceptanceIssue(
815
+ rule_id="R5",
816
+ task_id=task_id,
817
+ observed=(
818
+ f"scope violates planning constraints: {violation.path} "
819
+ f"({violation.classification}; {violation.reason_code})"
820
+ ),
821
+ detail=detail,
822
+ )
823
+ )
824
+
825
+ if not mutating_seen and candidates and not report_only_seen:
826
+ issues.insert(
827
+ 0,
828
+ PlanAcceptanceIssue(
829
+ rule_id="R1",
830
+ observed="no mutating execution candidates",
831
+ detail="execution candidates are read-only/report-only",
832
+ ),
833
+ )
834
+ return issues
835
+
836
+
837
+ def _format_plan_acceptance_block(issues: list[PlanAcceptanceIssue]) -> str:
838
+ if not issues:
839
+ return ""
840
+ rendered: list[str] = []
841
+ for issue in issues[:5]:
842
+ parts = [issue.rule_id]
843
+ if issue.task_id:
844
+ parts.append(f"task={issue.task_id}")
845
+ parts.append(f"observed={issue.observed}")
846
+ if issue.detail:
847
+ parts.append(f"detail={issue.detail}")
848
+ rendered.append(" ".join(parts))
849
+ if len(issues) > 5:
850
+ rendered.append(f"+{len(issues) - 5} more")
851
+ return "Execution blocked: " + "; ".join(rendered)
852
+
853
+
854
+ def _find_cycle(ids_in_order: list[str], deps_map: dict[str, list[str]]) -> list[str] | None:
855
+ white = 0
856
+ gray = 1
857
+ black = 2
858
+ state: dict[str, int] = {task_id: white for task_id in ids_in_order}
859
+ stack: list[str] = []
860
+ stack_index: dict[str, int] = {}
861
+
862
+ def _dfs(task_id: str) -> list[str] | None:
863
+ state[task_id] = gray
864
+ stack_index[task_id] = len(stack)
865
+ stack.append(task_id)
866
+ for dep_id in deps_map.get(task_id, []):
867
+ dep_state = state.get(dep_id, white)
868
+ if dep_state == white:
869
+ cycle = _dfs(dep_id)
870
+ if cycle is not None:
871
+ return cycle
872
+ continue
873
+ if dep_state == gray:
874
+ start = stack_index.get(dep_id, 0)
875
+ return stack[start:] + [dep_id]
876
+
877
+ stack.pop()
878
+ stack_index.pop(task_id, None)
879
+ state[task_id] = black
880
+ return None
881
+
882
+ for task_id in ids_in_order:
883
+ if state.get(task_id, white) != white:
884
+ continue
885
+ cycle = _dfs(task_id)
886
+ if cycle is not None:
887
+ return cycle
888
+ return None
889
+
890
+
891
+ def validate_plan(plan: dict[str, Any]) -> list[str]:
892
+ _warn_legacy_schema(plan)
893
+ tasks_raw = plan.get("tasks")
894
+ if not isinstance(tasks_raw, list):
895
+ return ["Plan validation warning: tasks field is missing or not an array."]
896
+
897
+ tasks: list[dict[str, Any]] = [task for task in tasks_raw if isinstance(task, dict)]
898
+ known_ids = _task_ids(tasks)
899
+ known_set = set(known_ids)
900
+ warnings: list[str] = []
901
+ planning_constraints = planning_constraints_from_plan(plan)
902
+
903
+ deps_map: dict[str, list[str]] = {}
904
+ for task in tasks:
905
+ task_id = str(task.get("id") or "").strip()
906
+ if not task_id or task_id not in known_set:
907
+ continue
908
+ status = canonical_task_status(str(task.get("status") or ""))
909
+
910
+ deps = _string_list(task.get("dependencies"))
911
+ deps_map[task_id] = (
912
+ []
913
+ if status in _NON_EXECUTABLE_OBSOLETE_STATUSES
914
+ else [dep for dep in deps if dep in known_set]
915
+ )
916
+
917
+ for dep_id in deps:
918
+ if dep_id not in known_set:
919
+ warnings.append(f"Task {task_id} has unknown dependency id: {dep_id}")
920
+ else:
921
+ dep_task = next(
922
+ (
923
+ candidate
924
+ for candidate in tasks
925
+ if str(candidate.get("id") or "").strip() == dep_id
926
+ ),
927
+ None,
928
+ )
929
+ dep_status = canonical_task_status(str((dep_task or {}).get("status") or ""))
930
+ if (
931
+ status not in _NON_EXECUTABLE_OBSOLETE_STATUSES
932
+ and dep_status in _NON_EXECUTABLE_OBSOLETE_STATUSES
933
+ ):
934
+ warnings.append(f"Task {task_id} depends on non-executable task id: {dep_id}")
935
+
936
+ if status in _NON_EXECUTABLE_OBSOLETE_STATUSES:
937
+ continue
938
+
939
+ acceptance = _string_list(task.get("acceptance_criteria"))
940
+ if not acceptance:
941
+ warnings.append(f"Task {task_id} is missing acceptance_criteria")
942
+
943
+ if task_is_missing_runnable_scope(task):
944
+ warnings.append(
945
+ task_readiness_warning(
946
+ task_id=task_id,
947
+ title=str(task.get("title") or "").strip(),
948
+ )
949
+ )
950
+
951
+ _normalized_mcp_scope, mcp_scope_warnings = normalize_task_mcp_scope(
952
+ task.get("mcp_scope"),
953
+ warning_prefix=f"Task {task_id}",
954
+ )
955
+ warnings.extend(mcp_scope_warnings)
956
+ warnings.extend(_validate_task_asset_briefing_shape(task, task_id=task_id))
957
+ for violation in task_scope_constraint_violations(task, planning_constraints):
958
+ warnings.append(
959
+ f"Task {task_id} violates planning scope constraints: {violation.path} "
960
+ f"({violation.classification}; {violation.reason_code})"
961
+ )
962
+
963
+ cycle = _find_cycle(known_ids, deps_map)
964
+ if cycle is not None:
965
+ warnings.append("Circular dependency detected: " + " -> ".join(cycle))
966
+
967
+ return warnings
968
+
969
+
970
+ def _warn_legacy_schema(plan: dict[str, Any]) -> None:
971
+ try:
972
+ schema_version = int(plan.get("schema_version", 1) or 1)
973
+ except (TypeError, ValueError):
974
+ schema_version = 1
975
+ if schema_version <= 1:
976
+ warnings.warn(
977
+ "Plan schema_version 1 is deprecated. Load the run to migrate it to schema_version 2.",
978
+ DeprecationWarning,
979
+ stacklevel=2,
980
+ )
981
+ elif schema_version != 2:
982
+ warnings.warn(
983
+ f"Plan schema_version {schema_version} is not explicitly supported; "
984
+ "validation will apply the closest compatible rules.",
985
+ RuntimeWarning,
986
+ stacklevel=2,
987
+ )
988
+
989
+
990
+ def validate_plan_against_assets(
991
+ plan: dict[str, Any],
992
+ assets_source: Any,
993
+ *,
994
+ max_primary_per_task: int = 8,
995
+ ) -> list[str]:
996
+ records = _asset_records_by_id(assets_source)
997
+ warnings: list[str] = []
998
+ for task in plan.get("tasks") or []:
999
+ if not isinstance(task, dict):
1000
+ continue
1001
+ task_id = str(task.get("id") or "").strip() or "(unknown)"
1002
+ try:
1003
+ briefing = task_asset_briefing(task)
1004
+ except AssetError as exc:
1005
+ warnings.append(f"Task {task_id} has invalid asset_briefing: {exc}")
1006
+ continue
1007
+ if briefing is None:
1008
+ continue
1009
+ primary_ids = [entry.asset_id for entry in briefing.primary]
1010
+ may_need_ids = [entry.asset_id for entry in briefing.may_need]
1011
+ if len(primary_ids) > max_primary_per_task:
1012
+ warnings.append(
1013
+ f"Task {task_id} references too many primary assets: "
1014
+ f"{len(primary_ids)} > {max_primary_per_task}"
1015
+ )
1016
+ overlap = sorted(set(primary_ids) & set(may_need_ids))
1017
+ if overlap:
1018
+ warnings.append(
1019
+ f"Task {task_id} references the same asset in primary and may_need: "
1020
+ + ", ".join(overlap)
1021
+ )
1022
+ for asset_id in [*primary_ids, *may_need_ids]:
1023
+ record = records.get(asset_id)
1024
+ if record is None:
1025
+ warnings.append(f"Task {task_id} references missing asset id: {asset_id}")
1026
+ elif getattr(record, "deleted_at", None) is not None:
1027
+ warnings.append(f"Task {task_id} references deleted asset id: {asset_id}")
1028
+ return warnings
1029
+
1030
+
1031
+ def _validate_task_asset_briefing_shape(
1032
+ task: dict[str, Any],
1033
+ *,
1034
+ task_id: str,
1035
+ max_primary_per_task: int = 8,
1036
+ ) -> list[str]:
1037
+ if "asset_briefing" not in task:
1038
+ return []
1039
+ try:
1040
+ briefing = task_asset_briefing(task)
1041
+ except AssetError as exc:
1042
+ return [f"Task {task_id} has invalid asset_briefing: {exc}"]
1043
+ if briefing is None:
1044
+ return []
1045
+ warnings: list[str] = []
1046
+ primary_ids = [entry.asset_id for entry in briefing.primary]
1047
+ may_need_ids = [entry.asset_id for entry in briefing.may_need]
1048
+ if len(primary_ids) > max_primary_per_task:
1049
+ warnings.append(
1050
+ f"Task {task_id} references too many primary assets: "
1051
+ f"{len(primary_ids)} > {max_primary_per_task}"
1052
+ )
1053
+ overlap = sorted(set(primary_ids) & set(may_need_ids))
1054
+ if overlap:
1055
+ warnings.append(
1056
+ f"Task {task_id} references the same asset in primary and may_need: "
1057
+ + ", ".join(overlap)
1058
+ )
1059
+ return warnings
1060
+
1061
+
1062
+ def _asset_records_by_id(assets_source: Any) -> dict[str, Any]:
1063
+ records_method = getattr(assets_source, "records", None)
1064
+ if callable(records_method):
1065
+ return {
1066
+ str(record.id): record
1067
+ for record in records_method(include_deleted=True)
1068
+ if getattr(record, "id", "")
1069
+ }
1070
+ index = getattr(assets_source, "index", None)
1071
+ index_records = getattr(index, "records", None)
1072
+ if callable(index_records):
1073
+ return {
1074
+ str(record.id): record
1075
+ for record in index_records(include_deleted=True)
1076
+ if getattr(record, "id", "")
1077
+ }
1078
+ if isinstance(assets_source, dict):
1079
+ return assets_source
1080
+ return {}
1081
+
1082
+
1083
+ def raise_for_execution_ready_plan(
1084
+ plan: dict[str, Any],
1085
+ *,
1086
+ retry_failed: bool = False,
1087
+ retry_changes_requested: bool = False,
1088
+ retry_merge_conflicts: bool = False,
1089
+ only: str | None = None,
1090
+ ) -> None:
1091
+ acceptance_issues = find_plan_acceptance_issues(
1092
+ plan,
1093
+ retry_failed=retry_failed,
1094
+ retry_changes_requested=retry_changes_requested,
1095
+ retry_merge_conflicts=retry_merge_conflicts,
1096
+ only=only,
1097
+ )
1098
+ if acceptance_issues:
1099
+ raise PlannerFailedError(_format_plan_acceptance_block(acceptance_issues))