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,1215 @@
1
+ # ruff: noqa: F401,F403,F405,I001
2
+ # Legacy split module: dependencies are synced by cli_surface.py.
3
+ from __future__ import annotations
4
+
5
+ from .cli_common import *
6
+ from .forge_asset_view import forge_asset_view_count
7
+
8
+
9
+ def _run_plan_mode_approval_loop(
10
+ *,
11
+ session: Any,
12
+ console: Console,
13
+ user_message: str,
14
+ max_iterations: int | None = None,
15
+ ) -> str | None:
16
+ from ..chat import _run_plan_mode_approval_loop_impl
17
+
18
+ return _run_plan_mode_approval_loop_impl(
19
+ _cli_module_for_legacy_impl(),
20
+ session=session,
21
+ console=console,
22
+ user_message=user_message,
23
+ max_iterations=max_iterations,
24
+ )
25
+
26
+
27
+ def _planning_help_panel() -> Panel:
28
+ rows = [
29
+ ("/help", "show planning commands"),
30
+ ("/goal <text>", "set project goal"),
31
+ (
32
+ "/task <title>",
33
+ "add a task; ambiguous/mutating work must name repo-relative file paths",
34
+ ),
35
+ ("/done", "save + validate the plan, then return to chat"),
36
+ ]
37
+ if _patchable("_is_narrow_terminal", _is_narrow_terminal)():
38
+ lines = [f"{cmd} - {desc}" for cmd, desc in rows]
39
+ lines.append("")
40
+ for footer_line in _forge_help_footer_lines():
41
+ lines.append(f"[dim]{footer_line}[/dim]")
42
+ text = "\n".join(lines)
43
+ return _Panel(text, title="Planning Help", border_style="bright_black")
44
+ table = _Table(
45
+ show_header=False,
46
+ box=None,
47
+ expand=True,
48
+ padding=(0, 1),
49
+ collapse_padding=True,
50
+ )
51
+ table.add_column("command", style=STYLE_EMPHASIS, no_wrap=True, ratio=2)
52
+ table.add_column("description", style="dim", no_wrap=False, ratio=5, overflow="fold")
53
+ for cmd, desc in rows:
54
+ table.add_row(cmd, desc)
55
+ content = _table_grid(expand=True)
56
+ content.add_row(table)
57
+ content.add_row("")
58
+ for footer_line in _forge_help_footer_lines():
59
+ content.add_row(_forge_bar_text(text=footer_line, style="dim"))
60
+ return _Panel(content, title="Planning Help", border_style="bright_black")
61
+
62
+
63
+ def _forge_help_panel() -> Panel:
64
+ return _chat_commands_panel(ui_mode="forge")
65
+
66
+
67
+ def _forge_enter_panel(
68
+ *,
69
+ paths: RunPaths,
70
+ plan: dict[str, Any],
71
+ entry_kind: str,
72
+ assistant_enabled: bool,
73
+ workspace_summary_lines: list[str] | None = None,
74
+ model: str | None = None,
75
+ mode: str | None = None,
76
+ ) -> Any:
77
+ from rich.console import Group
78
+
79
+ _ = workspace_summary_lines
80
+ task_count = len(plan.get("tasks") or [])
81
+ task_label = "task" if task_count == 1 else "tasks"
82
+ assistant_label = "assistant on" if assistant_enabled else "assistant off"
83
+ lines = [
84
+ _forge_bar_text(
85
+ text=f"Forge ready · run {paths.run_id} · {task_count} {task_label} · {assistant_label}",
86
+ style=STYLE_EMPHASIS,
87
+ ),
88
+ ]
89
+ context_bits = []
90
+ if str(model or "").strip():
91
+ context_bits.append(f"model {str(model).strip()}")
92
+ if str(mode or "").strip():
93
+ context_bits.append(f"mode {str(mode).strip()}")
94
+ if context_bits:
95
+ lines.append(_forge_bar_text(text=" · ".join(context_bits), style="dim"))
96
+ lines.append(
97
+ _forge_bar_text(
98
+ text=_forge_entry_status_text(entry_kind=entry_kind),
99
+ style="dim",
100
+ )
101
+ )
102
+ lines.append(
103
+ _forge_bar_text(
104
+ text="/show for summary · /execute plan when ready · /help for commands",
105
+ style="dim",
106
+ )
107
+ )
108
+ return Group(*lines)
109
+
110
+
111
+ def _forge_requirement_text(item: Any) -> str:
112
+ return requirement_text(item)
113
+
114
+
115
+ def _forge_has_nonempty_requirement(plan: dict[str, Any]) -> bool:
116
+ requirements = plan.get("requirements") or []
117
+ if not isinstance(requirements, list):
118
+ return False
119
+ return any(bool(_forge_requirement_text(req)) for req in requirements)
120
+
121
+
122
+ def _forge_has_usable_plan_input(plan: dict[str, Any]) -> bool:
123
+ tasks = plan.get("tasks") or []
124
+ has_tasks = isinstance(tasks, list) and len(tasks) > 0
125
+ return has_tasks or _forge_has_nonempty_requirement(plan)
126
+
127
+
128
+ def _forge_no_execution_ready_tasks_message(plan: dict[str, Any]) -> str | None:
129
+ tasks = plan.get("tasks") or []
130
+ if isinstance(tasks, list) and tasks:
131
+ return None
132
+ requirements = plan.get("requirements") or []
133
+ if not isinstance(requirements, list):
134
+ return None
135
+ if not any(_forge_requirement_text(req) for req in requirements):
136
+ return None
137
+ if any(requirement_is_execution_ready(req) for req in requirements):
138
+ return None
139
+ return (
140
+ "Execution blocked: planner fallback captured requirements, but no execution-ready "
141
+ "tasks exist. Re-run the planner after the model/API is available, or add scoped "
142
+ "tasks with repo-relative file paths before executing."
143
+ )
144
+
145
+
146
+ def _validate_forge_plan_shape(candidate: Any) -> str | None:
147
+ if not isinstance(candidate, dict):
148
+ return "Plan JSON must be an object."
149
+ for key in ("tasks", "assets", "requirements"):
150
+ if key in candidate and not isinstance(candidate.get(key), list):
151
+ return f"Invalid plan JSON: '{key}' must be an array."
152
+ return None
153
+
154
+
155
+ def _validate_forge_plan_for_paths(paths: RunPaths, plan: dict[str, Any]) -> list[str]:
156
+ warnings = _patchable("validate_plan", validate_plan)(plan)
157
+ if not hasattr(paths, "assets_index_path"):
158
+ return warnings
159
+ try:
160
+ cfg = load_config()
161
+ max_primary = int(cfg.assets.planner.max_primary_per_task)
162
+ except Exception as e: # noqa: BLE001
163
+ warnings.append(f"Asset validation config unavailable; using default limit: {e}")
164
+ max_primary = 8
165
+ try:
166
+ warnings.extend(
167
+ validate_plan_against_assets(
168
+ plan,
169
+ AssetIndex(paths),
170
+ max_primary_per_task=max_primary,
171
+ )
172
+ )
173
+ except Exception as e: # noqa: BLE001
174
+ warnings.append(f"Asset validation skipped: {e}")
175
+ return warnings
176
+ return warnings
177
+
178
+
179
+ def _edit_forge_plan_json(
180
+ *,
181
+ console: Console,
182
+ paths: RunPaths,
183
+ forge_state: _ForgeChatState,
184
+ ) -> None:
185
+ current_plan = forge_state.plan
186
+ if current_plan is None:
187
+ _print_forge_warning_messages(
188
+ console=console,
189
+ label="Plan edit",
190
+ warnings=["No Forge plan loaded."],
191
+ )
192
+ return
193
+
194
+ save_plan(paths, current_plan)
195
+ backup_path = paths.plan_json_path.with_suffix(".json.bak")
196
+ try:
197
+ backup_path.write_text(paths.plan_json_path.read_text(encoding="utf-8"), encoding="utf-8")
198
+ except Exception as e: # noqa: BLE001
199
+ _print_forge_warning_messages(
200
+ console=console,
201
+ label="Plan edit",
202
+ warnings=[f"Backup warning: {e}"],
203
+ )
204
+
205
+ try:
206
+ edited_text = typer.edit(filename=os.fspath(paths.plan_json_path), extension=".json")
207
+ except TypeError:
208
+ edited_text = typer.edit(os.fspath(paths.plan_json_path), extension=".json")
209
+ except Exception as e: # noqa: BLE001
210
+ _print_forge_warning_messages(
211
+ console=console,
212
+ label="Plan edit",
213
+ warnings=[f"Editor unavailable: {e}"],
214
+ )
215
+ _print_forge_meta(console=console, message=f"Edit directly · {paths.plan_json_path}")
216
+ return
217
+
218
+ if edited_text is None:
219
+ _print_forge_meta(console=console, message="Plan edit canceled.")
220
+ return
221
+
222
+ try:
223
+ edited_raw = paths.plan_json_path.read_text(encoding="utf-8")
224
+ except Exception as e: # noqa: BLE001
225
+ _print_forge_error(console=console, message=f"Failed to read updated plan file: {e}")
226
+ return
227
+
228
+ try:
229
+ candidate = json.loads(edited_raw)
230
+ except json.JSONDecodeError as e:
231
+ _print_forge_error(console=console, message=f"Invalid JSON: {e}")
232
+ _print_forge_meta(console=console, message="Current in-memory plan was not changed.")
233
+ try:
234
+ if backup_path.exists():
235
+ paths.plan_json_path.write_text(
236
+ backup_path.read_text(encoding="utf-8"), encoding="utf-8"
237
+ )
238
+ except Exception as restore_error: # noqa: BLE001
239
+ _print_forge_warning_messages(
240
+ console=console,
241
+ label="Plan edit",
242
+ warnings=[f"Failed to restore previous plan JSON: {restore_error}"],
243
+ )
244
+ return
245
+
246
+ shape_error = _validate_forge_plan_shape(candidate)
247
+ if shape_error:
248
+ _print_forge_error(console=console, message=shape_error)
249
+ _print_forge_meta(console=console, message="Current in-memory plan was not changed.")
250
+ try:
251
+ if backup_path.exists():
252
+ paths.plan_json_path.write_text(
253
+ backup_path.read_text(encoding="utf-8"), encoding="utf-8"
254
+ )
255
+ except Exception as restore_error: # noqa: BLE001
256
+ _print_forge_warning_messages(
257
+ console=console,
258
+ label="Plan edit",
259
+ warnings=[f"Failed to restore previous plan JSON: {restore_error}"],
260
+ )
261
+ return
262
+
263
+ assert isinstance(candidate, dict)
264
+ validation_warnings = _validate_forge_plan_for_paths(paths, candidate)
265
+ save_plan(paths, candidate)
266
+ forge_state.plan = candidate
267
+ _print_forge_meta(console=console, message=f"Updated plan · {paths.plan_json_path}")
268
+ if validation_warnings:
269
+ _print_forge_warning_messages(
270
+ console=console,
271
+ label="Plan validation",
272
+ warnings=validation_warnings,
273
+ )
274
+
275
+
276
+ def _show_forge_plan_summary(*, console: Console, paths: RunPaths, plan: dict[str, Any]) -> None:
277
+ goal = str(plan.get("project_goal") or "").strip() or "(not set)"
278
+ summary = str(plan.get("summary") or "").strip() or "(not set)"
279
+ tasks = plan.get("tasks") or []
280
+ task_count = len(tasks) if isinstance(tasks, list) else 0
281
+ asset_count = forge_asset_view_count(paths, plan)
282
+ _print_forge_meta(
283
+ console=console,
284
+ message=f"Run {paths.run_id} · {task_count} tasks · {asset_count} assets",
285
+ style=STYLE_EMPHASIS,
286
+ )
287
+ _print_forge_meta(console=console, message=f"Goal · {goal}", style=STYLE_CONTENT)
288
+ _print_forge_meta(console=console, message=f"Summary · {summary}", style=STYLE_CONTENT)
289
+ requirements = plan.get("requirements") or []
290
+ req_count = len(requirements) if isinstance(requirements, list) else 0
291
+ _print_forge_meta(console=console, message=f"Requirements · {req_count}", style="dim")
292
+ if req_count:
293
+ for idx, requirement in enumerate(requirements[:8], start=1):
294
+ req_text = _forge_requirement_text(requirement)
295
+ if not req_text:
296
+ continue
297
+ if len(req_text) > 120:
298
+ req_text = req_text[:117].rstrip() + "..."
299
+ _print_forge_meta(console=console, message=f"{idx}. {req_text}", style=STYLE_CONTENT)
300
+ if req_count > 8:
301
+ _print_forge_meta(
302
+ console=console,
303
+ message=f"... ({req_count - 8} more requirements)",
304
+ style="dim",
305
+ )
306
+
307
+ table = _forge_task_table()
308
+ table.add_column("id", style="dim", no_wrap=True, ratio=1)
309
+ table.add_column("status", style="dim", no_wrap=True, ratio=1)
310
+ table.add_column("title", style=STYLE_CONTENT, no_wrap=False, ratio=4, overflow="fold")
311
+ table.add_column("dependencies", style="dim", no_wrap=False, ratio=2, overflow="fold")
312
+ table.add_column("mcp", style="dim", no_wrap=False, ratio=2, overflow="fold")
313
+ if tasks:
314
+ for task in tasks:
315
+ deps = task.get("dependencies") or []
316
+ deps_label = ", ".join(str(dep) for dep in deps) if deps else "-"
317
+ table.add_row(
318
+ str(task.get("id") or "-"),
319
+ _forge_task_status_markup(str(task.get("status") or "planned")),
320
+ str(task.get("title") or "-"),
321
+ deps_label,
322
+ _forge_task_mcp_summary_label(task if isinstance(task, dict) else {}),
323
+ )
324
+ else:
325
+ table.add_row("-", "-", "(no tasks yet)", "-", "off")
326
+ console.print(table)
327
+ _print_forge_meta(console=console, message=f"Assets · {asset_count}", style="dim")
328
+
329
+
330
+ def _show_forge_plan_markdown(
331
+ *,
332
+ console: Console,
333
+ paths: RunPaths,
334
+ plan: dict[str, Any],
335
+ ) -> None:
336
+ # Ensure PLAN.md reflects in-memory plan changes before showing it.
337
+ save_plan(paths, plan)
338
+ plan_md_path = paths.plan_md_path
339
+ try:
340
+ markdown_text = plan_md_path.read_text(encoding="utf-8")
341
+ except Exception as e: # noqa: BLE001
342
+ _print_forge_error(console=console, message=f"Failed to read PLAN.md: {e}")
343
+ return
344
+
345
+ lines = markdown_text.splitlines()
346
+ preview_limit = 200
347
+ console.print(f"PLAN.md: {plan_md_path}")
348
+
349
+ if _patchable("_is_interactive_terminal", _is_interactive_terminal)():
350
+ try:
351
+ # Use plain-text pager output for maximum terminal compatibility.
352
+ pager_text = f"Press q to exit this view.\n\n{markdown_text}"
353
+ with console.pager(styles=False):
354
+ console.print(pager_text, markup=False, highlight=False)
355
+ return
356
+ except KeyboardInterrupt:
357
+ console.print("")
358
+ _print_forge_meta(console=console, message="Closed PLAN.md pager.")
359
+ return
360
+ except Exception as e: # noqa: BLE001
361
+ _print_forge_warning_messages(
362
+ console=console,
363
+ label="PLAN.md",
364
+ warnings=[f"Pager unavailable: {e}", "Showing preview instead."],
365
+ )
366
+
367
+ preview = "\n".join(lines[:preview_limit]).rstrip()
368
+ if preview:
369
+ console.print(preview, markup=False, highlight=False)
370
+ else:
371
+ console.print("(PLAN.md is empty)")
372
+ if len(lines) > preview_limit:
373
+ console.print(
374
+ f"[dim]... ({len(lines) - preview_limit} more lines in {plan_md_path.name})[/dim]"
375
+ )
376
+
377
+
378
+ def _forge_enrich_plan_enabled() -> bool:
379
+ raw = env_get("ALYSIS_FORGE_ENRICH_PLAN")
380
+ if raw is not None:
381
+ parsed = _parse_bool_text(raw)
382
+ return bool(parsed) if parsed is not None else False
383
+ return not _is_non_interactive_terminal()
384
+
385
+
386
+ def _forge_should_try_enrichment(validation_warnings: list[str]) -> bool:
387
+ target_tokens = (
388
+ "missing acceptance_criteria",
389
+ "missing estimated_files",
390
+ EXECUTION_UNREADY_SCOPE_WARNING.casefold(),
391
+ )
392
+ lowered = [str(item).casefold() for item in validation_warnings]
393
+ return any(any(token in warning for token in target_tokens) for warning in lowered)
394
+
395
+
396
+ def _set_forge_planner_follow_up_state(
397
+ *,
398
+ planner_state: _ForgePlannerSessionState,
399
+ questions: list[str] | None,
400
+ awaiting_clarification: bool,
401
+ ) -> None:
402
+ clean_questions = [
403
+ str(question).strip() for question in questions or [] if str(question).strip()
404
+ ]
405
+ planner_state.pending_questions = clean_questions
406
+ planner_state.awaiting_clarification = bool(awaiting_clarification and clean_questions)
407
+
408
+
409
+ def _sync_forge_planner_follow_up_state_from_result(
410
+ *,
411
+ planner_state: _ForgePlannerSessionState,
412
+ planner_result: Any,
413
+ goal_key: str | None = None,
414
+ ) -> None:
415
+ questions = getattr(planner_result, "questions", None)
416
+ plan_update = getattr(planner_result, "plan_update", None)
417
+ error = str(getattr(planner_result, "error", "") or "").strip()
418
+ awaiting = bool(not error and not plan_update)
419
+ _set_forge_planner_follow_up_state(
420
+ planner_state=planner_state,
421
+ questions=questions if isinstance(questions, list) else list(questions or []),
422
+ awaiting_clarification=awaiting,
423
+ )
424
+ if goal_key is None:
425
+ return
426
+ tracker = getattr(planner_state, "clarification_loop", None)
427
+ if tracker is None:
428
+ return
429
+ # A turn that produced a plan update resets the streak; only a genuine
430
+ # question-and-nothing-else turn counts toward the cap.
431
+ tracker.record(
432
+ goal_key=goal_key,
433
+ awaiting_clarification=awaiting and bool(planner_state.awaiting_clarification),
434
+ )
435
+
436
+
437
+ def _workspace_context_payload_for_paths(
438
+ *,
439
+ paths: RunPaths,
440
+ refresh_if_stale: bool = False,
441
+ ) -> dict[str, Any] | None:
442
+ try:
443
+ scan = ensure_workspace_context_artifacts(paths, refresh_if_stale=refresh_if_stale)
444
+ except ForgeError:
445
+ return None
446
+ payload = scan.to_dict()
447
+ payload["greenfield"] = bool(getattr(paths, "greenfield", False))
448
+ return _augment_workspace_context_with_mcp_execution_context(
449
+ workspace_context=payload,
450
+ workspace_root=paths.root,
451
+ )
452
+
453
+
454
+ def _reconcile_plan_for_paths(
455
+ *,
456
+ paths: RunPaths,
457
+ plan: dict[str, Any],
458
+ refresh_if_stale: bool = False,
459
+ user_text: str | None = None,
460
+ transcript_tail: list[dict[str, Any]] | None = None,
461
+ target_task_ids: list[str] | tuple[str, ...] | None = None,
462
+ ) -> tuple[Any, dict[str, Any] | None]:
463
+ workspace_context = _workspace_context_payload_for_paths(
464
+ paths=paths,
465
+ refresh_if_stale=refresh_if_stale,
466
+ )
467
+ result = reconcile_plan_with_workspace(
468
+ plan,
469
+ workspace_root=paths.root,
470
+ workspace_context=workspace_context,
471
+ user_text=user_text,
472
+ transcript_tail=transcript_tail,
473
+ target_task_ids=(
474
+ {str(task_id).strip() for task_id in target_task_ids if str(task_id).strip()}
475
+ if target_task_ids is not None
476
+ else None
477
+ ),
478
+ )
479
+ return result, workspace_context
480
+
481
+
482
+ @dataclass(frozen=True)
483
+ class _ForgePlannerTurnControllerResult:
484
+ status: str
485
+ planner_result: Any | None = None
486
+ summary_line: str | None = None
487
+
488
+
489
+ @dataclass(frozen=True)
490
+ class _ForgePlannerUpdateSanitizationResult:
491
+ plan_update: dict[str, Any] | None
492
+ warnings: list[str]
493
+
494
+
495
+ def _sanitize_forge_enrichment_plan_update(
496
+ *,
497
+ plan: dict[str, Any],
498
+ plan_update: dict[str, Any] | None,
499
+ ) -> _ForgePlannerUpdateSanitizationResult:
500
+ if not isinstance(plan_update, dict):
501
+ return _ForgePlannerUpdateSanitizationResult(plan_update=None, warnings=[])
502
+
503
+ warnings: list[str] = []
504
+ allowed_top_level = {"tasks_update"}
505
+ dropped_top_level = sorted(set(plan_update) - allowed_top_level)
506
+ if dropped_top_level:
507
+ warnings.append(
508
+ "Ignored plan enrichment fields outside narrow execution-readiness scope: "
509
+ + ", ".join(dropped_top_level)
510
+ + "."
511
+ )
512
+
513
+ tasks_by_id = {
514
+ str(task.get("id") or "").strip(): task
515
+ for task in plan.get("tasks") or []
516
+ if isinstance(task, dict) and str(task.get("id") or "").strip()
517
+ }
518
+ raw_updates = plan_update.get("tasks_update")
519
+ if not isinstance(raw_updates, list):
520
+ return _ForgePlannerUpdateSanitizationResult(plan_update=None, warnings=warnings)
521
+
522
+ sanitized_updates: list[dict[str, Any]] = []
523
+ for raw_patch in raw_updates:
524
+ if not isinstance(raw_patch, dict):
525
+ warnings.append("Ignored non-object plan enrichment tasks_update entry.")
526
+ continue
527
+ task_id = str(raw_patch.get("id") or "").strip()
528
+ if not task_id:
529
+ warnings.append("Ignored plan enrichment tasks_update entry without an id.")
530
+ continue
531
+ current_task = tasks_by_id.get(task_id)
532
+ sanitized_patch: dict[str, Any] = {"id": task_id}
533
+
534
+ if "acceptance_criteria" in raw_patch:
535
+ current_acceptance = (
536
+ list(current_task.get("acceptance_criteria") or [])
537
+ if isinstance(current_task, dict)
538
+ else []
539
+ )
540
+ new_acceptance = list(raw_patch.get("acceptance_criteria") or [])
541
+ if current_acceptance:
542
+ warnings.append(
543
+ f"Ignored plan enrichment acceptance_criteria update for {task_id} "
544
+ "because that field is already populated."
545
+ )
546
+ elif new_acceptance:
547
+ sanitized_patch["acceptance_criteria"] = new_acceptance
548
+ else:
549
+ warnings.append(
550
+ f"Ignored empty plan enrichment acceptance_criteria update for {task_id}."
551
+ )
552
+
553
+ if "estimated_files" in raw_patch:
554
+ current_estimated_files = (
555
+ list(current_task.get("estimated_files") or [])
556
+ if isinstance(current_task, dict)
557
+ else []
558
+ )
559
+ new_estimated_files = list(raw_patch.get("estimated_files") or [])
560
+ if current_estimated_files:
561
+ warnings.append(
562
+ f"Ignored plan enrichment estimated_files update for {task_id} "
563
+ "because that field is already populated."
564
+ )
565
+ elif new_estimated_files:
566
+ sanitized_patch["estimated_files"] = new_estimated_files
567
+ else:
568
+ warnings.append(
569
+ f"Ignored empty plan enrichment estimated_files update for {task_id}."
570
+ )
571
+
572
+ dropped_patch_fields = sorted(
573
+ set(raw_patch)
574
+ - {
575
+ "id",
576
+ "acceptance_criteria",
577
+ "estimated_files",
578
+ }
579
+ )
580
+ if dropped_patch_fields:
581
+ warnings.append(
582
+ f"Ignored plan enrichment tasks_update fields for {task_id}: "
583
+ + ", ".join(dropped_patch_fields)
584
+ + "."
585
+ )
586
+
587
+ if len(sanitized_patch) > 1:
588
+ sanitized_updates.append(sanitized_patch)
589
+ else:
590
+ warnings.append(
591
+ f"Ignored plan enrichment tasks_update for {task_id} because it did not add "
592
+ "missing execution-readiness fields."
593
+ )
594
+
595
+ sanitized_update = {"tasks_update": sanitized_updates} if sanitized_updates else None
596
+ return _ForgePlannerUpdateSanitizationResult(
597
+ plan_update=sanitized_update,
598
+ warnings=list(dict.fromkeys(warnings)),
599
+ )
600
+
601
+
602
+ def _run_forge_planner_turn_controller(
603
+ *,
604
+ console: Console,
605
+ paths: RunPaths,
606
+ plan: dict[str, Any],
607
+ planner_state: _ForgePlannerSessionState,
608
+ user_text: str,
609
+ cfg_loader: Callable[[], AppConfig],
610
+ unavailable_message_builder: Callable[[Exception], str],
611
+ emit_meta: Callable[[str], None],
612
+ emit_warning_group: Callable[[str, list[str]], None],
613
+ api_key_override: str | None,
614
+ render_reply: Callable[[str, list[str] | None], None],
615
+ selection_label: str = "planner",
616
+ planning_relevant: bool = True,
617
+ refresh_workspace_context: bool = False,
618
+ trace_callback: Callable[[str, bool], None] | None = None,
619
+ stream: bool = False,
620
+ on_text_delta: Callable[[str], None] | None = None,
621
+ on_usage_events: Callable[[list[dict[str, Any]]], None] | None = None,
622
+ error_fallback: Callable[[], None] | None = None,
623
+ ) -> _ForgePlannerTurnControllerResult:
624
+ planner_state.transcript.append({"role": "user", "content": user_text})
625
+ append_planner_chat(paths, role="user", message=user_text)
626
+
627
+ if planner_state.cfg is None:
628
+ try:
629
+ planner_state.cfg = cfg_loader()
630
+ except Exception as e: # noqa: BLE001
631
+ assistant_message = unavailable_message_builder(e)
632
+ emit_meta(assistant_message)
633
+ append_transcript_note(paths, role="assistant", message=assistant_message)
634
+ append_planner_chat(paths, role="assistant", message=assistant_message)
635
+ planner_state.transcript.append({"role": "assistant", "content": assistant_message})
636
+ append_planner_summary(paths, "no plan_update proposed")
637
+ append_transcript_note(
638
+ paths,
639
+ role="system",
640
+ message="Planner proposed no plan update.",
641
+ )
642
+ _set_forge_planner_follow_up_state(
643
+ planner_state=planner_state,
644
+ questions=[],
645
+ awaiting_clarification=False,
646
+ )
647
+ return _ForgePlannerTurnControllerResult(status="unavailable")
648
+
649
+ if refresh_workspace_context or planner_state.workspace_context is None:
650
+ planner_state.workspace_context = _workspace_context_payload_for_paths(
651
+ paths=paths,
652
+ refresh_if_stale=refresh_workspace_context,
653
+ )
654
+
655
+ planner_knowledge = _patchable("prepare_planner_knowledge", prepare_planner_knowledge)(
656
+ paths=paths,
657
+ plan=plan,
658
+ user_text=user_text,
659
+ selection_label=selection_label,
660
+ )
661
+ planner_workspace_root = resolve_knowledge_workspace_root(paths)
662
+ clarification_tracker = getattr(planner_state, "clarification_loop", None)
663
+ goal_key = clarification_goal_key(plan=plan, user_text=user_text)
664
+ if clarification_tracker is not None:
665
+ clarification_tracker.max_rounds = resolve_plan_repair_policy(
666
+ planner_state.cfg
667
+ ).max_clarification_rounds
668
+ clarification_rounds = clarification_tracker.rounds_for(goal_key)
669
+ else:
670
+ clarification_rounds = 0
671
+ planner_result = _patchable("run_planner_turn", run_planner_turn)(
672
+ cfg=planner_state.cfg,
673
+ api_key_override=api_key_override,
674
+ plan=plan,
675
+ transcript_tail=planner_state.transcript,
676
+ workspace_context=planner_state.workspace_context,
677
+ user_text=user_text,
678
+ stream=stream,
679
+ on_text_delta=on_text_delta if stream else None,
680
+ relevant_knowledge_section=planner_knowledge.render_prompt_section(
681
+ workspace_root=planner_workspace_root
682
+ ),
683
+ prefer_context="forge",
684
+ awaiting_clarification=planner_state.awaiting_clarification,
685
+ pending_questions=planner_state.pending_questions,
686
+ clarification_rounds=clarification_rounds,
687
+ run_paths=paths,
688
+ )
689
+ usage_events = list(getattr(planner_result, "usage_events", []) or [])
690
+ if usage_events and on_usage_events is not None:
691
+ try:
692
+ on_usage_events(usage_events)
693
+ except Exception:
694
+ pass
695
+ render_reply(
696
+ str(getattr(planner_result, "assistant_message", "") or ""),
697
+ list(getattr(planner_result, "questions", []) or []),
698
+ )
699
+ assistant_message = str(getattr(planner_result, "assistant_message", "") or "")
700
+ append_transcript_note(paths, role="assistant", message=assistant_message)
701
+ append_planner_chat(paths, role="assistant", message=assistant_message)
702
+ planner_state.transcript.append({"role": "assistant", "content": assistant_message})
703
+
704
+ request_retry_count = int(getattr(planner_result, "request_retry_count", 0) or 0)
705
+ planner_error = str(getattr(planner_result, "error", "") or "").strip()
706
+ _sync_forge_planner_follow_up_state_from_result(
707
+ planner_state=planner_state,
708
+ planner_result=planner_result,
709
+ goal_key=goal_key,
710
+ )
711
+
712
+ if planner_error:
713
+ if trace_callback is not None:
714
+ retry_word = "retry" if request_retry_count == 1 else "retries"
715
+ error_trace_message = (
716
+ "Planner returned an error after "
717
+ f"{request_retry_count} transient {retry_word}; using fallback handling."
718
+ if request_retry_count > 0
719
+ else "Planner returned an error; using fallback handling."
720
+ )
721
+ trace_callback(error_trace_message, False)
722
+ elif trace_callback is not None:
723
+ trace_callback("Planner response ready.", False)
724
+
725
+ if request_retry_count > 0 and not planner_error:
726
+ retry_word = "retry" if request_retry_count == 1 else "retries"
727
+ retry_notice = (
728
+ f"Planner request recovered after {request_retry_count} transient {retry_word}."
729
+ )
730
+ emit_warning_group("Planner", [retry_notice])
731
+ append_transcript_note(paths, role="system", message=f"Planner warning: {retry_notice}")
732
+ if trace_callback is not None:
733
+ trace_callback(retry_notice, True)
734
+
735
+ questions = [
736
+ str(question).strip() for question in getattr(planner_result, "questions", []) or []
737
+ ]
738
+ if questions:
739
+ append_transcript_note(
740
+ paths,
741
+ role="system",
742
+ message=f"Planner questions: {'; '.join(questions)}",
743
+ )
744
+
745
+ if planner_error:
746
+ retry_word = "retry" if request_retry_count == 1 else "retries"
747
+ planner_error_summary = (
748
+ f"planner error after {request_retry_count} transient {retry_word}: {planner_error}"
749
+ if request_retry_count > 0
750
+ else f"planner error: {planner_error}"
751
+ )
752
+ planner_error_note = (
753
+ f"Planner error after {request_retry_count} transient {retry_word}: {planner_error}"
754
+ if request_retry_count > 0
755
+ else f"Planner error: {planner_error}"
756
+ )
757
+ planner_error_warnings = (
758
+ [f"Planner request failed after {request_retry_count} transient {retry_word}."]
759
+ if request_retry_count > 0
760
+ else []
761
+ )
762
+ planner_error_warnings.append(f"Final planner error: {planner_error}")
763
+ emit_warning_group("Planner", planner_error_warnings)
764
+ append_planner_summary(paths, planner_error_summary)
765
+ append_transcript_note(paths, role="system", message=planner_error_note)
766
+ if error_fallback is not None:
767
+ error_fallback()
768
+ return _ForgePlannerTurnControllerResult(
769
+ status="error",
770
+ planner_result=planner_result,
771
+ summary_line=planner_error_summary,
772
+ )
773
+
774
+ raw_plan_update = getattr(planner_result, "plan_update", None)
775
+ if raw_plan_update:
776
+ if not planning_relevant:
777
+ append_planner_summary(paths, "plan_update ignored: message not planning-relevant")
778
+ append_transcript_note(
779
+ paths,
780
+ role="system",
781
+ message="Planner update ignored because the user message was not planning-relevant.",
782
+ )
783
+ emit_meta(
784
+ "Planner update ignored because this message did not look "
785
+ "planning-related; plan unchanged."
786
+ )
787
+ return _ForgePlannerTurnControllerResult(
788
+ status="ignored",
789
+ planner_result=planner_result,
790
+ )
791
+
792
+ apply_result = apply_guarded_planner_plan_update(
793
+ plan,
794
+ raw_plan_update,
795
+ latest_user_text=user_text,
796
+ workspace_context=planner_state.workspace_context,
797
+ )
798
+ summary_line = summarize_plan_update(apply_result)
799
+ reconciliation_result = None
800
+ if apply_result.changed:
801
+ reconciliation_target_ids = list(
802
+ dict.fromkeys([*apply_result.added_task_ids, *apply_result.updated_task_ids])
803
+ )
804
+ reconciliation_kwargs: dict[str, Any] = {
805
+ "paths": paths,
806
+ "plan": plan,
807
+ "refresh_if_stale": False,
808
+ "user_text": user_text,
809
+ "transcript_tail": planner_state.transcript,
810
+ }
811
+ if reconciliation_target_ids:
812
+ reconciliation_kwargs["target_task_ids"] = reconciliation_target_ids
813
+ reconciliation_result, planner_state.workspace_context = _reconcile_plan_for_paths(
814
+ **reconciliation_kwargs
815
+ )
816
+ # Record what the repair loop had to salvage, and stamp the plan with an
817
+ # explicit draft/execution_ready status, before it is written. Downstream
818
+ # surfaces read the status instead of rediscovering it at exec time.
819
+ record_plan_repair(plan, getattr(planner_result, "repair", None) or PlannerRepairReport())
820
+ apply_plan_status(plan, validation_warnings=validate_plan(plan))
821
+ save_plan(paths, plan)
822
+ if apply_result.changed:
823
+ if trace_callback is not None:
824
+ trace_callback("Applied planner update to the Forge plan.", False)
825
+ emit_meta("Applied planner update to plan.")
826
+ if apply_result.warnings:
827
+ emit_warning_group("Planner", list(apply_result.warnings))
828
+ if reconciliation_result is not None and reconciliation_result.warnings:
829
+ emit_warning_group("Plan reconciliation", list(reconciliation_result.warnings))
830
+ else:
831
+ if trace_callback is not None:
832
+ trace_callback("Planner update was a no-op.", True)
833
+ emit_meta("Planner update contained no applicable changes.")
834
+ if apply_result.warnings:
835
+ emit_warning_group("Planner", list(apply_result.warnings))
836
+ append_planner_summary(paths, summary_line)
837
+ append_transcript_note(paths, role="system", message=f"Planner update: {summary_line}")
838
+ for warning in apply_result.warnings:
839
+ append_transcript_note(paths, role="system", message=f"Planner warning: {warning}")
840
+ if reconciliation_result is not None:
841
+ for warning in reconciliation_result.warnings:
842
+ append_transcript_note(
843
+ paths,
844
+ role="system",
845
+ message=f"Plan reconciliation warning: {warning}",
846
+ )
847
+ return _ForgePlannerTurnControllerResult(
848
+ status="applied" if apply_result.changed else "noop",
849
+ planner_result=planner_result,
850
+ summary_line=summary_line,
851
+ )
852
+
853
+ append_planner_summary(paths, "no plan_update proposed")
854
+ append_transcript_note(paths, role="system", message="Planner proposed no plan update.")
855
+ return _ForgePlannerTurnControllerResult(status="no_update", planner_result=planner_result)
856
+
857
+
858
+ def _write_plan_validation_artifact(
859
+ *,
860
+ paths: RunPaths,
861
+ reconciliation_result: Any,
862
+ validation_warnings: list[str],
863
+ ) -> None:
864
+ validation_path = paths.notes_dir / "plan_validation.md"
865
+ lines: list[str] = [
866
+ "# Plan Validation",
867
+ "",
868
+ "## Reconciliation",
869
+ "",
870
+ ]
871
+
872
+ changed = bool(getattr(reconciliation_result, "changed", False))
873
+ updated_task_ids = list(getattr(reconciliation_result, "updated_task_ids", []) or [])
874
+ reconciliation_warnings = list(getattr(reconciliation_result, "warnings", []) or [])
875
+ lines.append(f"- Changed: `{'yes' if changed else 'no'}`")
876
+ if updated_task_ids:
877
+ lines.append(f"- Updated Tasks: {', '.join(updated_task_ids)}")
878
+ else:
879
+ lines.append("- Updated Tasks: (none)")
880
+ if reconciliation_warnings:
881
+ lines.append("- Warnings:")
882
+ for warning in reconciliation_warnings:
883
+ lines.append(f" - {warning}")
884
+ else:
885
+ lines.append("- No reconciliation warnings.")
886
+
887
+ lines.extend(["", "## Validation", ""])
888
+ if validation_warnings:
889
+ for warning in validation_warnings:
890
+ lines.append(f"- {warning}")
891
+ else:
892
+ lines.append("- No validation warnings.")
893
+
894
+ validation_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
895
+
896
+
897
+ def _capture_forge_requirement_from_planner_fallback(
898
+ *,
899
+ plan: dict[str, Any],
900
+ paths: RunPaths,
901
+ console: Console,
902
+ user_text: str,
903
+ planning_relevant: bool | None = None,
904
+ ) -> bool:
905
+ if planning_relevant is False:
906
+ append_transcript_note(
907
+ paths,
908
+ role="system",
909
+ message="Skipped fallback requirement capture for non-planning message.",
910
+ )
911
+ _print_forge_meta(
912
+ console=console,
913
+ message="Planner skipped this message because it looked off-topic; plan unchanged.",
914
+ )
915
+ return False
916
+
917
+ if _set_forge_project_goal_if_empty(
918
+ plan=plan,
919
+ paths=paths,
920
+ console=console,
921
+ goal=user_text,
922
+ transcript_message="Captured project goal (planner produced no structured update).",
923
+ display_message="Captured project goal because the planner produced no structured update.",
924
+ ):
925
+ return True
926
+
927
+ add_requirement(
928
+ plan,
929
+ user_text,
930
+ execution_ready=False,
931
+ source="planner_error_fallback",
932
+ )
933
+ save_plan(paths, plan)
934
+ append_transcript_note(
935
+ paths,
936
+ role="system",
937
+ message="Captured requirement note (planner produced no structured update).",
938
+ )
939
+ _print_forge_meta(
940
+ console=console,
941
+ message="Captured requirement note because the planner produced no structured update.",
942
+ )
943
+ return True
944
+
945
+
946
+ def _set_forge_project_goal_if_empty(
947
+ *,
948
+ plan: dict[str, Any],
949
+ paths: RunPaths,
950
+ console: Console,
951
+ goal: str,
952
+ transcript_message: str = "Updated project goal.",
953
+ display_message: str = "Project goal set.",
954
+ ) -> bool:
955
+ normalized_goal = str(goal or "").strip()
956
+ if not normalized_goal or str(plan.get("project_goal") or "").strip():
957
+ return False
958
+ plan["project_goal"] = normalized_goal
959
+ if not str(plan.get("summary") or "").strip():
960
+ plan["summary"] = normalized_goal
961
+ save_plan(paths, plan)
962
+ append_transcript_note(paths, role="system", message=transcript_message)
963
+ _print_forge_meta(console=console, message=display_message)
964
+ return True
965
+
966
+
967
+ def _finalize_forge_plan(
968
+ *,
969
+ console: Console,
970
+ paths: RunPaths,
971
+ plan: dict[str, Any],
972
+ transcript_tail: list[dict[str, Any]] | None = None,
973
+ ) -> None:
974
+ finalize_plan(plan)
975
+ reconciliation_result, _ = _patchable(
976
+ "_reconcile_plan_for_paths",
977
+ _reconcile_plan_for_paths,
978
+ )(
979
+ paths=paths,
980
+ plan=plan,
981
+ refresh_if_stale=True,
982
+ transcript_tail=transcript_tail,
983
+ )
984
+ save_plan(paths, plan)
985
+ validation_warnings = _validate_forge_plan_for_paths(paths, plan)
986
+ if reconciliation_result.warnings:
987
+ _print_forge_warning_messages(
988
+ console=console,
989
+ label="Plan reconciliation",
990
+ warnings=list(reconciliation_result.warnings),
991
+ )
992
+ for warning in reconciliation_result.warnings:
993
+ append_transcript_note(
994
+ paths,
995
+ role="system",
996
+ message=f"Plan reconciliation warning: {warning}",
997
+ )
998
+ _write_plan_validation_artifact(
999
+ paths=paths,
1000
+ reconciliation_result=reconciliation_result,
1001
+ validation_warnings=validation_warnings,
1002
+ )
1003
+ if validation_warnings:
1004
+ _print_forge_warning_messages(
1005
+ console=console,
1006
+ label="Plan validation",
1007
+ warnings=validation_warnings,
1008
+ )
1009
+ for warning in validation_warnings:
1010
+ append_transcript_note(
1011
+ paths,
1012
+ role="system",
1013
+ message=f"Plan validation warning: {warning}",
1014
+ )
1015
+ _print_forge_meta(console=console, message=f"Plan saved · {paths.plan_md_path}")
1016
+ _print_forge_meta(console=console, message=f"Structured plan · {paths.plan_json_path}")
1017
+
1018
+
1019
+ def _enter_forge_mode(
1020
+ *,
1021
+ root: Path,
1022
+ console: Console,
1023
+ forge_state: _ForgeChatState,
1024
+ model: str | None = None,
1025
+ mode: str | None = None,
1026
+ planner_assistant_default: bool | None = None,
1027
+ ) -> bool:
1028
+ forge_binding: WorkspaceBinding | None = None
1029
+ try:
1030
+ forge_binding = resolve_workspace_binding(
1031
+ root,
1032
+ create_if_missing=False,
1033
+ allow_broad_workspace=False,
1034
+ source="forge_entry",
1035
+ )
1036
+ ensure_workspace_policy(
1037
+ forge_binding,
1038
+ action=WorkspaceAction.FORGE,
1039
+ allow_broad_workspace=False,
1040
+ )
1041
+ except WorkspaceBindingError as e:
1042
+ if forge_binding is not None:
1043
+ console.print("[yellow]Forge requires a narrower workspace before planning.[/yellow]")
1044
+ console.print(f"- requested path: {forge_binding.requested_path}")
1045
+ console.print(f"- workspace root: {forge_binding.workspace_context.workspace_root}")
1046
+ console.print(f"- focus dir: {forge_binding.workspace_context.focus_path}")
1047
+ console.print(f"- risk level: {forge_binding.risk_level}")
1048
+ for reason in forge_binding.risk_reasons:
1049
+ console.print(f"- reason: {reason}")
1050
+ console.print(
1051
+ workspace_policy_violation_message(forge_binding, action=WorkspaceAction.FORGE)
1052
+ )
1053
+ else:
1054
+ _print_forge_error(console=console, message=f"Forge error: {e}")
1055
+ return False
1056
+
1057
+ workspace_scan = None
1058
+ try:
1059
+ entry_selection = _select_forge_chat_entry(
1060
+ forge_state=forge_state,
1061
+ workspace_binding=forge_binding,
1062
+ )
1063
+ except ForgeError as e:
1064
+ _print_forge_error(console=console, message=f"Forge error: {e}")
1065
+ return False
1066
+ paths = entry_selection.paths
1067
+ plan = entry_selection.plan
1068
+ forge_state.paths = paths
1069
+ forge_state.plan = plan
1070
+ entry_goal = str(getattr(forge_state, "entry_goal", "") or "").strip()
1071
+ forge_state.entry_goal = ""
1072
+ try:
1073
+ workspace_scan = refresh_workspace_context_artifacts(paths)
1074
+ except ForgeError as e:
1075
+ _print_forge_error(console=console, message=f"Forge error: {e}")
1076
+ return False
1077
+ if entry_selection.entry_kind in {"session_local_resume", "session_local_resume_rebound"}:
1078
+ refresh_current_run_pointer_if_tracking_same_run(paths)
1079
+
1080
+ forge_state.ui_mode = "forge"
1081
+ if planner_assistant_default is None:
1082
+ forge_state.assistant_enabled = _patchable(
1083
+ "_prompt_forge_entry_plan_assistant",
1084
+ _prompt_forge_entry_plan_assistant,
1085
+ )(console=console)
1086
+ else:
1087
+ forge_state.assistant_enabled = bool(planner_assistant_default)
1088
+ if entry_goal:
1089
+ _set_forge_project_goal_if_empty(
1090
+ plan=plan,
1091
+ paths=paths,
1092
+ console=console,
1093
+ goal=entry_goal,
1094
+ transcript_message="Set project goal from /forge shortcut.",
1095
+ display_message="Project goal set from /forge.",
1096
+ )
1097
+ forge_state.planner_session = _ForgePlannerSessionState(
1098
+ workspace_context=_augment_workspace_context_with_mcp_execution_context(
1099
+ workspace_context={
1100
+ **workspace_scan.to_dict(),
1101
+ "greenfield": bool(getattr(paths, "greenfield", False)),
1102
+ },
1103
+ workspace_root=paths.root,
1104
+ )
1105
+ )
1106
+ console.print(
1107
+ _forge_enter_panel(
1108
+ paths=paths,
1109
+ plan=plan,
1110
+ entry_kind=entry_selection.entry_kind,
1111
+ assistant_enabled=forge_state.assistant_enabled,
1112
+ workspace_summary_lines=format_workspace_context_summary_lines(workspace_scan),
1113
+ model=model,
1114
+ mode=mode,
1115
+ )
1116
+ )
1117
+ if (
1118
+ not forge_state.assistant_enabled
1119
+ and planner_assistant_default is None
1120
+ and _is_non_interactive_terminal()
1121
+ and env_get("ALYSIS_FORGE_PLAN_ASSISTANT") is None
1122
+ ):
1123
+ _print_forge_meta(
1124
+ console=console,
1125
+ message=(
1126
+ "Plan Assistant defaulted off in non-interactive mode; set "
1127
+ "ALYSIS_FORGE_PLAN_ASSISTANT=1 to enable it."
1128
+ ),
1129
+ )
1130
+ return True
1131
+
1132
+
1133
+ def _is_forge_enter_command(*, cmd: str, arg: str) -> bool:
1134
+ parsed = _parse_forge_enter_command(cmd=cmd, arg=arg)
1135
+ return parsed is not None and parsed.usage_error is None
1136
+
1137
+
1138
+ def _handle_forge_chat_command(
1139
+ *,
1140
+ input_text: str,
1141
+ forge_state: _ForgeChatState,
1142
+ session: Any,
1143
+ console: Console,
1144
+ ) -> str:
1145
+ from ..chat import _handle_forge_chat_command_impl
1146
+
1147
+ return _handle_forge_chat_command_impl(
1148
+ _cli_module_for_legacy_impl(),
1149
+ input_text=input_text,
1150
+ forge_state=forge_state,
1151
+ session=session,
1152
+ console=console,
1153
+ )
1154
+
1155
+
1156
+ def _build_forge_exec_instruction(
1157
+ *,
1158
+ plan: dict[str, Any],
1159
+ task: dict[str, Any],
1160
+ cfg: AppConfig,
1161
+ role_model: str,
1162
+ ) -> str:
1163
+ return _shared_build_task_execution_instruction(
1164
+ plan=plan,
1165
+ task=task,
1166
+ cfg=cfg,
1167
+ role_model=role_model,
1168
+ )
1169
+
1170
+
1171
+ def _build_forge_exec_instruction_bundle(
1172
+ *,
1173
+ plan: dict[str, Any],
1174
+ task: dict[str, Any],
1175
+ root: Path,
1176
+ cfg: AppConfig,
1177
+ role_model: str,
1178
+ mode: str,
1179
+ yes: bool,
1180
+ deny_write_prefixes: list[str] | None = None,
1181
+ allow_write_globs: list[str] | None = None,
1182
+ non_interactive: bool = False,
1183
+ verification_enabled: bool = True,
1184
+ authoritative_verification_commands: list[str] | None = None,
1185
+ api_key: str | None = None,
1186
+ subagents_enabled: bool = False,
1187
+ leading_sections: list[str] | None = None,
1188
+ relevant_assets_section: str | None = None,
1189
+ ) -> Any:
1190
+ return _shared_build_task_execution_instruction_bundle(
1191
+ plan=plan,
1192
+ task=task,
1193
+ root=root,
1194
+ cfg=cfg,
1195
+ role_model=role_model,
1196
+ mode=mode,
1197
+ yes=yes,
1198
+ deny_write_prefixes=deny_write_prefixes,
1199
+ allow_write_globs=allow_write_globs,
1200
+ non_interactive=non_interactive,
1201
+ verification_enabled=verification_enabled,
1202
+ authoritative_verification_commands=authoritative_verification_commands,
1203
+ api_key=api_key,
1204
+ subagents_enabled=subagents_enabled,
1205
+ leading_sections=leading_sections,
1206
+ relevant_assets_section=relevant_assets_section,
1207
+ managed_execution_startup_headroom=True,
1208
+ )
1209
+
1210
+
1211
+ def _snapshot_runtime_tree(root: Path) -> dict[str, str]:
1212
+ return _shared_snapshot_runtime_tree(root)
1213
+
1214
+
1215
+ __all__ = [name for name in globals() if (not name.startswith("__") or name == "__version__")]