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,904 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from fnmatch import fnmatchcase
6
+ from pathlib import PurePosixPath
7
+ from typing import Any, Literal
8
+
9
+ from .file_classification import is_code_implementation_path, is_test_path
10
+ from .task_scope import (
11
+ extract_forbidden_repo_path_hint_records,
12
+ normalize_repo_path_entry,
13
+ split_normalized_repo_path_list,
14
+ )
15
+
16
+ PLANNING_CONSTRAINTS_KEY = "planning_constraints"
17
+ PLANNING_CONSTRAINTS_SCHEMA_VERSION = 1
18
+
19
+ PlanningConstraintKind = Literal[
20
+ "target_root",
21
+ "forbidden_root",
22
+ "decoy_root",
23
+ "unrelated_root",
24
+ ]
25
+
26
+ _PATHLIKE_RE = re.compile(
27
+ r"(?<![\w/-])("
28
+ r"(?:[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+)"
29
+ r"|(?:[A-Za-z0-9_.-]+\.[A-Za-z0-9_*?\[\]-]+)"
30
+ r"|(?:README(?:\.md)?)"
31
+ r"|(?:Dockerfile|Makefile|LICENSE|NOTICE|CHANGELOG)"
32
+ r")(?![\w/-])"
33
+ )
34
+ _CONTEXT_SPLIT_RE = re.compile(r"[\n;]+|(?<=[.!?])\s+")
35
+ _MONOREPO_CONTAINER_NAMES = frozenset(
36
+ {
37
+ "app",
38
+ "apps",
39
+ "component",
40
+ "components",
41
+ "crate",
42
+ "crates",
43
+ "lib",
44
+ "libs",
45
+ "module",
46
+ "modules",
47
+ "package",
48
+ "packages",
49
+ "service",
50
+ "services",
51
+ "workspace",
52
+ "workspaces",
53
+ }
54
+ )
55
+ _TARGET_DIRECT_RE = re.compile(
56
+ r"\b(?:only|only\s+(?:edit|fix|modify|touch|change|update)|"
57
+ r"stay\s+(?:scoped\s+)?(?:to|inside|within)|scope(?:d)?\s+(?:to|inside|within)|"
58
+ r"target(?:ing)?|target\s+root|switch\s+to|focus\s+on|work\s+on|"
59
+ r"inside|under|within|in|for)\b",
60
+ re.IGNORECASE,
61
+ )
62
+ _TARGET_INTENT_RE = re.compile(
63
+ r"\b(?:add|adding|build|building|change|changing|configure|configuring|edit|editing|"
64
+ r"enable|enabling|fix|fixing|implement|implementing|improve|improving|modify|"
65
+ r"modifying|patch|patching|repair|repairing|support|supporting|update|updating|"
66
+ r"wire|wiring|work\s+on|working\s+on)\b",
67
+ re.IGNORECASE,
68
+ )
69
+ _FORBIDDEN_DIRECT_RE = re.compile(
70
+ r"\b(?:do\s+not|don't|dont|must\s+not|never)\s+"
71
+ r"(?:edit|modify|change|touch|write(?:\s+to)?|update|delete|remove|create)\b"
72
+ r"|\b(?:leave|keep)\s+(?:the\s+)?(?:file\s+)?(?:[\w./-]+\s+)?"
73
+ r"(?:unchanged|untouched|unmodified)\b"
74
+ r"|\b(?:remain|stays?|stay|left)\s+(?:unchanged|untouched|unmodified)\b",
75
+ re.IGNORECASE,
76
+ )
77
+ _FORBIDDEN_AFTER_RE = re.compile(
78
+ r"^\s*(?:file\s+)?(?:is|are|must\s+remain|should\s+remain|stays?|stay|left)?\s*"
79
+ r"(?:not\s+)?(?:untouched|unchanged|unmodified|preserved|not\s+modified|"
80
+ r"not\s+changed|not\s+touched)\b"
81
+ r"|^\s*(?:file\s+)?not\s+(?:modified|changed|touched|written)\b",
82
+ re.IGNORECASE,
83
+ )
84
+ _DECOY_RE = re.compile(
85
+ r"\b(?:decoy|negative\s+example|example\s+only|not\s+an?\s+(?:target|task))\b",
86
+ re.IGNORECASE,
87
+ )
88
+ _UNRELATED_RE = re.compile(
89
+ r"\b(?:unrelated|out\s+of\s+scope|not\s+relevant|irrelevant)\b",
90
+ re.IGNORECASE,
91
+ )
92
+ _IGNORE_RE = re.compile(r"\b(?:ignore|skip|exclude)\b", re.IGNORECASE)
93
+ _ONLY_RE = re.compile(r"\bonly\b", re.IGNORECASE)
94
+ _ADDITIVE_TARGET_RE = re.compile(
95
+ r"\b(?:also|additionally|as\s+well|too|include)\b",
96
+ re.IGNORECASE,
97
+ )
98
+ _EXCLUSIVE_TARGET_RE = re.compile(
99
+ r"\b(?:only|stay\s+(?:scoped\s+)?(?:to|inside|within)|"
100
+ r"scope(?:d)?\s+(?:to|inside|within)|target\s+root)\b",
101
+ re.IGNORECASE,
102
+ )
103
+ _RETARGET_RE = re.compile(
104
+ r"\b(?:actually|instead|now|switch\s+to|retarget(?:\s+to)?|"
105
+ r"change\s+(?:the\s+)?(?:target|scope|focus)\s+to|"
106
+ r"move\s+(?:the\s+)?(?:target|scope|focus)\s+to)\b",
107
+ re.IGNORECASE,
108
+ )
109
+ _MARKER_WINDOW_CHARS = 80
110
+ _SHARED_EVIDENCE_RE = re.compile(
111
+ r"\b(?:shared|common|cross[- ]?package|cross[- ]?service|dependency|depends\s+on|"
112
+ r"library|lib|used\s+by|imported\s+by)\b",
113
+ re.IGNORECASE,
114
+ )
115
+ _TEST_EVIDENCE_RE = re.compile(r"\b(?:test|tests|spec|coverage|regression)\b", re.IGNORECASE)
116
+ _NON_TARGET_SURFACE_RE = re.compile(
117
+ r"^(?:README(?:\.md)?|docs?/|notes?/|changelog(?:\.md)?|LICENSE|NOTICE)",
118
+ re.IGNORECASE,
119
+ )
120
+ _GLOB_CHARS = ("*", "?", "[")
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class PlanningPathConstraint:
125
+ path: str
126
+ kind: PlanningConstraintKind
127
+ reason_code: str
128
+ evidence: str
129
+
130
+ def to_payload(self) -> dict[str, str]:
131
+ return {
132
+ "path": self.path,
133
+ "kind": self.kind,
134
+ "reason_code": self.reason_code,
135
+ "evidence": self.evidence,
136
+ }
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class PlanningScopeConstraints:
141
+ target_roots: tuple[PlanningPathConstraint, ...] = ()
142
+ forbidden_roots: tuple[PlanningPathConstraint, ...] = ()
143
+ decoy_roots: tuple[PlanningPathConstraint, ...] = ()
144
+ unrelated_roots: tuple[PlanningPathConstraint, ...] = ()
145
+
146
+ @property
147
+ def has_constraints(self) -> bool:
148
+ return bool(
149
+ self.target_roots or self.forbidden_roots or self.decoy_roots or self.unrelated_roots
150
+ )
151
+
152
+ @property
153
+ def blocked_roots(self) -> tuple[PlanningPathConstraint, ...]:
154
+ return (*self.forbidden_roots, *self.decoy_roots, *self.unrelated_roots)
155
+
156
+ def to_payload(self) -> dict[str, Any]:
157
+ return {
158
+ "schema_version": PLANNING_CONSTRAINTS_SCHEMA_VERSION,
159
+ "target_roots": [item.to_payload() for item in self.target_roots],
160
+ "forbidden_roots": [item.to_payload() for item in self.forbidden_roots],
161
+ "decoy_roots": [item.to_payload() for item in self.decoy_roots],
162
+ "unrelated_roots": [item.to_payload() for item in self.unrelated_roots],
163
+ }
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class PlanningScopeViolation:
168
+ path: str
169
+ classification: str
170
+ reason_code: str
171
+ evidence: str
172
+ constraint_path: str | None = None
173
+
174
+ def to_payload(self) -> dict[str, str]:
175
+ payload = {
176
+ "path": self.path,
177
+ "classification": self.classification,
178
+ "reason_code": self.reason_code,
179
+ "evidence": self.evidence,
180
+ }
181
+ if self.constraint_path:
182
+ payload["constraint_path"] = self.constraint_path
183
+ return payload
184
+
185
+
186
+ def _dedupe_constraints(
187
+ constraints: list[PlanningPathConstraint],
188
+ ) -> tuple[PlanningPathConstraint, ...]:
189
+ seen: set[tuple[str, str]] = set()
190
+ out: list[PlanningPathConstraint] = []
191
+ for item in constraints:
192
+ key = (item.kind, _constraint_identity(item.path))
193
+ if key in seen:
194
+ continue
195
+ seen.add(key)
196
+ out.append(item)
197
+ return tuple(out)
198
+
199
+
200
+ def _constraint_identity(path: str) -> str:
201
+ return _constraint_root(path).casefold()
202
+
203
+
204
+ def _constraint_root(path: str) -> str:
205
+ cleaned = str(path or "").strip().replace("\\", "/")
206
+ while cleaned.startswith("./"):
207
+ cleaned = cleaned[2:]
208
+ cleaned = cleaned.rstrip("/")
209
+ if cleaned.endswith("/**"):
210
+ cleaned = cleaned[:-3].rstrip("/")
211
+ if "/**/" in cleaned:
212
+ cleaned = cleaned.split("/**/", 1)[0].rstrip("/")
213
+ return cleaned
214
+
215
+
216
+ def _normalize_constraint_path(value: str) -> str | None:
217
+ normalized = normalize_repo_path_entry(str(value or ""), allow_extensionless_file=True)
218
+ if not normalized:
219
+ return None
220
+ return _constraint_root(normalized)
221
+
222
+
223
+ def _looks_like_file_path(path: str) -> bool:
224
+ name = PurePosixPath(_constraint_root(path)).name
225
+ return "." in name or name in {
226
+ "README",
227
+ "Dockerfile",
228
+ "Makefile",
229
+ "LICENSE",
230
+ "NOTICE",
231
+ "CHANGELOG",
232
+ }
233
+
234
+
235
+ def _evidence_fragment(context: str) -> str:
236
+ cleaned = " ".join(str(context or "").strip().split())
237
+ if len(cleaned) <= 180:
238
+ return cleaned
239
+ return cleaned[:177].rstrip() + "..."
240
+
241
+
242
+ def _path_mentions(text: str) -> list[tuple[str, str]]:
243
+ mentions: list[tuple[str, str]] = []
244
+ seen: set[str] = set()
245
+ contexts = [item.strip() for item in _CONTEXT_SPLIT_RE.split(text or "") if item.strip()]
246
+ for context in contexts:
247
+ for match in _PATHLIKE_RE.finditer(context):
248
+ normalized = _normalize_constraint_path(match.group(1))
249
+ if not normalized:
250
+ continue
251
+ key = (context.casefold(), normalized.casefold())
252
+ if str(key) in seen:
253
+ continue
254
+ seen.add(str(key))
255
+ mentions.append((normalized, context))
256
+ return mentions
257
+
258
+
259
+ def _path_context_windows(context: str, path: str) -> tuple[str, str, str]:
260
+ context_text = str(context or "")
261
+ context_cf = context_text.casefold()
262
+ path_cf = str(path or "").casefold()
263
+ start = context_cf.find(path_cf)
264
+ if start < 0:
265
+ return context_text, "", context_text
266
+ end = start + len(path_cf)
267
+ before = context_text[max(0, start - _MARKER_WINDOW_CHARS) : start]
268
+ after = context_text[end : end + _MARKER_WINDOW_CHARS]
269
+ return before + context_text[start:end] + after, before, after
270
+
271
+
272
+ def _negative_marker_applies_before_path(
273
+ *,
274
+ before: str,
275
+ marker_re: re.Pattern[str],
276
+ ) -> bool:
277
+ matches = list(marker_re.finditer(before))
278
+ if not matches:
279
+ return False
280
+ last = matches[-1]
281
+ between = before[last.end() :]
282
+ return _TARGET_INTENT_RE.search(between) is None
283
+
284
+
285
+ def _context_reason_for_path(context: str, path: str) -> tuple[PlanningConstraintKind, str] | None:
286
+ local, before, after = _path_context_windows(context, path)
287
+ if _FORBIDDEN_AFTER_RE.search(after) or _negative_marker_applies_before_path(
288
+ before=before,
289
+ marker_re=_FORBIDDEN_DIRECT_RE,
290
+ ):
291
+ return "forbidden_root", "direct_path_forbidden_instruction"
292
+ if _DECOY_RE.search(after) or _negative_marker_applies_before_path(
293
+ before=before,
294
+ marker_re=_DECOY_RE,
295
+ ):
296
+ return "decoy_root", "decoy_path_constraint"
297
+ if _UNRELATED_RE.search(after) or _negative_marker_applies_before_path(
298
+ before=before,
299
+ marker_re=_UNRELATED_RE,
300
+ ):
301
+ return "unrelated_root", "unrelated_path_constraint"
302
+ if _IGNORE_RE.search(after) or _negative_marker_applies_before_path(
303
+ before=before,
304
+ marker_re=_IGNORE_RE,
305
+ ):
306
+ return "decoy_root", "ignored_path_constraint"
307
+ if not _looks_like_file_path(path) and (
308
+ _TARGET_DIRECT_RE.search(local) or _TARGET_INTENT_RE.search(local)
309
+ ):
310
+ return "target_root", "user_target_root"
311
+ return None
312
+
313
+
314
+ def _append_constraint(
315
+ items: list[PlanningPathConstraint],
316
+ *,
317
+ path: str,
318
+ kind: PlanningConstraintKind,
319
+ reason_code: str,
320
+ evidence: str,
321
+ ) -> None:
322
+ normalized = _normalize_constraint_path(path)
323
+ if normalized is None:
324
+ return
325
+ items.append(
326
+ PlanningPathConstraint(
327
+ path=normalized,
328
+ kind=kind,
329
+ reason_code=reason_code,
330
+ evidence=_evidence_fragment(evidence),
331
+ )
332
+ )
333
+
334
+
335
+ def _workspace_candidate_roots(workspace_context: dict[str, Any] | None) -> tuple[str, ...]:
336
+ if not isinstance(workspace_context, dict):
337
+ return ()
338
+ roots: list[str] = []
339
+ for entry in workspace_context.get("manifests") or []:
340
+ if not isinstance(entry, dict):
341
+ continue
342
+ path = _normalize_constraint_path(str(entry.get("path") or ""))
343
+ if not path or "/" not in path:
344
+ continue
345
+ parent = path.rsplit("/", 1)[0]
346
+ if parent and parent != ".":
347
+ roots.append(parent)
348
+ for path in workspace_context.get("observed_paths") or []:
349
+ normalized = _normalize_constraint_path(str(path or ""))
350
+ if not normalized or "/" not in normalized:
351
+ continue
352
+ parts = [part for part in normalized.split("/") if part]
353
+ if len(parts) >= 2 and parts[0].casefold() in _MONOREPO_CONTAINER_NAMES:
354
+ roots.append("/".join(parts[:2]))
355
+ focus_relpath = _normalize_constraint_path(str(workspace_context.get("focus_relpath") or ""))
356
+ if focus_relpath and focus_relpath != "." and "/" in focus_relpath:
357
+ roots.append(focus_relpath)
358
+
359
+ seen: set[str] = set()
360
+ out: list[str] = []
361
+ for root in roots:
362
+ key = root.casefold()
363
+ if key in seen:
364
+ continue
365
+ seen.add(key)
366
+ out.append(root)
367
+ return tuple(out)
368
+
369
+
370
+ def _workspace_known_scope_paths(workspace_context: dict[str, Any] | None) -> tuple[str, ...]:
371
+ if not isinstance(workspace_context, dict):
372
+ return ()
373
+ raw_paths: list[str] = []
374
+ raw_paths.extend(str(item or "") for item in workspace_context.get("observed_paths") or [])
375
+ raw_paths.extend(str(item or "") for item in workspace_context.get("readme_paths") or [])
376
+ conventions_path = str(workspace_context.get("conventions_path") or "").strip()
377
+ if conventions_path:
378
+ raw_paths.append(conventions_path)
379
+ for entry in workspace_context.get("manifests") or []:
380
+ if isinstance(entry, dict):
381
+ raw_paths.append(str(entry.get("path") or ""))
382
+ for entry in workspace_context.get("top_level_entries") or []:
383
+ if isinstance(entry, dict):
384
+ raw_paths.append(str(entry.get("path") or ""))
385
+
386
+ known: list[str] = []
387
+ seen: set[str] = set()
388
+ for raw in raw_paths:
389
+ normalized = _normalize_constraint_path(raw)
390
+ if not normalized:
391
+ continue
392
+ parts = [part for part in normalized.split("/") if part]
393
+ candidates = [normalized]
394
+ if len(parts) > 1:
395
+ candidates.extend("/".join(parts[:index]) for index in range(1, len(parts)))
396
+ for candidate in candidates:
397
+ key = candidate.casefold()
398
+ if key in seen:
399
+ continue
400
+ seen.add(key)
401
+ known.append(candidate)
402
+ return tuple(known)
403
+
404
+
405
+ def _path_is_grounded_in_workspace_context(
406
+ path: str,
407
+ *,
408
+ workspace_context: dict[str, Any] | None,
409
+ ) -> bool:
410
+ if not isinstance(workspace_context, dict):
411
+ return True
412
+ normalized = _normalize_constraint_path(path)
413
+ if not normalized:
414
+ return False
415
+ path_key = _constraint_root(normalized).casefold()
416
+ if not path_key:
417
+ return False
418
+ for candidate in (
419
+ *_workspace_candidate_roots(workspace_context),
420
+ *_workspace_known_scope_paths(workspace_context),
421
+ ):
422
+ candidate_key = _constraint_root(candidate).casefold()
423
+ if not candidate_key:
424
+ continue
425
+ if (
426
+ path_key == candidate_key
427
+ or candidate_key.startswith(path_key + "/")
428
+ or ("/" in candidate_key and path_key.startswith(candidate_key + "/"))
429
+ ):
430
+ return True
431
+ return False
432
+
433
+
434
+ def _candidate_roots_by_leaf(workspace_context: dict[str, Any] | None) -> dict[str, str]:
435
+ grouped: dict[str, list[str]] = {}
436
+ for root in _workspace_candidate_roots(workspace_context):
437
+ leaf = root.rsplit("/", 1)[-1].casefold()
438
+ if not leaf:
439
+ continue
440
+ grouped.setdefault(leaf, []).append(root)
441
+ return {
442
+ leaf: roots[0]
443
+ for leaf, roots in grouped.items()
444
+ if len({root.casefold() for root in roots}) == 1
445
+ }
446
+
447
+
448
+ def _word_present(text: str, word: str) -> bool:
449
+ return re.search(rf"(?<![A-Za-z0-9_]){re.escape(word)}(?![A-Za-z0-9_])", text, re.I) is not None
450
+
451
+
452
+ def _word_context_windows(context: str, word: str) -> tuple[str, str, str] | None:
453
+ match = re.search(rf"(?<![A-Za-z0-9_]){re.escape(word)}(?![A-Za-z0-9_])", context, re.I)
454
+ if match is None:
455
+ return None
456
+ start, end = match.span()
457
+ before = context[max(0, start - _MARKER_WINDOW_CHARS) : start]
458
+ after = context[end : end + _MARKER_WINDOW_CHARS]
459
+ return before + context[start:end] + after, before, after
460
+
461
+
462
+ def _leaf_name_constraints(
463
+ text: str,
464
+ *,
465
+ workspace_context: dict[str, Any] | None,
466
+ ) -> list[PlanningPathConstraint]:
467
+ by_leaf = _candidate_roots_by_leaf(workspace_context)
468
+ if not by_leaf:
469
+ return []
470
+ constraints: list[PlanningPathConstraint] = []
471
+ contexts = [item.strip() for item in _CONTEXT_SPLIT_RE.split(text or "") if item.strip()]
472
+ for context in contexts:
473
+ context_cf = context.casefold()
474
+ for leaf, root in by_leaf.items():
475
+ if not _word_present(context_cf, leaf):
476
+ continue
477
+ windows = _word_context_windows(context, leaf)
478
+ if windows is None:
479
+ continue
480
+ local, before, after = windows
481
+ if _FORBIDDEN_AFTER_RE.search(after) or _negative_marker_applies_before_path(
482
+ before=before,
483
+ marker_re=_FORBIDDEN_DIRECT_RE,
484
+ ):
485
+ _append_constraint(
486
+ constraints,
487
+ path=root,
488
+ kind="forbidden_root",
489
+ reason_code="direct_path_forbidden_instruction",
490
+ evidence=context,
491
+ )
492
+ elif _DECOY_RE.search(after) or _negative_marker_applies_before_path(
493
+ before=before,
494
+ marker_re=_DECOY_RE,
495
+ ):
496
+ _append_constraint(
497
+ constraints,
498
+ path=root,
499
+ kind="decoy_root",
500
+ reason_code="decoy_path_constraint",
501
+ evidence=context,
502
+ )
503
+ elif (
504
+ _UNRELATED_RE.search(after)
505
+ or _negative_marker_applies_before_path(before=before, marker_re=_UNRELATED_RE)
506
+ or _IGNORE_RE.search(after)
507
+ or _negative_marker_applies_before_path(before=before, marker_re=_IGNORE_RE)
508
+ ):
509
+ _append_constraint(
510
+ constraints,
511
+ path=root,
512
+ kind="unrelated_root",
513
+ reason_code="unrelated_path_constraint",
514
+ evidence=context,
515
+ )
516
+ elif (
517
+ _ONLY_RE.search(local)
518
+ or _TARGET_DIRECT_RE.search(local)
519
+ or _TARGET_INTENT_RE.search(local)
520
+ ):
521
+ _append_constraint(
522
+ constraints,
523
+ path=root,
524
+ kind="target_root",
525
+ reason_code="user_target_root_name",
526
+ evidence=context,
527
+ )
528
+ return constraints
529
+
530
+
531
+ def extract_planning_scope_constraints(
532
+ text: str,
533
+ *,
534
+ workspace_context: dict[str, Any] | None = None,
535
+ ) -> PlanningScopeConstraints:
536
+ raw_constraints: list[PlanningPathConstraint] = []
537
+ for forbidden in extract_forbidden_repo_path_hint_records(text or ""):
538
+ _append_constraint(
539
+ raw_constraints,
540
+ path=forbidden.path,
541
+ kind="forbidden_root",
542
+ reason_code=forbidden.reason_code,
543
+ evidence=forbidden.evidence,
544
+ )
545
+ for path, context in _path_mentions(text or ""):
546
+ reason = _context_reason_for_path(context, path)
547
+ if reason is None:
548
+ continue
549
+ kind, reason_code = reason
550
+ if kind == "target_root" and not _path_is_grounded_in_workspace_context(
551
+ path,
552
+ workspace_context=workspace_context,
553
+ ):
554
+ continue
555
+ _append_constraint(
556
+ raw_constraints,
557
+ path=path,
558
+ kind=kind,
559
+ reason_code=reason_code,
560
+ evidence=context,
561
+ )
562
+ raw_constraints.extend(_leaf_name_constraints(text or "", workspace_context=workspace_context))
563
+ return _constraints_from_items(raw_constraints)
564
+
565
+
566
+ def _constraints_from_items(items: list[PlanningPathConstraint]) -> PlanningScopeConstraints:
567
+ forbidden = [item for item in items if item.kind == "forbidden_root"]
568
+ decoy = [item for item in items if item.kind == "decoy_root"]
569
+ unrelated = [item for item in items if item.kind == "unrelated_root"]
570
+ blocked_keys = {_constraint_identity(item.path) for item in [*forbidden, *decoy, *unrelated]}
571
+ target = [
572
+ item
573
+ for item in items
574
+ if item.kind == "target_root" and _constraint_identity(item.path) not in blocked_keys
575
+ ]
576
+ return PlanningScopeConstraints(
577
+ target_roots=_dedupe_constraints(target),
578
+ forbidden_roots=_dedupe_constraints(forbidden),
579
+ decoy_roots=_dedupe_constraints(decoy),
580
+ unrelated_roots=_dedupe_constraints(unrelated),
581
+ )
582
+
583
+
584
+ def planning_constraints_from_payload(raw: Any) -> PlanningScopeConstraints:
585
+ if not isinstance(raw, dict):
586
+ return PlanningScopeConstraints()
587
+ items: list[PlanningPathConstraint] = []
588
+ for key, kind in (
589
+ ("target_roots", "target_root"),
590
+ ("forbidden_roots", "forbidden_root"),
591
+ ("decoy_roots", "decoy_root"),
592
+ ("unrelated_roots", "unrelated_root"),
593
+ ):
594
+ for entry in raw.get(key) or []:
595
+ if not isinstance(entry, dict):
596
+ continue
597
+ path = _normalize_constraint_path(str(entry.get("path") or ""))
598
+ if not path:
599
+ continue
600
+ items.append(
601
+ PlanningPathConstraint(
602
+ path=path,
603
+ kind=kind, # type: ignore[arg-type]
604
+ reason_code=str(entry.get("reason_code") or kind).strip() or kind,
605
+ evidence=str(entry.get("evidence") or "").strip(),
606
+ )
607
+ )
608
+ return _constraints_from_items(items)
609
+
610
+
611
+ def planning_constraints_from_plan(plan: dict[str, Any]) -> PlanningScopeConstraints:
612
+ return planning_constraints_from_payload(plan.get(PLANNING_CONSTRAINTS_KEY))
613
+
614
+
615
+ def merge_planning_scope_constraints(
616
+ *constraints: PlanningScopeConstraints,
617
+ replace_target_roots: bool = False,
618
+ unblock_target_roots: bool = False,
619
+ ) -> PlanningScopeConstraints:
620
+ items: list[PlanningPathConstraint] = []
621
+ latest = constraints[-1] if constraints else PlanningScopeConstraints()
622
+ latest_target_keys = (
623
+ {_constraint_identity(item.path) for item in latest.target_roots}
624
+ if unblock_target_roots
625
+ else set()
626
+ )
627
+ for index, constraint_set in enumerate(constraints):
628
+ is_latest = index == len(constraints) - 1
629
+ if not replace_target_roots or is_latest:
630
+ items.extend(constraint_set.target_roots)
631
+ for blocked in [
632
+ *constraint_set.forbidden_roots,
633
+ *constraint_set.decoy_roots,
634
+ *constraint_set.unrelated_roots,
635
+ ]:
636
+ if (
637
+ unblock_target_roots
638
+ and not is_latest
639
+ and _constraint_identity(blocked.path) in latest_target_keys
640
+ ):
641
+ continue
642
+ items.append(blocked)
643
+ return _constraints_from_items(items)
644
+
645
+
646
+ def _latest_text_replaces_target_roots(
647
+ *,
648
+ text: str,
649
+ extracted: PlanningScopeConstraints,
650
+ direction_change: Any | None,
651
+ ) -> bool:
652
+ if not extracted.target_roots:
653
+ return False
654
+ if _ADDITIVE_TARGET_RE.search(text or ""):
655
+ return False
656
+ if direction_change is not None:
657
+ return True
658
+ return bool(_EXCLUSIVE_TARGET_RE.search(text or "") or _RETARGET_RE.search(text or ""))
659
+
660
+
661
+ def merge_latest_planning_scope_constraints(
662
+ existing: PlanningScopeConstraints,
663
+ *,
664
+ text: str,
665
+ workspace_context: dict[str, Any] | None = None,
666
+ direction_change: Any | None = None,
667
+ ) -> PlanningScopeConstraints:
668
+ extracted = extract_planning_scope_constraints(text, workspace_context=workspace_context)
669
+ return merge_planning_scope_constraints(
670
+ existing,
671
+ extracted,
672
+ replace_target_roots=_latest_text_replaces_target_roots(
673
+ text=text,
674
+ extracted=extracted,
675
+ direction_change=direction_change,
676
+ ),
677
+ unblock_target_roots=bool(extracted.target_roots),
678
+ )
679
+
680
+
681
+ def update_plan_planning_constraints(
682
+ plan: dict[str, Any],
683
+ *,
684
+ text: str,
685
+ workspace_context: dict[str, Any] | None = None,
686
+ direction_change: Any | None = None,
687
+ ) -> tuple[PlanningScopeConstraints, bool]:
688
+ existing = planning_constraints_from_plan(plan)
689
+ merged = merge_latest_planning_scope_constraints(
690
+ existing,
691
+ text=text,
692
+ workspace_context=workspace_context,
693
+ direction_change=direction_change,
694
+ )
695
+ if not merged.has_constraints:
696
+ return merged, False
697
+ payload = merged.to_payload()
698
+ if plan.get(PLANNING_CONSTRAINTS_KEY) == payload:
699
+ return merged, False
700
+ plan[PLANNING_CONSTRAINTS_KEY] = payload
701
+ return merged, True
702
+
703
+
704
+ def planning_constraints_prompt_payload(
705
+ constraints: PlanningScopeConstraints,
706
+ ) -> dict[str, Any] | None:
707
+ if not constraints.has_constraints:
708
+ return None
709
+ return constraints.to_payload()
710
+
711
+
712
+ def _has_glob(path: str) -> bool:
713
+ return any(char in path for char in _GLOB_CHARS)
714
+
715
+
716
+ def _glob_prefix(path: str) -> str:
717
+ prefix_chars: list[str] = []
718
+ for char in path:
719
+ if char in _GLOB_CHARS:
720
+ break
721
+ prefix_chars.append(char)
722
+ return _constraint_root("".join(prefix_chars))
723
+
724
+
725
+ def path_matches_planning_root(path: str, root: str) -> bool:
726
+ normalized_path = _normalize_constraint_path(path)
727
+ normalized_root = _normalize_constraint_path(root)
728
+ if not normalized_path or not normalized_root:
729
+ return False
730
+ if _has_glob(normalized_root):
731
+ return fnmatchcase(normalized_path, normalized_root)
732
+ if _has_glob(normalized_path):
733
+ prefix = _glob_prefix(normalized_path)
734
+ return bool(prefix) and (
735
+ prefix.casefold() == normalized_root.casefold()
736
+ or prefix.casefold().startswith(normalized_root.casefold() + "/")
737
+ )
738
+ path_key = normalized_path.casefold()
739
+ root_key = normalized_root.casefold()
740
+ return path_key == root_key or path_key.startswith(root_key + "/")
741
+
742
+
743
+ def path_within_target_roots(
744
+ path: str,
745
+ constraints: PlanningScopeConstraints,
746
+ ) -> bool:
747
+ if not constraints.target_roots:
748
+ return True
749
+ return any(path_matches_planning_root(path, item.path) for item in constraints.target_roots)
750
+
751
+
752
+ def _path_text_parts(path: str) -> set[str]:
753
+ cleaned = _constraint_root(path).replace("\\", "/").casefold()
754
+ parts: set[str] = set()
755
+ for part in PurePosixPath(cleaned).parts:
756
+ if not part or part == ".":
757
+ continue
758
+ stem = part.split(".", 1)[0]
759
+ parts.add(part)
760
+ if stem:
761
+ parts.add(stem)
762
+ return parts
763
+
764
+
765
+ def _target_labels(constraints: PlanningScopeConstraints) -> set[str]:
766
+ labels: set[str] = set()
767
+ for item in constraints.target_roots:
768
+ labels.update(_path_text_parts(item.path))
769
+ return labels
770
+
771
+
772
+ def _task_text(task: dict[str, Any]) -> str:
773
+ acceptance = task.get("acceptance_criteria") or []
774
+ acceptance_items = acceptance if isinstance(acceptance, list) else []
775
+ return "\n".join(
776
+ [
777
+ str(task.get("title") or ""),
778
+ str(task.get("description") or ""),
779
+ *(str(item or "") for item in acceptance_items),
780
+ ]
781
+ )
782
+
783
+
784
+ def _scope_entry_requires_target(path: str) -> bool:
785
+ normalized = _constraint_root(path)
786
+ if not normalized:
787
+ return False
788
+ if _NON_TARGET_SURFACE_RE.match(normalized):
789
+ return False
790
+ if is_code_implementation_path(normalized) or is_test_path(normalized):
791
+ return True
792
+ pure = PurePosixPath(normalized)
793
+ name = pure.name.casefold()
794
+ if name in {"package.json", "pyproject.toml", "cargo.toml", "go.mod", "pom.xml"}:
795
+ return True
796
+ if _has_glob(normalized):
797
+ return not any(part.casefold() in {"docs", "doc", "notes"} for part in pure.parts)
798
+ if "/" in normalized and "." not in pure.name:
799
+ return True
800
+ return False
801
+
802
+
803
+ def _has_explicit_target_evidence(
804
+ *,
805
+ path: str,
806
+ task_text: str,
807
+ constraints: PlanningScopeConstraints,
808
+ ) -> bool:
809
+ text = str(task_text or "")
810
+ text_cf = text.casefold()
811
+ labels = _target_labels(constraints)
812
+ path_parts = _path_text_parts(path)
813
+ label_match = bool(labels & path_parts) or any(label and label in text_cf for label in labels)
814
+ if is_test_path(path):
815
+ return label_match and _TEST_EVIDENCE_RE.search(text) is not None
816
+ if is_code_implementation_path(path) or "/" in _constraint_root(path):
817
+ return label_match and _SHARED_EVIDENCE_RE.search(text) is not None
818
+ return label_match
819
+
820
+
821
+ def task_scope_constraint_violations(
822
+ task: dict[str, Any],
823
+ constraints: PlanningScopeConstraints,
824
+ ) -> list[PlanningScopeViolation]:
825
+ if not constraints.has_constraints:
826
+ return []
827
+ estimated_files, _dropped_estimated = split_normalized_repo_path_list(
828
+ task.get("estimated_files")
829
+ )
830
+ write_scope, _dropped_write_scope = split_normalized_repo_path_list(task.get("write_scope"))
831
+ paths = list(dict.fromkeys([*estimated_files, *write_scope]))
832
+ if not paths:
833
+ return []
834
+ text = _task_text(task)
835
+ violations: list[PlanningScopeViolation] = []
836
+ for path in paths:
837
+ for blocked in constraints.blocked_roots:
838
+ if not path_matches_planning_root(path, blocked.path):
839
+ continue
840
+ violations.append(
841
+ PlanningScopeViolation(
842
+ path=path,
843
+ classification=blocked.kind,
844
+ reason_code=blocked.reason_code,
845
+ evidence=blocked.evidence,
846
+ constraint_path=blocked.path,
847
+ )
848
+ )
849
+ break
850
+ else:
851
+ if (
852
+ constraints.target_roots
853
+ and _scope_entry_requires_target(path)
854
+ and not path_within_target_roots(path, constraints)
855
+ and not _has_explicit_target_evidence(
856
+ path=path,
857
+ task_text=text,
858
+ constraints=constraints,
859
+ )
860
+ ):
861
+ violations.append(
862
+ PlanningScopeViolation(
863
+ path=path,
864
+ classification="outside_target_root",
865
+ reason_code="invalid_out_of_scope",
866
+ evidence=(
867
+ "target roots: "
868
+ + ", ".join(item.path for item in constraints.target_roots)
869
+ ),
870
+ )
871
+ )
872
+ return violations
873
+
874
+
875
+ def task_has_target_root_scope(
876
+ task: dict[str, Any],
877
+ constraints: PlanningScopeConstraints,
878
+ ) -> bool:
879
+ if not constraints.target_roots:
880
+ return False
881
+ estimated_files, _dropped_estimated = split_normalized_repo_path_list(
882
+ task.get("estimated_files")
883
+ )
884
+ write_scope, _dropped_write_scope = split_normalized_repo_path_list(task.get("write_scope"))
885
+ return any(
886
+ path_within_target_roots(path, constraints) for path in [*estimated_files, *write_scope]
887
+ )
888
+
889
+
890
+ def filter_scope_entries_for_planning_constraints(
891
+ paths: list[str],
892
+ *,
893
+ task: dict[str, Any],
894
+ constraints: PlanningScopeConstraints,
895
+ ) -> tuple[list[str], list[PlanningScopeViolation]]:
896
+ if not constraints.has_constraints:
897
+ return paths, []
898
+ text_task = dict(task)
899
+ text_task["estimated_files"] = list(paths)
900
+ text_task["write_scope"] = list(paths)
901
+ violations = task_scope_constraint_violations(text_task, constraints)
902
+ violating = {_constraint_identity(violation.path) for violation in violations}
903
+ kept = [path for path in paths if _constraint_identity(path) not in violating]
904
+ return kept, violations