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,1228 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from pathlib import Path, PurePosixPath
6
+ from typing import Any
7
+
8
+ from .direction_change import detect_direction_change, filter_obsolete_direction_paths
9
+ from .file_classification import (
10
+ CODE_SCAN_SKIP_DIR_NAMES,
11
+ is_code_implementation_path,
12
+ is_symbol_scannable_path,
13
+ symbol_definition_regex,
14
+ )
15
+ from .planning_constraints import (
16
+ filter_scope_entries_for_planning_constraints,
17
+ planning_constraints_from_plan,
18
+ update_plan_planning_constraints,
19
+ )
20
+ from .swarm_scheduler import canonical_task_status
21
+ from .task_readiness import (
22
+ TASK_KIND_ANALYSIS_ONLY,
23
+ classify_task_lifecycle,
24
+ has_runnable_local_file_scope,
25
+ task_readiness_warning,
26
+ task_requires_runnable_file_scope,
27
+ )
28
+ from .task_scope import (
29
+ extract_forbidden_repo_path_hints,
30
+ extract_repo_path_hints,
31
+ is_internal_alysis_path,
32
+ scope_path_matches_pattern,
33
+ split_normalized_repo_path_list,
34
+ )
35
+
36
+ _GLOB_CHARS = ("*", "?")
37
+ _TASK_ID_HINT_RE = re.compile(r"\bT\d+\b", re.IGNORECASE)
38
+ _NON_EXECUTABLE_OBSOLETE_STATUSES = frozenset({"superseded", "invalidated"})
39
+ _FRAMEWORK_DYNAMIC_ROUTE_SEGMENT_RE = re.compile(
40
+ r"^(?:\[\[?\.\.\.[A-Za-z0-9_-]+\]?\]|\[[A-Za-z0-9_-]+\]|\([A-Za-z0-9_.-]+\))$"
41
+ )
42
+ _SYMBOL_CANDIDATE_RE = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]{2,}\b")
43
+ _SYMBOL_CANDIDATE_STOPWORDS = frozenset(
44
+ {
45
+ "add",
46
+ "and",
47
+ "blank",
48
+ "blanks",
49
+ "bug",
50
+ "class",
51
+ "code",
52
+ "def",
53
+ "description",
54
+ "file",
55
+ "fix",
56
+ "for",
57
+ "from",
58
+ "function",
59
+ "implement",
60
+ "module",
61
+ "old",
62
+ "output",
63
+ "return",
64
+ "test",
65
+ "tests",
66
+ "the",
67
+ "update",
68
+ "when",
69
+ "with",
70
+ }
71
+ )
72
+ _FILENAME_TOKEN_RE = re.compile(r"\b[a-z][a-z0-9_]{2,}\b", re.IGNORECASE)
73
+ _EXPLICIT_MISSING_PATH_ACTION_RE = re.compile(
74
+ r"\b(?:add|create|creating|generate|generating|introduce|new|scaffold|write|writing)\b",
75
+ re.IGNORECASE,
76
+ )
77
+ _MAX_SYMBOL_SCAN_FILES = 1500
78
+ _MAX_SYMBOL_SCAN_BYTES = 512_000
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class PlanReconciliationResult:
83
+ changed: bool
84
+ warnings: list[str]
85
+ task_updates: dict[str, dict[str, Any]]
86
+
87
+ @property
88
+ def updated_task_ids(self) -> list[str]:
89
+ return list(self.task_updates)
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class _PathAnchorGroup:
94
+ task_ids: tuple[str, ...]
95
+ paths: tuple[str, ...]
96
+
97
+
98
+ def summarize_plan_reconciliation(result: PlanReconciliationResult) -> str:
99
+ if result.task_updates:
100
+ return "reconciled tasks: " + ", ".join(result.updated_task_ids)
101
+ if result.warnings:
102
+ return f"reconciliation warnings: {len(result.warnings)}"
103
+ return "reconciliation made no changes"
104
+
105
+
106
+ def reconcile_plan_with_workspace(
107
+ plan: dict[str, Any],
108
+ *,
109
+ workspace_root: Path,
110
+ workspace_context: dict[str, Any] | None = None,
111
+ user_text: str | None = None,
112
+ transcript_tail: list[dict[str, Any]] | None = None,
113
+ target_task_ids: set[str] | None = None,
114
+ ) -> PlanReconciliationResult:
115
+ tasks_raw = plan.get("tasks")
116
+ if not isinstance(tasks_raw, list):
117
+ return PlanReconciliationResult(
118
+ changed=False,
119
+ warnings=["Plan reconciliation skipped: tasks field is missing or invalid."],
120
+ task_updates={},
121
+ )
122
+
123
+ root = workspace_root.expanduser().resolve()
124
+ known_paths = _known_workspace_paths(workspace_context)
125
+ greenfield = _workspace_context_is_greenfield(workspace_context)
126
+ normalized_target_task_ids = (
127
+ {str(task_id).strip() for task_id in target_task_ids if str(task_id).strip()}
128
+ if target_task_ids is not None
129
+ else None
130
+ )
131
+ anchor_groups = _latest_path_anchor_groups(
132
+ transcript_tail=transcript_tail,
133
+ user_text=user_text,
134
+ )
135
+ changed = False
136
+ warnings: list[str] = []
137
+ task_updates: dict[str, dict[str, Any]] = {}
138
+ planning_constraints = None
139
+ constraints_changed = False
140
+ candidate_constraint_messages = _candidate_user_messages(
141
+ transcript_tail=transcript_tail,
142
+ user_text=user_text,
143
+ )[:4]
144
+ for constraint_text in reversed(candidate_constraint_messages):
145
+ planning_constraints, message_changed = update_plan_planning_constraints(
146
+ plan,
147
+ text=constraint_text,
148
+ workspace_context=workspace_context,
149
+ direction_change=detect_direction_change(constraint_text),
150
+ )
151
+ constraints_changed = constraints_changed or message_changed
152
+ if planning_constraints is None:
153
+ planning_constraints = planning_constraints_from_plan(plan)
154
+ if constraints_changed:
155
+ changed = True
156
+ warnings.append("Recorded planning scope constraints from latest user direction.")
157
+
158
+ task_entries = [(index, task) for index, task in enumerate(tasks_raw) if isinstance(task, dict)]
159
+ task_count = len(task_entries)
160
+
161
+ for task_position, (index, task) in enumerate(task_entries):
162
+ task_id = _task_label(task=task, index=index)
163
+ if normalized_target_task_ids is not None and task_id not in normalized_target_task_ids:
164
+ continue
165
+ if (
166
+ canonical_task_status(str(task.get("status") or ""))
167
+ in _NON_EXECUTABLE_OBSOLETE_STATUSES
168
+ ):
169
+ continue
170
+ current_estimated = _string_list(task.get("estimated_files"))
171
+ current_write_scope = _string_list(task.get("write_scope"))
172
+ allow_create_targets = _task_allows_create_targets(task=task, greenfield=greenfield)
173
+ forbidden_path_identities = _task_forbidden_path_identities(
174
+ task=task,
175
+ user_text=user_text,
176
+ )
177
+ task_explicit_path_hints = [
178
+ path
179
+ for path in _task_explicit_path_hints(
180
+ task=task,
181
+ latest_user_text=str(user_text or ""),
182
+ )
183
+ if _path_identity_key(path) not in forbidden_path_identities
184
+ ]
185
+ task_anchor_paths = [
186
+ path
187
+ for path in _select_task_anchor_paths(
188
+ task=task,
189
+ task_position=task_position,
190
+ task_count=task_count,
191
+ anchor_groups=anchor_groups,
192
+ )
193
+ if _path_identity_key(path) not in forbidden_path_identities
194
+ ]
195
+ task_anchor_set = set(task_anchor_paths)
196
+ protected_symbol_remap_path_identities = _missing_path_identities(
197
+ [*task_explicit_path_hints, *task_anchor_paths],
198
+ workspace_root=root,
199
+ known_paths=known_paths,
200
+ )
201
+
202
+ normalized_estimated, dropped_estimated = split_normalized_repo_path_list(
203
+ task.get("estimated_files"),
204
+ root=root,
205
+ )
206
+ normalized_write_scope, dropped_write_scope = split_normalized_repo_path_list(
207
+ task.get("write_scope"),
208
+ root=root,
209
+ )
210
+
211
+ if dropped_estimated:
212
+ warnings.append(
213
+ f"Task {task_id}: dropped invalid estimated_files entries: "
214
+ + ", ".join(dropped_estimated)
215
+ )
216
+ if dropped_write_scope:
217
+ warnings.append(
218
+ f"Task {task_id}: dropped invalid write_scope entries: "
219
+ + ", ".join(dropped_write_scope)
220
+ )
221
+
222
+ estimated_files, dropped_internal_estimated = _drop_protected_paths(normalized_estimated)
223
+ write_scope, dropped_internal_write_scope = _drop_protected_paths(normalized_write_scope)
224
+ if dropped_internal_estimated:
225
+ warnings.append(
226
+ f"Task {task_id}: dropped protected estimated_files entries: "
227
+ + ", ".join(dropped_internal_estimated)
228
+ )
229
+ if dropped_internal_write_scope:
230
+ warnings.append(
231
+ f"Task {task_id}: dropped protected write_scope entries: "
232
+ + ", ".join(dropped_internal_write_scope)
233
+ )
234
+ estimated_files, dropped_forbidden_estimated = _drop_forbidden_paths(
235
+ estimated_files,
236
+ forbidden_path_identities,
237
+ )
238
+ if dropped_forbidden_estimated:
239
+ warnings.append(
240
+ f"Task {task_id}: dropped forbidden estimated_files entries: "
241
+ + ", ".join(dropped_forbidden_estimated)
242
+ )
243
+ write_scope, dropped_forbidden_write_scope = _drop_forbidden_paths(
244
+ write_scope,
245
+ forbidden_path_identities,
246
+ )
247
+ if dropped_forbidden_write_scope:
248
+ warnings.append(
249
+ f"Task {task_id}: dropped forbidden write_scope entries: "
250
+ + ", ".join(dropped_forbidden_write_scope)
251
+ )
252
+
253
+ if not estimated_files:
254
+ inferred_paths, _ = _infer_estimated_files(
255
+ task=task,
256
+ workspace_root=root,
257
+ known_paths=known_paths,
258
+ allowed_missing_paths=task_anchor_set,
259
+ latest_user_text=str(user_text or ""),
260
+ )
261
+ if inferred_paths:
262
+ estimated_files = inferred_paths
263
+ warnings.append(
264
+ f"Task {task_id}: inferred estimated_files from task text: "
265
+ + ", ".join(inferred_paths)
266
+ )
267
+
268
+ estimated_files, dropped_ungrounded_estimated = _drop_ungrounded_suspicious_paths(
269
+ paths=estimated_files,
270
+ workspace_root=root,
271
+ known_paths=known_paths,
272
+ allowed_missing_paths=task_anchor_set,
273
+ )
274
+ if dropped_ungrounded_estimated:
275
+ warnings.append(
276
+ f"Task {task_id}: dropped suspicious estimated_files entries not grounded in the "
277
+ "latest user request: " + ", ".join(dropped_ungrounded_estimated)
278
+ )
279
+ if not estimated_files:
280
+ inferred_paths, _ = _infer_estimated_files(
281
+ task=task,
282
+ workspace_root=root,
283
+ known_paths=known_paths,
284
+ allowed_missing_paths=task_anchor_set,
285
+ latest_user_text=str(user_text or ""),
286
+ )
287
+ if inferred_paths:
288
+ estimated_files = inferred_paths
289
+ warnings.append(
290
+ f"Task {task_id}: restored grounded estimated_files from task text: "
291
+ + ", ".join(inferred_paths)
292
+ )
293
+ explicit_task_paths, _ = _infer_estimated_files(
294
+ task=task,
295
+ workspace_root=root,
296
+ known_paths=known_paths,
297
+ allowed_missing_paths=task_anchor_set,
298
+ latest_user_text=str(user_text or ""),
299
+ )
300
+ added_explicit_estimated = [
301
+ path for path in explicit_task_paths if path not in estimated_files
302
+ ]
303
+ if added_explicit_estimated:
304
+ estimated_files = _dedupe_keep_order([*estimated_files, *added_explicit_estimated])
305
+ warnings.append(
306
+ f"Task {task_id}: added explicit task path hints to estimated_files: "
307
+ + ", ".join(added_explicit_estimated)
308
+ )
309
+
310
+ estimated_files, estimated_warning = _apply_task_anchor_paths(
311
+ current_paths=estimated_files,
312
+ anchor_paths=task_anchor_paths,
313
+ field_name="estimated_files",
314
+ )
315
+ if estimated_warning:
316
+ warnings.append(f"Task {task_id}: {estimated_warning}")
317
+ estimated_files, dropped_forbidden_estimated = _drop_forbidden_paths(
318
+ estimated_files,
319
+ forbidden_path_identities,
320
+ )
321
+ if dropped_forbidden_estimated:
322
+ warnings.append(
323
+ f"Task {task_id}: dropped forbidden estimated_files entries: "
324
+ + ", ".join(dropped_forbidden_estimated)
325
+ )
326
+
327
+ for path in estimated_files:
328
+ if path in task_anchor_set:
329
+ continue
330
+ if not allow_create_targets and _is_suspicious_path(
331
+ path=path,
332
+ workspace_root=root,
333
+ known_paths=known_paths,
334
+ ):
335
+ warnings.append(
336
+ f"Task {task_id}: estimated_files entry may be suspicious or missing: {path}"
337
+ )
338
+
339
+ if not write_scope and estimated_files:
340
+ write_scope = list(estimated_files)
341
+ warnings.append(
342
+ f"Task {task_id}: seeded write_scope from estimated_files: "
343
+ + ", ".join(estimated_files)
344
+ )
345
+
346
+ write_scope, dropped_ungrounded_write_scope = _drop_ungrounded_suspicious_paths(
347
+ paths=write_scope,
348
+ workspace_root=root,
349
+ known_paths=known_paths,
350
+ allowed_missing_paths=task_anchor_set,
351
+ )
352
+ if dropped_ungrounded_write_scope:
353
+ warnings.append(
354
+ f"Task {task_id}: dropped suspicious write_scope entries not grounded in the "
355
+ "latest user request: " + ", ".join(dropped_ungrounded_write_scope)
356
+ )
357
+ if not write_scope and estimated_files:
358
+ write_scope = list(estimated_files)
359
+ warnings.append(
360
+ f"Task {task_id}: reseeded write_scope from grounded estimated_files: "
361
+ + ", ".join(estimated_files)
362
+ )
363
+ added_explicit_write_scope = [
364
+ path
365
+ for path in explicit_task_paths
366
+ if path in estimated_files and path not in write_scope
367
+ ]
368
+ if added_explicit_write_scope:
369
+ write_scope = _dedupe_keep_order([*write_scope, *added_explicit_write_scope])
370
+ warnings.append(
371
+ f"Task {task_id}: added explicit task path hints to write_scope: "
372
+ + ", ".join(added_explicit_write_scope)
373
+ )
374
+
375
+ write_scope, write_scope_warning = _apply_task_anchor_paths(
376
+ current_paths=write_scope,
377
+ anchor_paths=task_anchor_paths,
378
+ field_name="write_scope",
379
+ )
380
+ if write_scope_warning:
381
+ warnings.append(f"Task {task_id}: {write_scope_warning}")
382
+ write_scope, dropped_forbidden_write_scope = _drop_forbidden_paths(
383
+ write_scope,
384
+ forbidden_path_identities,
385
+ )
386
+ if dropped_forbidden_write_scope:
387
+ warnings.append(
388
+ f"Task {task_id}: dropped forbidden write_scope entries: "
389
+ + ", ".join(dropped_forbidden_write_scope)
390
+ )
391
+
392
+ if not task_anchor_paths:
393
+ symbol_paths = _resolve_symbol_grounded_paths(
394
+ task=task,
395
+ workspace_root=root,
396
+ latest_user_text=str(user_text or ""),
397
+ )
398
+ grounded_paths = symbol_paths or _resolve_named_code_file_paths(
399
+ task=task,
400
+ workspace_root=root,
401
+ latest_user_text=str(user_text or ""),
402
+ )
403
+ if grounded_paths:
404
+ estimated_files, estimated_symbol_warning = _apply_symbol_grounded_paths(
405
+ current_paths=estimated_files,
406
+ symbol_paths=grounded_paths,
407
+ workspace_root=root,
408
+ task=task,
409
+ field_name="estimated_files",
410
+ protected_missing_path_identities=protected_symbol_remap_path_identities,
411
+ )
412
+ if estimated_symbol_warning:
413
+ warnings.append(f"Task {task_id}: {estimated_symbol_warning}")
414
+ write_scope, write_symbol_warning = _apply_symbol_grounded_paths(
415
+ current_paths=write_scope,
416
+ symbol_paths=grounded_paths,
417
+ workspace_root=root,
418
+ task=task,
419
+ field_name="write_scope",
420
+ protected_missing_path_identities=protected_symbol_remap_path_identities,
421
+ )
422
+ if write_symbol_warning:
423
+ warnings.append(f"Task {task_id}: {write_symbol_warning}")
424
+
425
+ estimated_files, estimated_constraint_violations = (
426
+ filter_scope_entries_for_planning_constraints(
427
+ estimated_files,
428
+ task={
429
+ **task,
430
+ "estimated_files": estimated_files,
431
+ "write_scope": estimated_files,
432
+ },
433
+ constraints=planning_constraints,
434
+ )
435
+ )
436
+ if estimated_constraint_violations:
437
+ warnings.append(
438
+ f"Task {task_id}: dropped estimated_files outside planning constraints: "
439
+ + ", ".join(
440
+ f"{item.path} ({item.classification}; {item.reason_code})"
441
+ for item in estimated_constraint_violations[:6]
442
+ )
443
+ )
444
+ write_scope, write_constraint_violations = filter_scope_entries_for_planning_constraints(
445
+ write_scope,
446
+ task={**task, "estimated_files": write_scope, "write_scope": write_scope},
447
+ constraints=planning_constraints,
448
+ )
449
+ if write_constraint_violations:
450
+ warnings.append(
451
+ f"Task {task_id}: dropped write_scope outside planning constraints: "
452
+ + ", ".join(
453
+ f"{item.path} ({item.classification}; {item.reason_code})"
454
+ for item in write_constraint_violations[:6]
455
+ )
456
+ )
457
+
458
+ for path in write_scope:
459
+ if path in task_anchor_set:
460
+ continue
461
+ if not allow_create_targets and _is_suspicious_path(
462
+ path=path,
463
+ workspace_root=root,
464
+ known_paths=known_paths,
465
+ ):
466
+ warnings.append(
467
+ f"Task {task_id}: write_scope entry may be suspicious or missing: {path}"
468
+ )
469
+
470
+ lifecycle = classify_task_lifecycle(
471
+ title=str(task.get("title") or "").strip(),
472
+ description=str(task.get("description") or "").strip(),
473
+ acceptance_criteria=_string_list(task.get("acceptance_criteria")),
474
+ estimated_files=estimated_files,
475
+ write_scope=write_scope,
476
+ explicit_analysis_only=True if task.get("analysis_only") is True else None,
477
+ )
478
+ if lifecycle.kind == TASK_KIND_ANALYSIS_ONLY:
479
+ if estimated_files or write_scope:
480
+ warnings.append(
481
+ f"Task {task_id}: cleared file mutation scope for analysis-only/report-only task"
482
+ )
483
+ estimated_files = []
484
+ write_scope = []
485
+
486
+ if task_requires_runnable_file_scope(
487
+ title=str(task.get("title") or "").strip(),
488
+ description=str(task.get("description") or "").strip(),
489
+ acceptance_criteria=_string_list(task.get("acceptance_criteria")),
490
+ estimated_files=estimated_files,
491
+ write_scope=write_scope,
492
+ ) and not has_runnable_local_file_scope(
493
+ estimated_files=estimated_files,
494
+ write_scope=write_scope,
495
+ ):
496
+ warnings.append(
497
+ task_readiness_warning(
498
+ task_id=task_id,
499
+ title=str(task.get("title") or "").strip(),
500
+ )
501
+ )
502
+
503
+ patch: dict[str, Any] = {}
504
+ if estimated_files != current_estimated:
505
+ task["estimated_files"] = estimated_files
506
+ patch["estimated_files"] = estimated_files
507
+ if write_scope != current_write_scope:
508
+ task["write_scope"] = write_scope
509
+ patch["write_scope"] = write_scope
510
+ scope_changed = "estimated_files" in patch or "write_scope" in patch
511
+ should_persist_lifecycle = (
512
+ scope_changed
513
+ or "task_kind" in task
514
+ or "task_kind_reason" in task
515
+ or lifecycle.kind == TASK_KIND_ANALYSIS_ONLY
516
+ )
517
+ if should_persist_lifecycle and task.get("task_kind") != lifecycle.kind:
518
+ task["task_kind"] = lifecycle.kind
519
+ patch["task_kind"] = lifecycle.kind
520
+ if should_persist_lifecycle and task.get("task_kind_reason") != lifecycle.reason_code:
521
+ task["task_kind_reason"] = lifecycle.reason_code
522
+ patch["task_kind_reason"] = lifecycle.reason_code
523
+ if lifecycle.kind == TASK_KIND_ANALYSIS_ONLY and task.get("analysis_only") is not True:
524
+ task["analysis_only"] = True
525
+ patch["analysis_only"] = True
526
+ elif (
527
+ lifecycle.kind != TASK_KIND_ANALYSIS_ONLY
528
+ and task.get("analysis_only") is True
529
+ and task.get("task_kind") != TASK_KIND_ANALYSIS_ONLY
530
+ ):
531
+ task.pop("analysis_only", None)
532
+ patch["analysis_only"] = None
533
+ if patch:
534
+ task_updates[task_id] = patch
535
+ changed = True
536
+
537
+ return PlanReconciliationResult(
538
+ changed=changed,
539
+ warnings=_dedupe_keep_order(warnings),
540
+ task_updates=task_updates,
541
+ )
542
+
543
+
544
+ def _task_label(*, task: dict[str, Any], index: int) -> str:
545
+ task_id = str(task.get("id") or "").strip()
546
+ if task_id:
547
+ return task_id
548
+ title = str(task.get("title") or "").strip()
549
+ if title:
550
+ return title
551
+ return f"task[{index}]"
552
+
553
+
554
+ def _string_list(value: Any) -> list[str]:
555
+ if not isinstance(value, list):
556
+ return []
557
+ out: list[str] = []
558
+ for item in value:
559
+ text = str(item).strip()
560
+ if text:
561
+ out.append(text)
562
+ return out
563
+
564
+
565
+ def _dedupe_keep_order(values: list[str]) -> list[str]:
566
+ seen: set[str] = set()
567
+ out: list[str] = []
568
+ for value in values:
569
+ key = value.casefold()
570
+ if key in seen:
571
+ continue
572
+ seen.add(key)
573
+ out.append(value)
574
+ return out
575
+
576
+
577
+ def _path_identity_key(value: str) -> str:
578
+ normalized = str(value or "").strip().replace("\\", "/").rstrip("/")
579
+ if normalized.casefold() in {"readme", "readme.md"}:
580
+ return "__readme_alias__"
581
+ return normalized.casefold()
582
+
583
+
584
+ def _drop_forbidden_paths(
585
+ paths: list[str], forbidden_paths: set[str]
586
+ ) -> tuple[list[str], list[str]]:
587
+ if not forbidden_paths:
588
+ return _dedupe_keep_order(paths), []
589
+ kept: list[str] = []
590
+ dropped: list[str] = []
591
+ for path in _dedupe_keep_order(paths):
592
+ if _path_identity_key(path) in forbidden_paths:
593
+ dropped.append(path)
594
+ continue
595
+ kept.append(path)
596
+ return kept, dropped
597
+
598
+
599
+ def _task_forbidden_path_identities(*, task: dict[str, Any], user_text: str | None) -> set[str]:
600
+ acceptance = _string_list(task.get("acceptance_criteria"))
601
+ text = "\n".join(
602
+ [
603
+ str(task.get("title") or ""),
604
+ str(task.get("description") or ""),
605
+ *acceptance,
606
+ str(user_text or ""),
607
+ ]
608
+ )
609
+ return {_path_identity_key(path) for path in extract_forbidden_repo_path_hints(text)}
610
+
611
+
612
+ def _task_explicit_path_hints(
613
+ *,
614
+ task: dict[str, Any],
615
+ latest_user_text: str = "",
616
+ ) -> list[str]:
617
+ task_text = "\n".join(
618
+ [
619
+ str(task.get("title") or ""),
620
+ str(task.get("description") or ""),
621
+ *[str(item) for item in _string_list(task.get("acceptance_criteria"))],
622
+ ]
623
+ )
624
+ hints, _obsolete_hints = filter_obsolete_direction_paths(
625
+ extract_repo_path_hints(task_text),
626
+ latest_user_text=latest_user_text,
627
+ task_text=task_text,
628
+ )
629
+ out: list[str] = []
630
+ for hint in hints:
631
+ if _has_glob(hint):
632
+ continue
633
+ if is_internal_alysis_path(hint) or hint == ".git" or hint.startswith(".git/"):
634
+ continue
635
+ out.append(hint)
636
+ return _dedupe_keep_order(out)
637
+
638
+
639
+ def _missing_path_identities(
640
+ paths: list[str],
641
+ *,
642
+ workspace_root: Path,
643
+ known_paths: set[str],
644
+ ) -> set[str]:
645
+ identities: set[str] = set()
646
+ for path in _dedupe_keep_order(paths):
647
+ if _has_glob(path) or path in known_paths:
648
+ continue
649
+ try:
650
+ exists = (workspace_root / path).resolve().exists()
651
+ except OSError:
652
+ exists = False
653
+ if exists:
654
+ continue
655
+ identities.add(_path_identity_key(path))
656
+ return identities
657
+
658
+
659
+ def _candidate_user_messages(
660
+ *,
661
+ transcript_tail: list[dict[str, Any]] | None,
662
+ user_text: str | None,
663
+ ) -> list[str]:
664
+ messages: list[str] = []
665
+ latest = str(user_text or "").strip()
666
+ if latest:
667
+ messages.append(latest)
668
+ for item in reversed(transcript_tail or []):
669
+ if not isinstance(item, dict):
670
+ continue
671
+ role = str(item.get("role") or "").strip().lower()
672
+ if role != "user":
673
+ continue
674
+ content = str(item.get("content") or "").strip()
675
+ if not content:
676
+ continue
677
+ if messages and content == messages[-1]:
678
+ continue
679
+ messages.append(content)
680
+ return messages
681
+
682
+
683
+ def _extract_task_ids(text: str) -> tuple[str, ...]:
684
+ seen: set[str] = set()
685
+ ids: list[str] = []
686
+ for match in _TASK_ID_HINT_RE.findall(text or ""):
687
+ task_id = match.upper()
688
+ if task_id in seen:
689
+ continue
690
+ seen.add(task_id)
691
+ ids.append(task_id)
692
+ return tuple(ids)
693
+
694
+
695
+ def _extract_path_anchor_groups(text: str) -> list[_PathAnchorGroup]:
696
+ groups: list[_PathAnchorGroup] = []
697
+ for raw_line in (text or "").splitlines():
698
+ line = raw_line.strip()
699
+ if not line:
700
+ continue
701
+ forbidden = {_path_identity_key(path) for path in extract_forbidden_repo_path_hints(line)}
702
+ paths = tuple(
703
+ path
704
+ for path in filter_obsolete_direction_paths(
705
+ extract_repo_path_hints(line), latest_user_text=text, task_text=line
706
+ )[0]
707
+ if _path_identity_key(path) not in forbidden
708
+ )
709
+ if not paths:
710
+ continue
711
+ groups.append(
712
+ _PathAnchorGroup(
713
+ task_ids=_extract_task_ids(line),
714
+ paths=paths,
715
+ )
716
+ )
717
+ if groups:
718
+ return groups
719
+ forbidden = {_path_identity_key(path) for path in extract_forbidden_repo_path_hints(text or "")}
720
+ global_paths = tuple(
721
+ path
722
+ for path in filter_obsolete_direction_paths(
723
+ extract_repo_path_hints(text or ""), latest_user_text=text, task_text=text
724
+ )[0]
725
+ if _path_identity_key(path) not in forbidden
726
+ )
727
+ if not global_paths:
728
+ return []
729
+ return [_PathAnchorGroup(task_ids=(), paths=global_paths)]
730
+
731
+
732
+ def _latest_path_anchor_groups(
733
+ *,
734
+ transcript_tail: list[dict[str, Any]] | None,
735
+ user_text: str | None,
736
+ ) -> list[_PathAnchorGroup]:
737
+ for message in _candidate_user_messages(
738
+ transcript_tail=transcript_tail,
739
+ user_text=user_text,
740
+ ):
741
+ groups = _extract_path_anchor_groups(message)
742
+ if groups:
743
+ return groups
744
+ return []
745
+
746
+
747
+ def _select_task_anchor_paths(
748
+ *,
749
+ task: dict[str, Any],
750
+ task_position: int,
751
+ task_count: int,
752
+ anchor_groups: list[_PathAnchorGroup],
753
+ ) -> list[str]:
754
+ if not anchor_groups:
755
+ return []
756
+ task_id = str(task.get("id") or "").strip().upper()
757
+ if task_id:
758
+ matched = [
759
+ path for group in anchor_groups if task_id in group.task_ids for path in group.paths
760
+ ]
761
+ if matched:
762
+ return _dedupe_keep_order(matched)
763
+
764
+ if task_count == 1:
765
+ return _dedupe_keep_order([path for group in anchor_groups for path in group.paths])
766
+
767
+ anonymous_groups = [group for group in anchor_groups if not group.task_ids]
768
+ if len(anonymous_groups) == task_count:
769
+ return list(anonymous_groups[task_position].paths)
770
+ return []
771
+
772
+
773
+ def _drop_protected_paths(paths: list[str]) -> tuple[list[str], list[str]]:
774
+ kept: list[str] = []
775
+ dropped: list[str] = []
776
+ for path in paths:
777
+ if is_internal_alysis_path(path) or path == ".git" or path.startswith(".git/"):
778
+ dropped.append(path)
779
+ continue
780
+ kept.append(path)
781
+ return _dedupe_keep_order(kept), dropped
782
+
783
+
784
+ def _workspace_context_is_greenfield(workspace_context: dict[str, Any] | None) -> bool:
785
+ return bool(isinstance(workspace_context, dict) and workspace_context.get("greenfield"))
786
+
787
+
788
+ def _task_allows_create_targets(*, task: dict[str, Any], greenfield: bool) -> bool:
789
+ if greenfield:
790
+ return True
791
+ text = "\n".join(
792
+ [
793
+ str(task.get("title") or ""),
794
+ str(task.get("description") or ""),
795
+ *[str(item) for item in _string_list(task.get("acceptance_criteria"))],
796
+ ]
797
+ )
798
+ return bool(_EXPLICIT_MISSING_PATH_ACTION_RE.search(text))
799
+
800
+
801
+ def _apply_task_anchor_paths(
802
+ *,
803
+ current_paths: list[str],
804
+ anchor_paths: list[str],
805
+ field_name: str,
806
+ ) -> tuple[list[str], str | None]:
807
+ normalized_current = _dedupe_keep_order(current_paths)
808
+ normalized_anchor = _dedupe_keep_order(anchor_paths)
809
+ if not normalized_anchor:
810
+ return normalized_current, None
811
+ if not normalized_current:
812
+ return normalized_anchor, (
813
+ f"restored {field_name} from explicit user grounding: " + ", ".join(normalized_anchor)
814
+ )
815
+ if any(path in normalized_current for path in normalized_anchor):
816
+ missing = [path for path in normalized_anchor if path not in normalized_current]
817
+ if not missing:
818
+ return normalized_current, None
819
+ return normalized_current + missing, (
820
+ f"added explicit user-grounded {field_name} entries: " + ", ".join(missing)
821
+ )
822
+ return normalized_anchor, (
823
+ f"replaced {field_name} with explicit user-grounded paths: "
824
+ + ", ".join(normalized_current)
825
+ + " -> "
826
+ + ", ".join(normalized_anchor)
827
+ )
828
+
829
+
830
+ def _has_glob(path: str) -> bool:
831
+ return any(char in path for char in _GLOB_CHARS)
832
+
833
+
834
+ def _has_framework_dynamic_route_segment(path: str) -> bool:
835
+ try:
836
+ parts = PurePosixPath(path.replace("\\", "/")).parts
837
+ except ValueError:
838
+ return False
839
+ return any(_FRAMEWORK_DYNAMIC_ROUTE_SEGMENT_RE.fullmatch(part) for part in parts)
840
+
841
+
842
+ def _known_workspace_paths(workspace_context: dict[str, Any] | None) -> set[str]:
843
+ if not isinstance(workspace_context, dict):
844
+ return set()
845
+ known: set[str] = set()
846
+ for rel_path in _string_list(workspace_context.get("readme_paths")):
847
+ known.add(rel_path)
848
+ for rel_path in _string_list(workspace_context.get("observed_paths")):
849
+ known.add(rel_path)
850
+ conventions_path = str(workspace_context.get("conventions_path") or "").strip()
851
+ if conventions_path:
852
+ known.add(conventions_path)
853
+ for entry in workspace_context.get("manifests") or []:
854
+ if not isinstance(entry, dict):
855
+ continue
856
+ rel_path = str(entry.get("path") or "").strip()
857
+ if rel_path:
858
+ known.add(rel_path)
859
+ for entry in workspace_context.get("top_level_entries") or []:
860
+ if not isinstance(entry, dict):
861
+ continue
862
+ rel_path = str(entry.get("path") or "").strip()
863
+ if rel_path:
864
+ known.add(rel_path)
865
+ return known
866
+
867
+
868
+ def _is_suspicious_path(
869
+ *,
870
+ path: str,
871
+ workspace_root: Path,
872
+ known_paths: set[str],
873
+ ) -> bool:
874
+ if path in known_paths:
875
+ return False
876
+ if _has_glob(path) or _has_framework_dynamic_route_segment(path):
877
+ return False
878
+ candidate = (workspace_root / path).resolve()
879
+ return not candidate.exists() and not candidate.parent.exists()
880
+
881
+
882
+ def _explicit_missing_path_is_grounded_in_task_text(*, task_text: str, path: str) -> bool:
883
+ normalized_path = str(path or "").strip().replace("\\", "/")
884
+ if not normalized_path:
885
+ return False
886
+ tail = normalized_path.rsplit("/", 1)[-1]
887
+ if "." not in tail:
888
+ return False
889
+ for raw_line in (task_text or "").splitlines():
890
+ line = raw_line.strip()
891
+ if not line or normalized_path.casefold() not in line.casefold():
892
+ continue
893
+ if _EXPLICIT_MISSING_PATH_ACTION_RE.search(line):
894
+ return True
895
+ return False
896
+
897
+
898
+ def _infer_estimated_files(
899
+ *,
900
+ task: dict[str, Any],
901
+ workspace_root: Path,
902
+ known_paths: set[str],
903
+ allowed_missing_paths: set[str] | None = None,
904
+ latest_user_text: str = "",
905
+ ) -> tuple[list[str], list[str]]:
906
+ title = str(task.get("title") or "").strip()
907
+ description = str(task.get("description") or "").strip()
908
+ acceptance = _string_list(task.get("acceptance_criteria"))
909
+ task_text = "\n".join([title, description, *acceptance]).strip()
910
+ hints = extract_repo_path_hints(task_text)
911
+ forbidden_hints = {
912
+ _path_identity_key(path)
913
+ for path in extract_forbidden_repo_path_hints(
914
+ "\n".join([task_text, str(latest_user_text or "")])
915
+ )
916
+ }
917
+ hints, obsolete_hints = filter_obsolete_direction_paths(
918
+ hints,
919
+ latest_user_text=latest_user_text,
920
+ task_text=task_text,
921
+ )
922
+ allowed_missing = {path for path in (allowed_missing_paths or set()) if path}
923
+ inferred: list[str] = []
924
+ ignored: list[str] = list(obsolete_hints)
925
+ for hint in hints:
926
+ if _has_glob(hint):
927
+ continue
928
+ if _path_identity_key(hint) in forbidden_hints:
929
+ ignored.append(hint)
930
+ continue
931
+ if is_internal_alysis_path(hint) or hint == ".git" or hint.startswith(".git/"):
932
+ ignored.append(hint)
933
+ continue
934
+ if hint in allowed_missing:
935
+ inferred.append(hint)
936
+ continue
937
+ if _is_suspicious_path(path=hint, workspace_root=workspace_root, known_paths=known_paths):
938
+ if _explicit_missing_path_is_grounded_in_task_text(
939
+ task_text=task_text,
940
+ path=hint,
941
+ ):
942
+ inferred.append(hint)
943
+ continue
944
+ ignored.append(hint)
945
+ continue
946
+ inferred.append(hint)
947
+ return _dedupe_keep_order(inferred), _dedupe_keep_order(ignored)
948
+
949
+
950
+ def _task_grounding_text(task: dict[str, Any], *, latest_user_text: str = "") -> str:
951
+ user_text = str(latest_user_text or "").strip()
952
+ title = str(task.get("title") or "").strip()
953
+ description = str(task.get("description") or "").strip()
954
+ if (
955
+ user_text
956
+ and title == "Implement requested repository change"
957
+ and "Use local search/read tools to locate" in description
958
+ ):
959
+ return user_text
960
+ return "\n".join(
961
+ [
962
+ title,
963
+ description,
964
+ *[str(item) for item in _string_list(task.get("acceptance_criteria"))],
965
+ ]
966
+ )
967
+
968
+
969
+ def _task_symbol_candidates(
970
+ task: dict[str, Any],
971
+ *,
972
+ latest_user_text: str = "",
973
+ ) -> list[str]:
974
+ text = _task_grounding_text(task, latest_user_text=latest_user_text)
975
+ candidates: list[str] = []
976
+ for raw in _SYMBOL_CANDIDATE_RE.findall(text):
977
+ symbol = raw.strip()
978
+ if not symbol:
979
+ continue
980
+ lowered = symbol.casefold()
981
+ if lowered in _SYMBOL_CANDIDATE_STOPWORDS:
982
+ continue
983
+ if symbol.isupper() and "_" not in symbol:
984
+ continue
985
+ if "_" not in symbol and not any(char.isupper() for char in symbol[1:]):
986
+ continue
987
+ candidates.append(symbol)
988
+ return _dedupe_keep_order(candidates)[:6]
989
+
990
+
991
+ def _task_filename_candidates(
992
+ task: dict[str, Any],
993
+ *,
994
+ latest_user_text: str = "",
995
+ ) -> list[str]:
996
+ text = _task_grounding_text(task, latest_user_text=latest_user_text)
997
+ candidates: list[str] = []
998
+ for raw in _FILENAME_TOKEN_RE.findall(text):
999
+ token = raw.strip().casefold()
1000
+ if not token or token in _SYMBOL_CANDIDATE_STOPWORDS:
1001
+ continue
1002
+ candidates.append(token)
1003
+ return _dedupe_keep_order(candidates)[:10]
1004
+
1005
+
1006
+ def _iter_symbol_scan_files(workspace_root: Path) -> list[Path]:
1007
+ files: list[Path] = []
1008
+ try:
1009
+ iterator = workspace_root.rglob("*")
1010
+ except OSError:
1011
+ return files
1012
+ for path in iterator:
1013
+ if len(files) >= _MAX_SYMBOL_SCAN_FILES:
1014
+ break
1015
+ try:
1016
+ rel_parts = path.resolve().relative_to(workspace_root).parts
1017
+ except (OSError, ValueError):
1018
+ continue
1019
+ if any(part.casefold() in CODE_SCAN_SKIP_DIR_NAMES for part in rel_parts):
1020
+ continue
1021
+ if not path.is_file():
1022
+ continue
1023
+ rel_path = "/".join(part for part in rel_parts if part)
1024
+ if not is_symbol_scannable_path(rel_path):
1025
+ continue
1026
+ files.append(path)
1027
+ return files
1028
+
1029
+
1030
+ def _symbol_definition_regex(symbol: str, suffix: str) -> re.Pattern[str]:
1031
+ return symbol_definition_regex(symbol, suffix)
1032
+
1033
+
1034
+ def _file_text(path: Path) -> str:
1035
+ try:
1036
+ if path.stat().st_size > _MAX_SYMBOL_SCAN_BYTES:
1037
+ return ""
1038
+ return path.read_text(encoding="utf-8", errors="ignore")
1039
+ except OSError:
1040
+ return ""
1041
+
1042
+
1043
+ def _resolve_symbol_grounded_paths(
1044
+ *,
1045
+ task: dict[str, Any],
1046
+ workspace_root: Path,
1047
+ latest_user_text: str = "",
1048
+ ) -> list[str]:
1049
+ symbols = _task_symbol_candidates(task, latest_user_text=latest_user_text)
1050
+ if not symbols:
1051
+ return []
1052
+
1053
+ paths_by_symbol: dict[str, list[str]] = {symbol: [] for symbol in symbols}
1054
+ for path in _iter_symbol_scan_files(workspace_root):
1055
+ text = _file_text(path)
1056
+ if not text:
1057
+ continue
1058
+ suffix = path.suffix.casefold()
1059
+ for symbol in symbols:
1060
+ if not _symbol_definition_regex(symbol, suffix).search(text):
1061
+ continue
1062
+ try:
1063
+ rel_path = path.resolve().relative_to(workspace_root).as_posix()
1064
+ except (OSError, ValueError):
1065
+ continue
1066
+ paths_by_symbol[symbol].append(rel_path)
1067
+
1068
+ grounded: list[str] = []
1069
+ for symbol in symbols:
1070
+ symbol_paths = _dedupe_keep_order(paths_by_symbol.get(symbol, []))
1071
+ if len(symbol_paths) == 1:
1072
+ grounded.extend(symbol_paths)
1073
+ return _dedupe_keep_order(grounded)[:3]
1074
+
1075
+
1076
+ def _resolve_named_code_file_paths(
1077
+ *,
1078
+ task: dict[str, Any],
1079
+ workspace_root: Path,
1080
+ latest_user_text: str = "",
1081
+ ) -> list[str]:
1082
+ candidates = _task_filename_candidates(task, latest_user_text=latest_user_text)
1083
+ if not candidates:
1084
+ return []
1085
+
1086
+ paths_by_stem: dict[str, list[str]] = {candidate: [] for candidate in candidates}
1087
+ for path in _iter_symbol_scan_files(workspace_root):
1088
+ stem = path.stem.casefold()
1089
+ if stem not in paths_by_stem:
1090
+ continue
1091
+ try:
1092
+ rel_path = path.resolve().relative_to(workspace_root).as_posix()
1093
+ except (OSError, ValueError):
1094
+ continue
1095
+ if not is_code_implementation_path(rel_path):
1096
+ continue
1097
+ paths_by_stem[stem].append(rel_path)
1098
+
1099
+ grounded: list[str] = []
1100
+ for candidate in candidates:
1101
+ candidate_paths = _dedupe_keep_order(paths_by_stem.get(candidate, []))
1102
+ if len(candidate_paths) == 1:
1103
+ grounded.extend(candidate_paths)
1104
+ return _dedupe_keep_order(grounded)[:3]
1105
+
1106
+
1107
+ def _is_concrete_code_implementation_path(path: str) -> bool:
1108
+ return not _has_glob(path) and is_code_implementation_path(path)
1109
+
1110
+
1111
+ def _is_code_implementation_path(path: str) -> bool:
1112
+ return is_code_implementation_path(path)
1113
+
1114
+
1115
+ def _path_contains_any_task_symbol(
1116
+ *,
1117
+ workspace_root: Path,
1118
+ path: str,
1119
+ task: dict[str, Any],
1120
+ ) -> bool:
1121
+ symbols = _task_symbol_candidates(task)
1122
+ if not symbols:
1123
+ return False
1124
+ candidate = workspace_root / path
1125
+ text = _file_text(candidate)
1126
+ if not text:
1127
+ return False
1128
+ return any(re.search(rf"\b{re.escape(symbol)}\b", text) for symbol in symbols)
1129
+
1130
+
1131
+ def _apply_symbol_grounded_paths(
1132
+ *,
1133
+ current_paths: list[str],
1134
+ symbol_paths: list[str],
1135
+ workspace_root: Path,
1136
+ task: dict[str, Any],
1137
+ field_name: str,
1138
+ protected_missing_path_identities: set[str] | None = None,
1139
+ ) -> tuple[list[str], str | None]:
1140
+ normalized_current = _dedupe_keep_order(current_paths)
1141
+ normalized_symbol_paths = _dedupe_keep_order(symbol_paths)
1142
+ if not normalized_symbol_paths:
1143
+ return normalized_current, None
1144
+ if any(path in normalized_current for path in normalized_symbol_paths):
1145
+ return normalized_current, None
1146
+ if all(
1147
+ any(
1148
+ scope_path_matches_pattern(symbol_path, current_path, root=workspace_root)
1149
+ for current_path in normalized_current
1150
+ )
1151
+ for symbol_path in normalized_symbol_paths
1152
+ ):
1153
+ return normalized_current, None
1154
+
1155
+ implementation_paths = [
1156
+ path for path in normalized_current if _is_concrete_code_implementation_path(path)
1157
+ ]
1158
+ protected_identities = protected_missing_path_identities or set()
1159
+ protected_missing_paths = [
1160
+ path for path in implementation_paths if _path_identity_key(path) in protected_identities
1161
+ ]
1162
+ if protected_missing_paths:
1163
+ return normalized_current, (
1164
+ f"kept explicit missing {field_name} path(s) and did not retarget them to "
1165
+ "repository symbol definition path(s): "
1166
+ + ", ".join(protected_missing_paths)
1167
+ + " (candidate: "
1168
+ + ", ".join(normalized_symbol_paths)
1169
+ + ")"
1170
+ )
1171
+ if implementation_paths and any(
1172
+ _path_contains_any_task_symbol(
1173
+ workspace_root=workspace_root,
1174
+ path=path,
1175
+ task=task,
1176
+ )
1177
+ for path in implementation_paths
1178
+ ):
1179
+ return normalized_current, None
1180
+
1181
+ if implementation_paths:
1182
+ replacement: list[str] = []
1183
+ inserted_symbols = False
1184
+ for path in normalized_current:
1185
+ if _is_concrete_code_implementation_path(path):
1186
+ if not inserted_symbols:
1187
+ replacement.extend(normalized_symbol_paths)
1188
+ inserted_symbols = True
1189
+ continue
1190
+ replacement.append(path)
1191
+ replacement = _dedupe_keep_order(replacement)
1192
+ return replacement, (
1193
+ f"replaced symbol-mismatched {field_name} entries with repository symbol "
1194
+ "definition path(s): "
1195
+ + ", ".join(implementation_paths)
1196
+ + " -> "
1197
+ + ", ".join(normalized_symbol_paths)
1198
+ )
1199
+
1200
+ replacement = _dedupe_keep_order([*normalized_current, *normalized_symbol_paths])
1201
+ return replacement, (
1202
+ f"added repository symbol definition path(s) to {field_name}: "
1203
+ + ", ".join(normalized_symbol_paths)
1204
+ )
1205
+
1206
+
1207
+ def _drop_ungrounded_suspicious_paths(
1208
+ *,
1209
+ paths: list[str],
1210
+ workspace_root: Path,
1211
+ known_paths: set[str],
1212
+ allowed_missing_paths: set[str] | None = None,
1213
+ ) -> tuple[list[str], list[str]]:
1214
+ allowed_missing = {path for path in (allowed_missing_paths or set()) if path}
1215
+ if not allowed_missing:
1216
+ return _dedupe_keep_order(paths), []
1217
+
1218
+ kept: list[str] = []
1219
+ dropped: list[str] = []
1220
+ for path in _dedupe_keep_order(paths):
1221
+ if path in allowed_missing:
1222
+ kept.append(path)
1223
+ continue
1224
+ if _is_suspicious_path(path=path, workspace_root=workspace_root, known_paths=known_paths):
1225
+ dropped.append(path)
1226
+ continue
1227
+ kept.append(path)
1228
+ return kept, dropped