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,605 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ from dataclasses import dataclass
7
+ from datetime import UTC, datetime
8
+ from pathlib import Path
9
+ from types import SimpleNamespace
10
+ from typing import Any, Literal
11
+
12
+ from .forge import RunPaths
13
+ from .knowledge_base import (
14
+ KnowledgeIndexEntry,
15
+ derive_run_id_from_knowledge_file_path,
16
+ is_effectively_accepted_task_attempt,
17
+ is_effectively_open_status,
18
+ load_knowledge_index,
19
+ rebuild_knowledge_index,
20
+ )
21
+ from .task_scope import extract_repo_path_hints
22
+
23
+ KnowledgeConsumer = Literal["execution", "planner", "replanner"]
24
+
25
+ _STOPWORDS = {
26
+ "a",
27
+ "an",
28
+ "and",
29
+ "before",
30
+ "for",
31
+ "from",
32
+ "how",
33
+ "if",
34
+ "in",
35
+ "into",
36
+ "is",
37
+ "issue",
38
+ "issues",
39
+ "it",
40
+ "of",
41
+ "on",
42
+ "or",
43
+ "plan",
44
+ "planner",
45
+ "planning",
46
+ "replanner",
47
+ "request",
48
+ "requests",
49
+ "task",
50
+ "tasks",
51
+ "the",
52
+ "this",
53
+ "to",
54
+ "we",
55
+ "work",
56
+ "works",
57
+ "why",
58
+ }
59
+
60
+
61
+ def _now_iso() -> str:
62
+ return datetime.now(UTC).replace(microsecond=0).isoformat()
63
+
64
+
65
+ def _sanitize_component(value: str) -> str:
66
+ safe = "".join(c if c.isalnum() or c in {"-", "_"} else "_" for c in str(value).strip())
67
+ return safe or "item"
68
+
69
+
70
+ def _normalize_rel_path(value: str) -> str:
71
+ cleaned = str(value).strip().replace("\\", "/")
72
+ while cleaned.startswith("./"):
73
+ cleaned = cleaned[2:]
74
+ return cleaned.rstrip("/")
75
+
76
+
77
+ def _is_run_local_task_id_token(token: str) -> bool:
78
+ return bool(re.fullmatch(r"t\d+", token))
79
+
80
+
81
+ def _tokenize(text: str) -> set[str]:
82
+ tokens = {
83
+ token
84
+ for token in re.findall(r"[a-z0-9_./-]+", text.lower())
85
+ if len(token) >= 3 and token not in _STOPWORDS and not _is_run_local_task_id_token(token)
86
+ }
87
+ return tokens
88
+
89
+
90
+ def _task_paths(task: dict[str, Any], *, extra_paths: list[str] | None = None) -> set[str]:
91
+ values = [
92
+ *[str(path).strip() for path in task.get("write_scope") or []],
93
+ *[str(path).strip() for path in task.get("estimated_files") or []],
94
+ *[str(path).strip() for path in extra_paths or []],
95
+ ]
96
+ return {_normalize_rel_path(value) for value in values if _normalize_rel_path(value)}
97
+
98
+
99
+ def _task_keywords(task: dict[str, Any]) -> set[str]:
100
+ parts = [
101
+ str(task.get("title") or "").strip(),
102
+ str(task.get("description") or "").strip(),
103
+ *[str(item).strip() for item in task.get("acceptance_criteria") or []],
104
+ ]
105
+ return _tokenize(" ".join(parts))
106
+
107
+
108
+ def _task_id_hints(text: str) -> list[str]:
109
+ seen: list[str] = []
110
+ for raw in re.findall(r"\bT\d+\b", text.upper()):
111
+ if raw not in seen:
112
+ seen.append(raw)
113
+ return seen
114
+
115
+
116
+ def _path_value(candidate: Any) -> Path | None:
117
+ if candidate is None:
118
+ return None
119
+ try:
120
+ return Path(candidate).expanduser().resolve()
121
+ except TypeError:
122
+ return None
123
+
124
+
125
+ def _infer_workspace_root_from_artifact(candidate: Path | None) -> Path | None:
126
+ if candidate is None:
127
+ return None
128
+ probe = candidate if candidate.is_dir() else candidate.parent
129
+ for path in (probe, *probe.parents):
130
+ if path.name == ".alysis":
131
+ return path.parent.resolve()
132
+ return None
133
+
134
+
135
+ def resolve_knowledge_workspace_root(paths: Any) -> Path:
136
+ root = _path_value(getattr(paths, "root", None))
137
+ if root is not None:
138
+ return root
139
+ for attr in (
140
+ "plan_dir",
141
+ "plan_json_path",
142
+ "plan_md_path",
143
+ "planner_chat_path",
144
+ "planner_summary_path",
145
+ "notes_path",
146
+ ):
147
+ inferred = _infer_workspace_root_from_artifact(_path_value(getattr(paths, attr, None)))
148
+ if inferred is not None:
149
+ return inferred
150
+ return Path(".").resolve()
151
+
152
+
153
+ def _planner_plan_dir(paths: Any) -> Path:
154
+ plan_dir = _path_value(getattr(paths, "plan_dir", None))
155
+ if plan_dir is not None:
156
+ return plan_dir
157
+ plan_json_path = _path_value(getattr(paths, "plan_json_path", None))
158
+ if plan_json_path is not None:
159
+ return plan_json_path.parent
160
+ planner_chat_path = _path_value(getattr(paths, "planner_chat_path", None))
161
+ if planner_chat_path is not None:
162
+ return planner_chat_path.parent.parent
163
+ planner_summary_path = _path_value(getattr(paths, "planner_summary_path", None))
164
+ if planner_summary_path is not None:
165
+ return planner_summary_path.parent.parent
166
+ root = resolve_knowledge_workspace_root(paths)
167
+ return root / ".alysis" / "planner"
168
+
169
+
170
+ def _coerce_planner_paths(paths: Any) -> Any:
171
+ root = resolve_knowledge_workspace_root(paths)
172
+ plan_dir = _planner_plan_dir(paths)
173
+ run_id = str(getattr(paths, "run_id", "") or "").strip()
174
+ if not run_id:
175
+ run_id = plan_dir.parent.name
176
+ knowledge_index_path = _path_value(getattr(paths, "knowledge_index_path", None))
177
+ if knowledge_index_path is None:
178
+ knowledge_index_path = plan_dir.parent / "knowledge" / "index.json"
179
+ knowledge_selected_dir = _path_value(getattr(paths, "knowledge_selected_dir", None))
180
+ if knowledge_selected_dir is None:
181
+ knowledge_selected_dir = plan_dir.parent / "knowledge" / "selected"
182
+ return SimpleNamespace(
183
+ root=root,
184
+ run_id=run_id,
185
+ plan_dir=plan_dir,
186
+ knowledge_index_path=knowledge_index_path,
187
+ knowledge_selected_dir=knowledge_selected_dir,
188
+ )
189
+
190
+
191
+ def _entry_keywords(entry: KnowledgeIndexEntry) -> set[str]:
192
+ return _tokenize(
193
+ " ".join(
194
+ [
195
+ entry.title,
196
+ entry.preview,
197
+ *entry.tags,
198
+ *entry.paths,
199
+ ]
200
+ )
201
+ )
202
+
203
+
204
+ def _parse_iso(value: str) -> datetime | None:
205
+ text = value.strip()
206
+ if not text:
207
+ return None
208
+ normalized = text.replace("Z", "+00:00")
209
+ try:
210
+ return datetime.fromisoformat(normalized)
211
+ except ValueError:
212
+ return None
213
+
214
+
215
+ @dataclass(frozen=True)
216
+ class KnowledgeSelection:
217
+ entry: KnowledgeIndexEntry
218
+ score: int
219
+ reasons: tuple[str, ...]
220
+
221
+ def to_payload(self) -> dict[str, Any]:
222
+ payload = self.entry.to_payload()
223
+ payload["score"] = self.score
224
+ payload["reasons"] = list(self.reasons)
225
+ return payload
226
+
227
+
228
+ @dataclass(frozen=True)
229
+ class MaterializedKnowledgeSelection:
230
+ task_id: str
231
+ selection_label: str
232
+ selected_dir: Path
233
+ entries_dir: Path
234
+ manifest_path: Path
235
+ summary_path: Path
236
+ selections: tuple[KnowledgeSelection, ...]
237
+
238
+ def render_prompt_section(self, *, workspace_root: Path) -> str:
239
+ manifest_rel = self.manifest_path.resolve().relative_to(workspace_root.resolve()).as_posix()
240
+ entries_rel = self.entries_dir.resolve().relative_to(workspace_root.resolve()).as_posix()
241
+ lines = [
242
+ "## Relevant Knowledge",
243
+ "",
244
+ f"- Manifest: `{manifest_rel}`",
245
+ f"- Selected Knowledge Files: `{entries_rel}`",
246
+ ]
247
+ if not self.selections:
248
+ lines.append("- No prior knowledge entries were selected for this task.")
249
+ return "\n".join(lines)
250
+
251
+ for selection in self.selections:
252
+ reason_text = (
253
+ "; ".join(selection.reasons[:3]) if selection.reasons else "baseline relevance"
254
+ )
255
+ state_suffix = ""
256
+ effective_status = selection.entry.effective_status or selection.entry.status
257
+ if selection.entry.kind in {"issue", "decision"}:
258
+ state_suffix = f"; state={effective_status}"
259
+ lines.append(
260
+ f"- `{selection.entry.kind}` `{selection.entry.id}`: {selection.entry.title} "
261
+ f"(score={selection.score}{state_suffix}; {reason_text}). Preview: {selection.entry.preview or '(none)'}"
262
+ )
263
+ return "\n".join(lines)
264
+
265
+
266
+ def _current_issue_status(entry: KnowledgeIndexEntry) -> str:
267
+ return entry.effective_status or entry.status
268
+
269
+
270
+ def _current_decision_status(entry: KnowledgeIndexEntry) -> str:
271
+ return entry.effective_status or entry.status
272
+
273
+
274
+ def _consumer_issue_boost(*, consumer: KnowledgeConsumer) -> int:
275
+ if consumer == "replanner":
276
+ return 7
277
+ if consumer == "planner":
278
+ return 5
279
+ return 4
280
+
281
+
282
+ def _consumer_decision_boost(*, consumer: KnowledgeConsumer) -> int:
283
+ if consumer == "replanner":
284
+ return 5
285
+ if consumer == "planner":
286
+ return 4
287
+ return 3
288
+
289
+
290
+ def _consumer_fact_boost(*, consumer: KnowledgeConsumer) -> int:
291
+ return 2 if consumer in {"planner", "replanner"} else 1
292
+
293
+
294
+ def _consumer_task_attempt_boost(*, consumer: KnowledgeConsumer) -> int:
295
+ if consumer == "replanner":
296
+ return 3
297
+ if consumer == "planner":
298
+ return 1
299
+ return 0
300
+
301
+
302
+ def _entry_sort_priority(entry: KnowledgeIndexEntry, *, consumer: KnowledgeConsumer) -> int:
303
+ if consumer == "replanner":
304
+ order = {"issue": 4, "decision": 3, "task_attempt": 2, "fact": 1}
305
+ elif consumer == "planner":
306
+ order = {"decision": 4, "issue": 3, "fact": 2, "task_attempt": 1}
307
+ else:
308
+ order = {"issue": 4, "decision": 3, "fact": 2, "task_attempt": 1}
309
+ return order.get(entry.kind, 0)
310
+
311
+
312
+ def _resolve_current_run_id(paths: Any) -> str:
313
+ run_id = str(getattr(paths, "run_id", "") or "").strip()
314
+ if run_id:
315
+ return run_id
316
+ for attr in ("knowledge_index_path", "plan_dir", "plan_json_path", "planner_chat_path"):
317
+ path_value = _path_value(getattr(paths, attr, None))
318
+ if path_value is None:
319
+ continue
320
+ derived = derive_run_id_from_knowledge_file_path(path_value)
321
+ if derived:
322
+ return derived
323
+ return ""
324
+
325
+
326
+ def _score_entry(
327
+ *,
328
+ task: dict[str, Any],
329
+ entry: KnowledgeIndexEntry,
330
+ extra_paths: list[str] | None,
331
+ now: datetime,
332
+ consumer: KnowledgeConsumer,
333
+ current_run_id: str,
334
+ ) -> KnowledgeSelection | None:
335
+ if entry.kind == "task_attempt" and entry.resolves:
336
+ return None
337
+ current_task_id = str(task.get("id") or "").strip()
338
+ task_paths = _task_paths(task, extra_paths=extra_paths)
339
+ task_keywords = _task_keywords(task)
340
+ dependency_ids = {
341
+ str(item).strip() for item in task.get("dependencies") or [] if str(item).strip()
342
+ }
343
+
344
+ score = 0
345
+ reasons: list[str] = []
346
+ same_run = bool(current_run_id and entry.run_id and current_run_id == entry.run_id)
347
+
348
+ normalized_entry_paths = {
349
+ _normalize_rel_path(path) for path in entry.paths if _normalize_rel_path(path)
350
+ }
351
+ path_overlap = sorted(task_paths.intersection(normalized_entry_paths))
352
+ if path_overlap:
353
+ score += 8 + min(4, len(path_overlap))
354
+ reasons.append(f"path overlap: {', '.join(path_overlap[:3])}")
355
+
356
+ conflict_overlap = sorted(
357
+ {
358
+ _normalize_rel_path(path) for path in extra_paths or [] if _normalize_rel_path(path)
359
+ }.intersection(normalized_entry_paths)
360
+ )
361
+ if conflict_overlap:
362
+ score += 5 + min(3, len(conflict_overlap))
363
+ reasons.append(f"conflict overlap: {', '.join(conflict_overlap[:3])}")
364
+
365
+ if same_run and entry.task_id and entry.task_id == current_task_id:
366
+ score += 6
367
+ reasons.append("same task history")
368
+
369
+ dependency_overlap = (
370
+ sorted(dependency_ids.intersection({entry.task_id, *entry.related_tasks}))
371
+ if same_run
372
+ else []
373
+ )
374
+ if dependency_overlap:
375
+ score += 5 + min(2, len(dependency_overlap))
376
+ reasons.append(f"dependency link: {', '.join(dependency_overlap[:3])}")
377
+
378
+ keyword_overlap = sorted(task_keywords.intersection(_entry_keywords(entry)))
379
+ if keyword_overlap:
380
+ score += min(8, len(keyword_overlap) * 2)
381
+ reasons.append(f"keyword overlap: {', '.join(keyword_overlap[:3])}")
382
+
383
+ if score <= 0:
384
+ return None
385
+
386
+ effective_status = _current_issue_status(entry)
387
+ if entry.kind == "issue" and is_effectively_open_status(effective_status):
388
+ score += _consumer_issue_boost(consumer=consumer)
389
+ reasons.append("open issue boost")
390
+ if entry.kind == "decision" and _current_decision_status(entry) == "active":
391
+ score += _consumer_decision_boost(consumer=consumer)
392
+ reasons.append("active decision boost")
393
+ if entry.kind == "fact" and score > 0:
394
+ score += _consumer_fact_boost(consumer=consumer)
395
+ reasons.append("recorded fact context")
396
+ if (
397
+ entry.kind == "task_attempt"
398
+ and entry.result == "success"
399
+ and is_effectively_accepted_task_attempt(entry.effective_status or entry.status)
400
+ and score > 0
401
+ ):
402
+ bonus = _consumer_task_attempt_boost(consumer=consumer)
403
+ if bonus:
404
+ score += bonus
405
+ reasons.append("accepted task attempt context")
406
+
407
+ created_at = _parse_iso(entry.created_at)
408
+ if created_at is not None:
409
+ age_seconds = max(0.0, (now - created_at).total_seconds())
410
+ if age_seconds <= 7 * 24 * 3600:
411
+ score += 3
412
+ reasons.append("recent context")
413
+ elif age_seconds <= 30 * 24 * 3600:
414
+ score += 1
415
+ reasons.append("moderately recent context")
416
+
417
+ return KnowledgeSelection(
418
+ entry=entry,
419
+ score=score,
420
+ reasons=tuple(reasons),
421
+ )
422
+
423
+
424
+ def select_relevant_knowledge(
425
+ *,
426
+ paths: RunPaths,
427
+ task: dict[str, Any],
428
+ extra_paths: list[str] | None = None,
429
+ limit: int = 4,
430
+ consumer: KnowledgeConsumer = "execution",
431
+ ) -> tuple[KnowledgeSelection, ...]:
432
+ index = load_knowledge_index(paths, rebuild=False)
433
+ now = datetime.now(UTC)
434
+ current_run_id = _resolve_current_run_id(paths)
435
+ scored: list[KnowledgeSelection] = []
436
+ for entry in index.entries:
437
+ selection = _score_entry(
438
+ task=task,
439
+ entry=entry,
440
+ extra_paths=extra_paths,
441
+ now=now,
442
+ consumer=consumer,
443
+ current_run_id=current_run_id,
444
+ )
445
+ if selection is not None:
446
+ scored.append(selection)
447
+ scored.sort(
448
+ key=lambda item: (
449
+ item.score,
450
+ _entry_sort_priority(item.entry, consumer=consumer),
451
+ item.entry.created_at,
452
+ item.entry.id,
453
+ ),
454
+ reverse=True,
455
+ )
456
+ return tuple(scored[: max(0, int(limit))])
457
+
458
+
459
+ def materialize_selected_knowledge(
460
+ *,
461
+ paths: RunPaths,
462
+ task_id: str,
463
+ selection_label: str,
464
+ selections: tuple[KnowledgeSelection, ...],
465
+ selected_dir_override: Path | None = None,
466
+ ) -> MaterializedKnowledgeSelection:
467
+ selected_dir = (
468
+ selected_dir_override
469
+ if selected_dir_override is not None
470
+ else (
471
+ paths.knowledge_selected_dir
472
+ / _sanitize_component(task_id)
473
+ / _sanitize_component(selection_label)
474
+ )
475
+ )
476
+ entries_dir = selected_dir / "entries"
477
+ if selected_dir.exists():
478
+ shutil.rmtree(selected_dir, ignore_errors=True)
479
+ entries_dir.mkdir(parents=True, exist_ok=True)
480
+
481
+ manifest_entries: list[dict[str, Any]] = []
482
+ for index, selection in enumerate(selections, start=1):
483
+ source_path = paths.root / selection.entry.knowledge_file_path
484
+ materialized_path = entries_dir / f"{index:02d}_{source_path.name}"
485
+ shutil.copy2(source_path, materialized_path)
486
+ payload = selection.to_payload()
487
+ payload["materialized_path"] = (
488
+ materialized_path.resolve().relative_to(paths.root.resolve()).as_posix()
489
+ )
490
+ manifest_entries.append(payload)
491
+
492
+ manifest_path = selected_dir / "manifest.json"
493
+ manifest_payload = {
494
+ "schema_version": 1,
495
+ "generated_at": _now_iso(),
496
+ "task_id": task_id,
497
+ "selection_label": selection_label,
498
+ "manifest_path": manifest_path.resolve().relative_to(paths.root.resolve()).as_posix(),
499
+ "selected_entries": manifest_entries,
500
+ }
501
+ manifest_path.write_text(
502
+ json.dumps(manifest_payload, indent=2, sort_keys=True) + "\n",
503
+ encoding="utf-8",
504
+ )
505
+ summary_path = selected_dir / "summary.md"
506
+ section = MaterializedKnowledgeSelection(
507
+ task_id=task_id,
508
+ selection_label=selection_label,
509
+ selected_dir=selected_dir,
510
+ entries_dir=entries_dir,
511
+ manifest_path=manifest_path,
512
+ summary_path=summary_path,
513
+ selections=selections,
514
+ ).render_prompt_section(workspace_root=paths.root)
515
+ summary_path.write_text(section.rstrip() + "\n", encoding="utf-8")
516
+ return MaterializedKnowledgeSelection(
517
+ task_id=task_id,
518
+ selection_label=selection_label,
519
+ selected_dir=selected_dir,
520
+ entries_dir=entries_dir,
521
+ manifest_path=manifest_path,
522
+ summary_path=summary_path,
523
+ selections=selections,
524
+ )
525
+
526
+
527
+ def prepare_relevant_knowledge(
528
+ *,
529
+ paths: RunPaths,
530
+ task: dict[str, Any],
531
+ selection_label: str,
532
+ extra_paths: list[str] | None = None,
533
+ limit: int = 4,
534
+ consumer: KnowledgeConsumer = "execution",
535
+ selected_dir_override: Path | None = None,
536
+ ) -> MaterializedKnowledgeSelection:
537
+ rebuild_knowledge_index(paths)
538
+ selections = select_relevant_knowledge(
539
+ paths=paths,
540
+ task=task,
541
+ extra_paths=extra_paths,
542
+ limit=limit,
543
+ consumer=consumer,
544
+ )
545
+ task_id = str(task.get("id") or "").strip() or "task"
546
+ return materialize_selected_knowledge(
547
+ paths=paths,
548
+ task_id=task_id,
549
+ selection_label=selection_label,
550
+ selections=selections,
551
+ selected_dir_override=selected_dir_override,
552
+ )
553
+
554
+
555
+ def prepare_planner_knowledge(
556
+ *,
557
+ paths: RunPaths,
558
+ plan: dict[str, Any],
559
+ user_text: str,
560
+ selection_label: str = "planner",
561
+ limit: int = 5,
562
+ selected_dir_override: Path | None = None,
563
+ ) -> MaterializedKnowledgeSelection:
564
+ effective_paths = _coerce_planner_paths(paths)
565
+ task_titles = [
566
+ str(task.get("title") or "").strip()
567
+ for task in plan.get("tasks") or []
568
+ if isinstance(task, dict) and str(task.get("title") or "").strip()
569
+ ]
570
+ title = str(user_text or "").strip() or str(plan.get("project_goal") or "").strip() or "planner"
571
+ description_parts = [
572
+ str(plan.get("project_goal") or "").strip(),
573
+ str(plan.get("summary") or "").strip(),
574
+ "Current plan tasks: " + "; ".join(task_titles[:8]) if task_titles else "",
575
+ ]
576
+ task_id_candidates = {
577
+ str(task.get("id") or "").strip()
578
+ for task in plan.get("tasks") or []
579
+ if isinstance(task, dict) and str(task.get("id") or "").strip()
580
+ }
581
+ hinted_task_ids = [
582
+ task_id for task_id in _task_id_hints(user_text) if task_id in task_id_candidates
583
+ ]
584
+ path_hints = extract_repo_path_hints(user_text)
585
+ synthetic_task = {
586
+ "id": "planner",
587
+ "title": title,
588
+ "description": "\n".join(part for part in description_parts if part),
589
+ "acceptance_criteria": [],
590
+ "estimated_files": path_hints,
591
+ "write_scope": path_hints,
592
+ "dependencies": hinted_task_ids,
593
+ }
594
+ selected_dir = selected_dir_override or (
595
+ effective_paths.plan_dir / "selected_knowledge" / _sanitize_component(selection_label)
596
+ )
597
+ return prepare_relevant_knowledge(
598
+ paths=effective_paths,
599
+ task=synthetic_task,
600
+ selection_label=selection_label,
601
+ extra_paths=path_hints,
602
+ limit=limit,
603
+ consumer="planner",
604
+ selected_dir_override=selected_dir,
605
+ )
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+ DEFAULT_REPLY_LANGUAGE = "English"
7
+ DEFAULT_REPLY_SCRIPT = "Latin"
8
+
9
+ _MAX_LANGUAGE_FIELD_CHARS = 80
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class LanguagePolicyDecision:
14
+ language: str = ""
15
+ script: str = ""
16
+ confidence: float = 0.0
17
+ explicit_language_override: bool = False
18
+ source: str = "default"
19
+ failure_reason: str = ""
20
+
21
+
22
+ def _normalize_model_text(raw: object, *, max_chars: int = _MAX_LANGUAGE_FIELD_CHARS) -> str:
23
+ text = re.sub(r"\s+", " ", str(raw or "").strip())
24
+ if not text:
25
+ return ""
26
+ return text[:max_chars]
27
+
28
+
29
+ def normalize_language_name(raw: object) -> str:
30
+ return _normalize_model_text(raw)
31
+
32
+
33
+ def normalize_script_name(raw: object) -> str:
34
+ return _normalize_model_text(raw)