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,992 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from .direction_change import filter_obsolete_direction_paths
8
+ from .file_classification import is_docs_path, is_test_path
9
+ from .task_scope import (
10
+ extract_forbidden_repo_path_hints,
11
+ extract_repo_path_hints,
12
+ is_agent_internal_scope_path,
13
+ split_normalized_repo_path_list,
14
+ )
15
+
16
+ MUTATING_TASK_SCOPE_WARNING = (
17
+ "runnable or ambiguous task requires runnable estimated_files/write_scope; provide "
18
+ "repo-relative file scope, explicit repo-relative path hints, or mark the task as "
19
+ "analysis-only/report-only."
20
+ )
21
+ EXECUTION_UNREADY_SCOPE_WARNING = (
22
+ "runnable or ambiguous task lacks runnable estimated_files/write_scope; include "
23
+ "repo-relative file paths in the task, use /plan edit / planner flow to produce "
24
+ "execution-ready scope, or mark the task as analysis-only/report-only."
25
+ )
26
+
27
+ _TASK_ACTION_WORD_RE = re.compile(r"[a-z][a-z0-9_-]*")
28
+ _MUTATING_TASK_ACTIONS = frozenset(
29
+ {
30
+ "add",
31
+ "build",
32
+ "change",
33
+ "configure",
34
+ "create",
35
+ "document",
36
+ "edit",
37
+ "enable",
38
+ "fix",
39
+ "improve",
40
+ "implement",
41
+ "make",
42
+ "migrate",
43
+ "modify",
44
+ "patch",
45
+ "refactor",
46
+ "remove",
47
+ "rename",
48
+ "repair",
49
+ "support",
50
+ "test",
51
+ "touch",
52
+ "update",
53
+ "upgrade",
54
+ "wire",
55
+ "write",
56
+ }
57
+ )
58
+ _WEAK_MUTATING_TASK_ACTIONS = frozenset({"change", "test"})
59
+ _NON_MUTATING_TASK_ACTIONS = frozenset(
60
+ {
61
+ "analyze",
62
+ "analyse",
63
+ "assess",
64
+ "audit",
65
+ "compare",
66
+ "diagnose",
67
+ "examine",
68
+ "explain",
69
+ "explore",
70
+ "find",
71
+ "inspect",
72
+ "investigate",
73
+ "identify",
74
+ "locate",
75
+ "map",
76
+ "report",
77
+ "review",
78
+ "summarize",
79
+ "summarise",
80
+ "survey",
81
+ "triage",
82
+ "understand",
83
+ }
84
+ )
85
+ _NON_MUTATING_TASK_PHRASES = (
86
+ "analysis only",
87
+ "explain the issue",
88
+ "investigate and report",
89
+ "no code changes",
90
+ "no file changes",
91
+ "read current",
92
+ "read-only",
93
+ "readonly",
94
+ "report findings",
95
+ "report only",
96
+ "review and report",
97
+ "summarize findings",
98
+ "summarise findings",
99
+ )
100
+ _REPORT_ONLY_DOCUMENTATION_PHRASES = (
101
+ "document findings",
102
+ "document the findings",
103
+ "findings documented",
104
+ "findings are documented",
105
+ "root cause documented",
106
+ "root cause is documented",
107
+ "summary documented",
108
+ "summary is documented",
109
+ )
110
+ _REPORT_ONLY_DOCUMENTATION_MUTATING_TOKENS = frozenset({"document"})
111
+ _NOUN_PRONE_MUTATING_TASK_ACTIONS = frozenset(
112
+ {
113
+ "build",
114
+ "change",
115
+ "make",
116
+ "patch",
117
+ "repair",
118
+ "support",
119
+ "test",
120
+ }
121
+ )
122
+ _TASK_ACTION_CLAUSE_CONNECTORS = frozenset(
123
+ {
124
+ "and",
125
+ "must",
126
+ "need",
127
+ "needs",
128
+ "require",
129
+ "requires",
130
+ "should",
131
+ "then",
132
+ "to",
133
+ "will",
134
+ }
135
+ )
136
+ _TASK_ACTION_TOKEN_CANONICAL_MAP = {
137
+ "added": "add",
138
+ "adding": "add",
139
+ "adds": "add",
140
+ "analysed": "analyse",
141
+ "analyses": "analyse",
142
+ "analysing": "analyse",
143
+ "analyzed": "analyze",
144
+ "analyzes": "analyze",
145
+ "analyzing": "analyze",
146
+ "assessed": "assess",
147
+ "assesses": "assess",
148
+ "assessing": "assess",
149
+ "audited": "audit",
150
+ "auditing": "audit",
151
+ "audits": "audit",
152
+ "changed": "change",
153
+ "changes": "change",
154
+ "changing": "change",
155
+ "built": "build",
156
+ "building": "build",
157
+ "builds": "build",
158
+ "compared": "compare",
159
+ "compares": "compare",
160
+ "comparing": "compare",
161
+ "configured": "configure",
162
+ "configures": "configure",
163
+ "configuring": "configure",
164
+ "created": "create",
165
+ "creates": "create",
166
+ "creating": "create",
167
+ "documented": "document",
168
+ "documenting": "document",
169
+ "documents": "document",
170
+ "diagnosed": "diagnose",
171
+ "diagnoses": "diagnose",
172
+ "diagnosing": "diagnose",
173
+ "edited": "edit",
174
+ "editing": "edit",
175
+ "edits": "edit",
176
+ "enabled": "enable",
177
+ "enables": "enable",
178
+ "enabling": "enable",
179
+ "explained": "explain",
180
+ "explaining": "explain",
181
+ "explains": "explain",
182
+ "examined": "examine",
183
+ "examines": "examine",
184
+ "examining": "examine",
185
+ "explored": "explore",
186
+ "explores": "explore",
187
+ "exploring": "explore",
188
+ "found": "find",
189
+ "finding": "find",
190
+ "finds": "find",
191
+ "fixed": "fix",
192
+ "fixes": "fix",
193
+ "fixing": "fix",
194
+ "identified": "identify",
195
+ "identifies": "identify",
196
+ "identifying": "identify",
197
+ "improved": "improve",
198
+ "improves": "improve",
199
+ "improving": "improve",
200
+ "implemented": "implement",
201
+ "implementing": "implement",
202
+ "implements": "implement",
203
+ "inspected": "inspect",
204
+ "inspecting": "inspect",
205
+ "inspects": "inspect",
206
+ "investigated": "investigate",
207
+ "investigates": "investigate",
208
+ "investigating": "investigate",
209
+ "located": "locate",
210
+ "locates": "locate",
211
+ "locating": "locate",
212
+ "made": "make",
213
+ "makes": "make",
214
+ "making": "make",
215
+ "mapped": "map",
216
+ "mapping": "map",
217
+ "maps": "map",
218
+ "migrated": "migrate",
219
+ "migrates": "migrate",
220
+ "migrating": "migrate",
221
+ "modified": "modify",
222
+ "modifies": "modify",
223
+ "modifying": "modify",
224
+ "patched": "patch",
225
+ "patches": "patch",
226
+ "patching": "patch",
227
+ "refactored": "refactor",
228
+ "refactoring": "refactor",
229
+ "refactors": "refactor",
230
+ "removed": "remove",
231
+ "removes": "remove",
232
+ "removing": "remove",
233
+ "renamed": "rename",
234
+ "renames": "rename",
235
+ "renaming": "rename",
236
+ "repaired": "repair",
237
+ "repairing": "repair",
238
+ "repairs": "repair",
239
+ "reported": "report",
240
+ "reporting": "report",
241
+ "reports": "report",
242
+ "read": "read",
243
+ "reading": "read",
244
+ "reads": "read",
245
+ "reviewed": "review",
246
+ "reviewing": "review",
247
+ "reviews": "review",
248
+ "surveyed": "survey",
249
+ "surveying": "survey",
250
+ "surveys": "survey",
251
+ "summarised": "summarise",
252
+ "summarises": "summarise",
253
+ "summarising": "summarise",
254
+ "summarized": "summarize",
255
+ "summarizes": "summarize",
256
+ "summarizing": "summarize",
257
+ "supported": "support",
258
+ "supporting": "support",
259
+ "supports": "support",
260
+ "tested": "test",
261
+ "testing": "test",
262
+ "tests": "test",
263
+ "touched": "touch",
264
+ "touches": "touch",
265
+ "touching": "touch",
266
+ "triaged": "triage",
267
+ "triages": "triage",
268
+ "triaging": "triage",
269
+ "understood": "understand",
270
+ "understanding": "understand",
271
+ "understands": "understand",
272
+ "updated": "update",
273
+ "updates": "update",
274
+ "updating": "update",
275
+ "upgraded": "upgrade",
276
+ "upgrades": "upgrade",
277
+ "upgrading": "upgrade",
278
+ "wired": "wire",
279
+ "wires": "wire",
280
+ "wiring": "wire",
281
+ "writes": "write",
282
+ "writing": "write",
283
+ "written": "write",
284
+ "wrote": "write",
285
+ }
286
+
287
+ TASK_KIND_ANALYSIS_ONLY = "analysis_only"
288
+ TASK_KIND_IMPLEMENTATION = "implementation"
289
+ TASK_KIND_TEST_ONLY = "test_only"
290
+ TASK_KIND_VERIFICATION_ONLY = "verification_only"
291
+ TASK_KIND_DOCUMENTATION_ONLY = "documentation_only"
292
+ TASK_KIND_UNKNOWN = "unknown"
293
+
294
+ ZERO_DIFF_ANALYSIS_OK = "analysis_artifact"
295
+ ZERO_DIFF_REQUIRES_VERIFICATION = "requires_verification"
296
+ ZERO_DIFF_SUSPICIOUS = "needs_attention"
297
+
298
+ _VERIFICATION_ONLY_RE = re.compile(
299
+ r"^\s*(?:run\s+)?(?:verify|verification|validate|validation|check|tests?)\b",
300
+ re.IGNORECASE,
301
+ )
302
+
303
+
304
+ @dataclass(frozen=True)
305
+ class TaskFileScopeNormalization:
306
+ estimated_files: list[str]
307
+ write_scope: list[str]
308
+ warnings: list[str]
309
+ requires_runnable_scope: bool
310
+ task_kind: str = TASK_KIND_UNKNOWN
311
+ task_kind_reason: str = "unclassified"
312
+ zero_diff_policy: str = ZERO_DIFF_SUSPICIOUS
313
+
314
+
315
+ @dataclass(frozen=True)
316
+ class ExecutionUnreadyTask:
317
+ task_id: str
318
+ title: str
319
+ status: str
320
+ warning: str
321
+
322
+
323
+ @dataclass(frozen=True)
324
+ class TaskLifecycleClassification:
325
+ kind: str
326
+ reason_code: str
327
+ mutating: bool
328
+ requires_runnable_scope: bool
329
+ zero_diff_policy: str
330
+
331
+ @property
332
+ def read_only(self) -> bool:
333
+ return not self.mutating
334
+
335
+
336
+ def normalized_text_list(value: Any) -> list[str]:
337
+ if not isinstance(value, list):
338
+ return []
339
+ out: list[str] = []
340
+ for item in value:
341
+ text = str(item).strip()
342
+ if text:
343
+ out.append(text)
344
+ return out
345
+
346
+
347
+ def _dedupe_keep_order(values: list[str]) -> list[str]:
348
+ seen: set[str] = set()
349
+ out: list[str] = []
350
+ for value in values:
351
+ key = value.casefold()
352
+ if key in seen:
353
+ continue
354
+ seen.add(key)
355
+ out.append(value)
356
+ return out
357
+
358
+
359
+ def _scope_identity_key(value: str) -> str:
360
+ normalized = str(value or "").strip().replace("\\", "/").rstrip("/")
361
+ if normalized.casefold() in {"readme", "readme.md"}:
362
+ return "__readme_alias__"
363
+ return normalized.casefold()
364
+
365
+
366
+ def task_text_blob(*, title: str, description: str, acceptance_criteria: list[str]) -> str:
367
+ return "\n".join(
368
+ [
369
+ str(title or ""),
370
+ str(description or ""),
371
+ *(str(item or "") for item in acceptance_criteria),
372
+ ]
373
+ ).strip()
374
+
375
+
376
+ def _first_task_action_word(text: str) -> str:
377
+ for token in _TASK_ACTION_WORD_RE.findall((text or "").casefold()):
378
+ if token in {"a", "an", "the", "to"}:
379
+ continue
380
+ return _normalize_task_action_token(token)
381
+ return ""
382
+
383
+
384
+ def _normalize_task_action_token(token: str) -> str:
385
+ return _TASK_ACTION_TOKEN_CANONICAL_MAP.get(token.casefold(), token.casefold())
386
+
387
+
388
+ def _normalized_task_action_tokens(text: str) -> list[str]:
389
+ return [
390
+ _normalize_task_action_token(token)
391
+ for token in _TASK_ACTION_WORD_RE.findall((text or "").casefold())
392
+ ]
393
+
394
+
395
+ def contains_mutating_task_signal(text: str) -> bool:
396
+ return any(token in _MUTATING_TASK_ACTIONS for token in _normalized_task_action_tokens(text))
397
+
398
+
399
+ def has_mutating_task_action_clause(text: str) -> bool:
400
+ return _has_mutating_task_action_clause(text, allowed_tokens=frozenset())
401
+
402
+
403
+ def _has_mutating_task_action_clause(text: str, *, allowed_tokens: frozenset[str]) -> bool:
404
+ folded = (text or "").casefold()
405
+ previous_token = ""
406
+ previous_end = 0
407
+ for index, match in enumerate(_TASK_ACTION_WORD_RE.finditer(folded)):
408
+ token = _normalize_task_action_token(match.group(0))
409
+ gap = folded[previous_end : match.start()]
410
+ starts_clause = (
411
+ index == 0
412
+ or any(separator in gap for separator in "\n.;:")
413
+ or ("," in gap and token not in _NOUN_PRONE_MUTATING_TASK_ACTIONS)
414
+ or previous_token in _TASK_ACTION_CLAUSE_CONNECTORS
415
+ )
416
+ if starts_clause and token in (
417
+ _MUTATING_TASK_ACTIONS - _WEAK_MUTATING_TASK_ACTIONS - allowed_tokens
418
+ ):
419
+ return True
420
+ previous_token = token
421
+ previous_end = match.end()
422
+ return False
423
+
424
+
425
+ def is_clearly_non_mutating_task(
426
+ *,
427
+ title: str,
428
+ description: str,
429
+ acceptance_criteria: list[str],
430
+ ) -> bool:
431
+ raw_text = task_text_blob(
432
+ title=title,
433
+ description=description,
434
+ acceptance_criteria=acceptance_criteria,
435
+ )
436
+ text = raw_text.casefold()
437
+ if not text:
438
+ return False
439
+ normalized_action_tokens = _normalized_task_action_tokens(text)
440
+ first_title_word = _first_task_action_word(title)
441
+ has_explicit_non_mutating_phrase = any(phrase in text for phrase in _NON_MUTATING_TASK_PHRASES)
442
+ has_report_only_documentation_phrase = any(
443
+ phrase in text for phrase in _REPORT_ONLY_DOCUMENTATION_PHRASES
444
+ )
445
+ allowed_report_only_tokens = (
446
+ _REPORT_ONLY_DOCUMENTATION_MUTATING_TOKENS
447
+ if has_report_only_documentation_phrase
448
+ else frozenset()
449
+ )
450
+ has_mutating_action_clause = _has_mutating_task_action_clause(
451
+ raw_text,
452
+ allowed_tokens=allowed_report_only_tokens,
453
+ )
454
+ if (
455
+ first_title_word in _NON_MUTATING_TASK_ACTIONS
456
+ and has_report_only_documentation_phrase
457
+ and not has_mutating_action_clause
458
+ ):
459
+ return True
460
+ if has_explicit_non_mutating_phrase and not has_mutating_action_clause:
461
+ return True
462
+ if first_title_word == "read" and not has_mutating_action_clause:
463
+ return True
464
+ if first_title_word in _NON_MUTATING_TASK_ACTIONS:
465
+ if not has_mutating_action_clause:
466
+ return True
467
+ if any(token in _MUTATING_TASK_ACTIONS for token in normalized_action_tokens):
468
+ return False
469
+ if any(token in _NON_MUTATING_TASK_ACTIONS for token in normalized_action_tokens):
470
+ return not has_mutating_action_clause
471
+ if first_title_word in _NON_MUTATING_TASK_ACTIONS:
472
+ return True
473
+ return has_explicit_non_mutating_phrase
474
+
475
+
476
+ def _normalized_scoped_paths(*, estimated_files: list[str], write_scope: list[str]) -> list[str]:
477
+ return _dedupe_keep_order([*estimated_files, *write_scope])
478
+
479
+
480
+ def _coerce_explicit_task_kind(value: Any) -> str | None:
481
+ raw = str(value or "").strip().casefold().replace("-", "_")
482
+ if not raw:
483
+ return None
484
+ aliases = {
485
+ "analysis": TASK_KIND_ANALYSIS_ONLY,
486
+ "analysis_only": TASK_KIND_ANALYSIS_ONLY,
487
+ "discovery": TASK_KIND_ANALYSIS_ONLY,
488
+ "read_only": TASK_KIND_ANALYSIS_ONLY,
489
+ "readonly": TASK_KIND_ANALYSIS_ONLY,
490
+ "report_only": TASK_KIND_ANALYSIS_ONLY,
491
+ "implementation": TASK_KIND_IMPLEMENTATION,
492
+ "mutation": TASK_KIND_IMPLEMENTATION,
493
+ "mutating": TASK_KIND_IMPLEMENTATION,
494
+ "test": TASK_KIND_TEST_ONLY,
495
+ "test_only": TASK_KIND_TEST_ONLY,
496
+ "verification": TASK_KIND_VERIFICATION_ONLY,
497
+ "verification_only": TASK_KIND_VERIFICATION_ONLY,
498
+ "docs": TASK_KIND_DOCUMENTATION_ONLY,
499
+ "documentation": TASK_KIND_DOCUMENTATION_ONLY,
500
+ "documentation_only": TASK_KIND_DOCUMENTATION_ONLY,
501
+ "unknown": TASK_KIND_UNKNOWN,
502
+ }
503
+ return aliases.get(raw)
504
+
505
+
506
+ def _classification_for_kind(kind: str, *, reason_code: str) -> TaskLifecycleClassification:
507
+ if kind == TASK_KIND_ANALYSIS_ONLY:
508
+ return TaskLifecycleClassification(
509
+ kind=kind,
510
+ reason_code=reason_code,
511
+ mutating=False,
512
+ requires_runnable_scope=False,
513
+ zero_diff_policy=ZERO_DIFF_ANALYSIS_OK,
514
+ )
515
+ if kind == TASK_KIND_VERIFICATION_ONLY:
516
+ return TaskLifecycleClassification(
517
+ kind=kind,
518
+ reason_code=reason_code,
519
+ mutating=False,
520
+ requires_runnable_scope=False,
521
+ zero_diff_policy=ZERO_DIFF_REQUIRES_VERIFICATION,
522
+ )
523
+ if kind == TASK_KIND_TEST_ONLY:
524
+ return TaskLifecycleClassification(
525
+ kind=kind,
526
+ reason_code=reason_code,
527
+ mutating=True,
528
+ requires_runnable_scope=True,
529
+ zero_diff_policy=ZERO_DIFF_SUSPICIOUS,
530
+ )
531
+ if kind == TASK_KIND_DOCUMENTATION_ONLY:
532
+ return TaskLifecycleClassification(
533
+ kind=kind,
534
+ reason_code=reason_code,
535
+ mutating=True,
536
+ requires_runnable_scope=True,
537
+ zero_diff_policy=ZERO_DIFF_SUSPICIOUS,
538
+ )
539
+ if kind == TASK_KIND_IMPLEMENTATION:
540
+ return TaskLifecycleClassification(
541
+ kind=kind,
542
+ reason_code=reason_code,
543
+ mutating=True,
544
+ requires_runnable_scope=True,
545
+ zero_diff_policy=ZERO_DIFF_REQUIRES_VERIFICATION,
546
+ )
547
+ return TaskLifecycleClassification(
548
+ kind=TASK_KIND_UNKNOWN,
549
+ reason_code=reason_code,
550
+ mutating=True,
551
+ requires_runnable_scope=True,
552
+ zero_diff_policy=ZERO_DIFF_SUSPICIOUS,
553
+ )
554
+
555
+
556
+ def classify_task_lifecycle(
557
+ *,
558
+ title: str,
559
+ description: str,
560
+ acceptance_criteria: list[str],
561
+ estimated_files: list[str] | None = None,
562
+ write_scope: list[str] | None = None,
563
+ explicit_analysis_only: bool | None = None,
564
+ explicit_task_kind: Any = None,
565
+ ) -> TaskLifecycleClassification:
566
+ explicit_kind = _coerce_explicit_task_kind(explicit_task_kind)
567
+ if explicit_analysis_only is True:
568
+ return _classification_for_kind(
569
+ TASK_KIND_ANALYSIS_ONLY,
570
+ reason_code="explicit_analysis_only",
571
+ )
572
+ if explicit_kind is not None:
573
+ return _classification_for_kind(
574
+ explicit_kind,
575
+ reason_code=f"explicit_task_kind:{explicit_kind}",
576
+ )
577
+
578
+ acceptance = normalized_text_list(acceptance_criteria)
579
+ if is_clearly_non_mutating_task(
580
+ title=title,
581
+ description=description,
582
+ acceptance_criteria=acceptance,
583
+ ):
584
+ return _classification_for_kind(
585
+ TASK_KIND_ANALYSIS_ONLY,
586
+ reason_code="non_mutating_intent",
587
+ )
588
+
589
+ text = task_text_blob(
590
+ title=title,
591
+ description=description,
592
+ acceptance_criteria=acceptance,
593
+ )
594
+ has_mutating_clause = _has_mutating_task_action_clause(text, allowed_tokens=frozenset())
595
+ if _VERIFICATION_ONLY_RE.search(text) and not has_mutating_clause:
596
+ return _classification_for_kind(
597
+ TASK_KIND_VERIFICATION_ONLY,
598
+ reason_code="verification_intent",
599
+ )
600
+
601
+ scoped_paths = _normalized_scoped_paths(
602
+ estimated_files=list(estimated_files or []),
603
+ write_scope=list(write_scope or []),
604
+ )
605
+ if scoped_paths and all(is_test_path(path) for path in scoped_paths):
606
+ return _classification_for_kind(TASK_KIND_TEST_ONLY, reason_code="test_scope")
607
+ if scoped_paths and all(is_docs_path(path) for path in scoped_paths):
608
+ return _classification_for_kind(
609
+ TASK_KIND_DOCUMENTATION_ONLY,
610
+ reason_code="documentation_scope",
611
+ )
612
+ if scoped_paths:
613
+ return _classification_for_kind(
614
+ TASK_KIND_IMPLEMENTATION,
615
+ reason_code="runnable_file_scope",
616
+ )
617
+ if contains_mutating_task_signal(text):
618
+ return _classification_for_kind(
619
+ TASK_KIND_IMPLEMENTATION,
620
+ reason_code="mutating_intent",
621
+ )
622
+ return _classification_for_kind(TASK_KIND_UNKNOWN, reason_code="ambiguous_intent")
623
+
624
+
625
+ def has_runnable_local_file_scope(
626
+ *,
627
+ estimated_files: list[str],
628
+ write_scope: list[str],
629
+ ) -> bool:
630
+ return bool(estimated_files or write_scope)
631
+
632
+
633
+ def normalize_existing_task_scope_fields(
634
+ *,
635
+ estimated_files: Any,
636
+ write_scope: Any,
637
+ ) -> tuple[list[str], list[str]]:
638
+ normalized_estimated, _dropped_estimated = split_normalized_repo_path_list(estimated_files)
639
+ normalized_write_scope, _dropped_write_scope = split_normalized_repo_path_list(write_scope)
640
+
641
+ def _filter_runnable_paths(paths: list[str]) -> list[str]:
642
+ filtered: list[str] = []
643
+ for path in paths:
644
+ if is_agent_internal_scope_path(path):
645
+ continue
646
+ filtered.append(path)
647
+ return _dedupe_keep_order(filtered)
648
+
649
+ return _filter_runnable_paths(normalized_estimated), _filter_runnable_paths(
650
+ normalized_write_scope
651
+ )
652
+
653
+
654
+ _normalize_existing_task_scope_fields = normalize_existing_task_scope_fields
655
+
656
+
657
+ def task_requires_runnable_file_scope(
658
+ *,
659
+ title: str,
660
+ description: str,
661
+ acceptance_criteria: list[str],
662
+ estimated_files: list[str],
663
+ write_scope: list[str],
664
+ ) -> bool:
665
+ lifecycle = classify_task_lifecycle(
666
+ title=title,
667
+ description=description,
668
+ acceptance_criteria=acceptance_criteria,
669
+ estimated_files=estimated_files,
670
+ write_scope=write_scope,
671
+ )
672
+ if lifecycle.requires_runnable_scope:
673
+ return True
674
+ text = task_text_blob(
675
+ title=title,
676
+ description=description,
677
+ acceptance_criteria=acceptance_criteria,
678
+ )
679
+ if extract_repo_path_hints(text):
680
+ return lifecycle.mutating
681
+ return lifecycle.requires_runnable_scope
682
+
683
+
684
+ def normalize_task_file_fields(
685
+ *,
686
+ title: str,
687
+ description: str,
688
+ acceptance_criteria: list[str],
689
+ estimated_files: Any,
690
+ write_scope: Any,
691
+ warning_prefix: str,
692
+ latest_user_text: str = "",
693
+ ) -> TaskFileScopeNormalization:
694
+ warnings: list[str] = []
695
+ normalized_estimated, dropped_estimated = split_normalized_repo_path_list(estimated_files)
696
+ normalized_write_scope, dropped_write_scope = split_normalized_repo_path_list(write_scope)
697
+ if dropped_estimated:
698
+ warnings.append(
699
+ f"{warning_prefix}: dropped invalid estimated_files entries: "
700
+ + ", ".join(dropped_estimated)
701
+ )
702
+ if dropped_write_scope:
703
+ warnings.append(
704
+ f"{warning_prefix}: dropped invalid write_scope entries: "
705
+ + ", ".join(dropped_write_scope)
706
+ )
707
+
708
+ task_text = task_text_blob(
709
+ title=title,
710
+ description=description,
711
+ acceptance_criteria=acceptance_criteria,
712
+ )
713
+ initial_lifecycle = classify_task_lifecycle(
714
+ title=title,
715
+ description=description,
716
+ acceptance_criteria=acceptance_criteria,
717
+ estimated_files=normalized_estimated,
718
+ write_scope=normalized_write_scope,
719
+ )
720
+ clearly_non_mutating = initial_lifecycle.kind == TASK_KIND_ANALYSIS_ONLY
721
+ forbidden_paths = extract_forbidden_repo_path_hints(
722
+ "\n".join([task_text, str(latest_user_text or "")])
723
+ )
724
+ forbidden_identities = {_scope_identity_key(path) for path in forbidden_paths}
725
+
726
+ def _drop_forbidden(paths: list[str]) -> tuple[list[str], list[str]]:
727
+ kept: list[str] = []
728
+ dropped: list[str] = []
729
+ for path in paths:
730
+ if _scope_identity_key(path) in forbidden_identities:
731
+ dropped.append(path)
732
+ continue
733
+ kept.append(path)
734
+ return kept, dropped
735
+
736
+ inferred_paths = [] if clearly_non_mutating else extract_repo_path_hints(task_text)
737
+ inferred_paths, forbidden_inferred_paths = _drop_forbidden(inferred_paths)
738
+ if forbidden_inferred_paths:
739
+ warnings.append(
740
+ f"{warning_prefix}: ignored forbidden path hints from task text: "
741
+ + ", ".join(forbidden_inferred_paths)
742
+ )
743
+ inferred_paths, obsolete_inferred_paths = filter_obsolete_direction_paths(
744
+ inferred_paths,
745
+ latest_user_text=latest_user_text,
746
+ task_text=task_text,
747
+ )
748
+ if obsolete_inferred_paths:
749
+ warnings.append(
750
+ f"{warning_prefix}: ignored obsolete inferred path hints from direction-change text: "
751
+ + ", ".join(obsolete_inferred_paths)
752
+ )
753
+ added_inferred: list[str] = []
754
+ for path in inferred_paths:
755
+ if path not in normalized_estimated:
756
+ normalized_estimated.append(path)
757
+ added_inferred.append(path)
758
+ if added_inferred:
759
+ warnings.append(
760
+ f"{warning_prefix}: inferred estimated_files from task text: "
761
+ + ", ".join(added_inferred)
762
+ )
763
+
764
+ filtered_estimated: list[str] = []
765
+ dropped_internal_estimated: list[str] = []
766
+ for path in normalized_estimated:
767
+ if is_agent_internal_scope_path(path):
768
+ dropped_internal_estimated.append(path)
769
+ continue
770
+ filtered_estimated.append(path)
771
+ if dropped_internal_estimated:
772
+ warnings.append(
773
+ f"{warning_prefix}: dropped protected estimated_files entries: "
774
+ + ", ".join(dropped_internal_estimated)
775
+ )
776
+ filtered_estimated, dropped_forbidden_estimated = _drop_forbidden(filtered_estimated)
777
+ if dropped_forbidden_estimated:
778
+ warnings.append(
779
+ f"{warning_prefix}: dropped forbidden estimated_files entries: "
780
+ + ", ".join(dropped_forbidden_estimated)
781
+ )
782
+
783
+ filtered_write_scope: list[str] = []
784
+ dropped_internal_write_scope: list[str] = []
785
+ for path in normalized_write_scope:
786
+ if is_agent_internal_scope_path(path):
787
+ dropped_internal_write_scope.append(path)
788
+ continue
789
+ filtered_write_scope.append(path)
790
+ if dropped_internal_write_scope:
791
+ warnings.append(
792
+ f"{warning_prefix}: dropped protected write_scope entries: "
793
+ + ", ".join(dropped_internal_write_scope)
794
+ )
795
+ filtered_write_scope, dropped_forbidden_write_scope = _drop_forbidden(filtered_write_scope)
796
+ if dropped_forbidden_write_scope:
797
+ warnings.append(
798
+ f"{warning_prefix}: dropped forbidden write_scope entries: "
799
+ + ", ".join(dropped_forbidden_write_scope)
800
+ )
801
+
802
+ filtered_estimated = _dedupe_keep_order(filtered_estimated)
803
+ filtered_write_scope = _dedupe_keep_order(filtered_write_scope)
804
+ lifecycle = classify_task_lifecycle(
805
+ title=title,
806
+ description=description,
807
+ acceptance_criteria=acceptance_criteria,
808
+ estimated_files=filtered_estimated,
809
+ write_scope=filtered_write_scope,
810
+ )
811
+ if lifecycle.kind == TASK_KIND_ANALYSIS_ONLY:
812
+ if filtered_estimated or filtered_write_scope:
813
+ warnings.append(
814
+ f"{warning_prefix}: cleared file mutation scope for analysis-only/report-only task"
815
+ )
816
+ return TaskFileScopeNormalization(
817
+ estimated_files=[],
818
+ write_scope=[],
819
+ warnings=warnings,
820
+ requires_runnable_scope=False,
821
+ task_kind=lifecycle.kind,
822
+ task_kind_reason=lifecycle.reason_code,
823
+ zero_diff_policy=lifecycle.zero_diff_policy,
824
+ )
825
+ expanded_write_scope: list[str] = list(filtered_write_scope)
826
+ added_to_write_scope: list[str] = []
827
+ for path in filtered_estimated:
828
+ if path in expanded_write_scope:
829
+ continue
830
+ expanded_write_scope.append(path)
831
+ added_to_write_scope.append(path)
832
+ if added_to_write_scope:
833
+ warnings.append(
834
+ f"{warning_prefix}: expanded write_scope to include estimated_files: "
835
+ + ", ".join(added_to_write_scope)
836
+ )
837
+ requires_runnable_scope = task_requires_runnable_file_scope(
838
+ title=title,
839
+ description=description,
840
+ acceptance_criteria=acceptance_criteria,
841
+ estimated_files=filtered_estimated,
842
+ write_scope=expanded_write_scope,
843
+ )
844
+ if requires_runnable_scope and not has_runnable_local_file_scope(
845
+ estimated_files=filtered_estimated,
846
+ write_scope=expanded_write_scope,
847
+ ):
848
+ warnings.append(f"{warning_prefix}: {MUTATING_TASK_SCOPE_WARNING}")
849
+ return TaskFileScopeNormalization(
850
+ estimated_files=filtered_estimated,
851
+ write_scope=expanded_write_scope,
852
+ warnings=warnings,
853
+ requires_runnable_scope=requires_runnable_scope,
854
+ task_kind=lifecycle.kind,
855
+ task_kind_reason=lifecycle.reason_code,
856
+ zero_diff_policy=lifecycle.zero_diff_policy,
857
+ )
858
+
859
+
860
+ def task_readiness_warning(*, task_id: str, title: str = "") -> str:
861
+ label = str(task_id or "").strip() or "-"
862
+ title_text = str(title or "").strip()
863
+ if title_text:
864
+ return f"Task {label} ({title_text}): {EXECUTION_UNREADY_SCOPE_WARNING}"
865
+ return f"Task {label}: {EXECUTION_UNREADY_SCOPE_WARNING}"
866
+
867
+
868
+ def manual_task_scope_error_message(*, title: str) -> str:
869
+ title_text = str(title or "").strip()
870
+ if title_text:
871
+ return (
872
+ f"Task '{title_text}' looks like runnable or ambiguous work but lacks runnable "
873
+ "file scope. Include repo-relative file paths in /task, use /plan edit / the "
874
+ "planner flow to produce execution-ready scope, or mark the task as "
875
+ "analysis-only/report-only if no file mutation is intended."
876
+ )
877
+ return (
878
+ "Task looks like runnable or ambiguous work but lacks runnable file scope. Include "
879
+ "repo-relative file paths in /task, use /plan edit / the planner flow to produce "
880
+ "execution-ready scope, or mark the task as analysis-only/report-only if no file "
881
+ "mutation is intended."
882
+ )
883
+
884
+
885
+ def _canonical_task_status(status: str) -> str:
886
+ value = (status or "").strip().lower()
887
+ if value == "todo":
888
+ return "planned"
889
+ return value or "planned"
890
+
891
+
892
+ def _parse_only_ids(only: str | None) -> set[str] | None:
893
+ if only is None:
894
+ return None
895
+ ids = {part.strip() for part in only.split(",") if part.strip()}
896
+ return ids or None
897
+
898
+
899
+ def status_is_execution_candidate(
900
+ status: str,
901
+ *,
902
+ retry_failed: bool,
903
+ retry_changes_requested: bool,
904
+ retry_merge_conflicts: bool,
905
+ ) -> bool:
906
+ if status in {"planned", "ready_for_merge"}:
907
+ return True
908
+ if status in {"failed", "verify_failed", "candidate_rejected"} and retry_failed:
909
+ return True
910
+ if status == "changes_requested" and retry_changes_requested:
911
+ return True
912
+ if status == "merge_conflict" and retry_merge_conflicts:
913
+ return True
914
+ return False
915
+
916
+
917
+ _status_is_execution_candidate = status_is_execution_candidate
918
+
919
+
920
+ def task_is_missing_runnable_scope(task: dict[str, Any]) -> bool:
921
+ estimated_files, write_scope = _normalize_existing_task_scope_fields(
922
+ estimated_files=task.get("estimated_files"),
923
+ write_scope=task.get("write_scope"),
924
+ )
925
+ return task_requires_runnable_file_scope(
926
+ title=str(task.get("title") or "").strip(),
927
+ description=str(task.get("description") or "").strip(),
928
+ acceptance_criteria=normalized_text_list(task.get("acceptance_criteria")),
929
+ estimated_files=estimated_files,
930
+ write_scope=write_scope,
931
+ ) and not has_runnable_local_file_scope(
932
+ estimated_files=estimated_files,
933
+ write_scope=write_scope,
934
+ )
935
+
936
+
937
+ def find_execution_unready_mutating_tasks(
938
+ plan: dict[str, Any],
939
+ *,
940
+ retry_failed: bool = False,
941
+ retry_changes_requested: bool = False,
942
+ retry_merge_conflicts: bool = False,
943
+ only: str | None = None,
944
+ ) -> list[ExecutionUnreadyTask]:
945
+ only_ids = _parse_only_ids(only)
946
+ tasks_raw = plan.get("tasks")
947
+ if not isinstance(tasks_raw, list):
948
+ return []
949
+
950
+ issues: list[ExecutionUnreadyTask] = []
951
+ for index, task in enumerate(tasks_raw):
952
+ if not isinstance(task, dict):
953
+ continue
954
+ task_id = str(task.get("id") or "").strip() or f"task[{index}]"
955
+ status = _canonical_task_status(str(task.get("status") or ""))
956
+ # Merge-phase tasks are handled before scheduler filtering, so mirror run_swarm's
957
+ # execution surface instead of treating --only as a complete preflight filter.
958
+ if (
959
+ only_ids is not None
960
+ and task_id not in only_ids
961
+ and status != "ready_for_merge"
962
+ and not (retry_merge_conflicts and status == "merge_conflict")
963
+ ):
964
+ continue
965
+ if not status_is_execution_candidate(
966
+ status,
967
+ retry_failed=retry_failed,
968
+ retry_changes_requested=retry_changes_requested,
969
+ retry_merge_conflicts=retry_merge_conflicts,
970
+ ):
971
+ continue
972
+ title = str(task.get("title") or "").strip()
973
+ if not task_is_missing_runnable_scope(task):
974
+ continue
975
+ issues.append(
976
+ ExecutionUnreadyTask(
977
+ task_id=task_id,
978
+ title=title,
979
+ status=status,
980
+ warning=task_readiness_warning(task_id=task_id, title=title),
981
+ )
982
+ )
983
+ return issues
984
+
985
+
986
+ def format_execution_readiness_block(issues: list[ExecutionUnreadyTask]) -> str:
987
+ if not issues:
988
+ return ""
989
+ preview = "; ".join(issue.warning for issue in issues[:5])
990
+ if len(issues) > 5:
991
+ preview += f"; +{len(issues) - 5} more"
992
+ return "Execution blocked: " + preview