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,942 @@
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 contextvars import ContextVar
6
+
7
+ from .cli_common import *
8
+
9
+ _PICKER_NARROW_OVERRIDE = ContextVar("_PICKER_NARROW_OVERRIDE", default=None)
10
+
11
+
12
+ def _picker_row_styles(*, selected: bool) -> tuple[str, str]:
13
+ if selected:
14
+ return STYLE_SELECTED_LABEL, STYLE_SELECTED_DESC
15
+ return STYLE_DESELECTED_LABEL, STYLE_DESELECTED_DESC
16
+
17
+
18
+ def _picker_hint_text() -> str:
19
+ return "Up/Down navigate / Enter confirm / Esc cancel"
20
+
21
+
22
+ def _picker_label_text(label: str, *, selected: bool) -> str:
23
+ text = str(label or "")
24
+ if not selected:
25
+ return text
26
+ match = re.match(r"^(\s*\d+\))\s*(.+)$", text)
27
+ if match:
28
+ return f"{match.group(1)} ({match.group(2)})"
29
+ return f"({text})"
30
+
31
+
32
+ def _fallback_picker_is_narrow_terminal() -> bool:
33
+ return _terminal_width() < 88
34
+
35
+
36
+ def _picker_is_narrow_terminal() -> bool:
37
+ default = globals().get("_is_narrow_terminal") or _fallback_picker_is_narrow_terminal
38
+ return bool(_patchable("_is_narrow_terminal", default)())
39
+
40
+
41
+ def _forge_picker_row_renderables(*, label: str, desc: str, selected: bool) -> list[Any]:
42
+ from rich.text import Text
43
+
44
+ label_style, desc_style = _picker_row_styles(selected=selected)
45
+ renderables: list[Any] = [
46
+ Text.assemble(
47
+ ("│ ", "bright_black"),
48
+ (_picker_label_text(label, selected=selected), label_style),
49
+ )
50
+ ]
51
+ if str(desc or "").strip():
52
+ renderables.append(
53
+ Text.assemble(
54
+ ("│ ", "bright_black"),
55
+ (str(desc), desc_style),
56
+ )
57
+ )
58
+ return renderables
59
+
60
+
61
+ def _plan_mode_picker_row_renderables(*, label: str, desc: str, selected: bool) -> list[Any]:
62
+ from rich.text import Text
63
+
64
+ label_style, desc_style = _picker_row_styles(selected=selected)
65
+ renderables: list[Any] = [
66
+ Text.assemble(
67
+ (" ", "bright_black"),
68
+ (_picker_label_text(label, selected=selected), label_style),
69
+ )
70
+ ]
71
+ if str(desc or "").strip():
72
+ renderables.append(
73
+ Text.assemble(
74
+ (" ", "bright_black"),
75
+ (str(desc), desc_style),
76
+ )
77
+ )
78
+ return renderables
79
+
80
+
81
+ def _plan_mode_picker_hint_renderable() -> Any:
82
+ from rich.text import Text
83
+
84
+ return Text.assemble(
85
+ (" ", "bright_black"),
86
+ (_picker_hint_text(), "dim"),
87
+ )
88
+
89
+
90
+ def _selectable_options_panel(
91
+ *,
92
+ title: str,
93
+ rows: list[tuple[str, str, str]],
94
+ selected_value: str | None = None,
95
+ interactive: bool = False,
96
+ ) -> Any:
97
+ from rich.console import Group
98
+ from rich.text import Text
99
+
100
+ selected = (selected_value or "").strip().casefold()
101
+ renderables: list[Any] = (
102
+ [Text(str(title or ""), style=STYLE_EMPHASIS)] if str(title or "").strip() else []
103
+ )
104
+ narrow_override = _PICKER_NARROW_OVERRIDE.get()
105
+ is_narrow = (
106
+ bool(narrow_override) if narrow_override is not None else _picker_is_narrow_terminal()
107
+ )
108
+ if is_narrow:
109
+ for value, label, desc in rows:
110
+ row_selected = str(value).strip().casefold() == selected
111
+ label_style, desc_style = _picker_row_styles(selected=row_selected)
112
+ renderables.append(
113
+ Text.assemble(
114
+ (_picker_label_text(label, selected=row_selected), label_style),
115
+ )
116
+ )
117
+ if str(desc or "").strip():
118
+ renderables.append(
119
+ Text.assemble(
120
+ (" ", "bright_black"),
121
+ (str(desc), desc_style),
122
+ )
123
+ )
124
+ if interactive:
125
+ renderables.append(Text(_picker_hint_text(), style="dim"))
126
+ return Group(*renderables)
127
+
128
+ table = _Table(show_header=False, box=None, expand=True, padding=(0, 1), collapse_padding=True)
129
+ table.add_column("option", no_wrap=True, ratio=2)
130
+ table.add_column("description", no_wrap=False, ratio=5, overflow="fold")
131
+ for value, label, desc in rows:
132
+ row_selected = str(value).strip().casefold() == selected
133
+ label_style, desc_style = _picker_row_styles(selected=row_selected)
134
+ table.add_row(
135
+ Text(_picker_label_text(str(label), selected=row_selected), style=label_style),
136
+ Text(str(desc), style=desc_style),
137
+ )
138
+
139
+ renderables.append(table)
140
+ if interactive:
141
+ renderables.append(Text(_picker_hint_text(), style="dim"))
142
+ return Group(*renderables)
143
+
144
+
145
+ def _terminal_dimensions() -> tuple[int, int]:
146
+ size = shutil.get_terminal_size(fallback=(80, 24))
147
+ columns = int(getattr(size, "columns", 80) or 80)
148
+ rows = int(getattr(size, "lines", 24) or 24)
149
+ if columns < 1:
150
+ columns = 1
151
+ if rows < 1:
152
+ rows = 1
153
+ return columns, rows
154
+
155
+
156
+ def _terminal_too_small() -> bool:
157
+ columns, rows = _patchable("_terminal_dimensions", _terminal_dimensions)()
158
+ return columns < _MIN_TERMINAL_COLUMNS or rows < _MIN_TERMINAL_ROWS
159
+
160
+
161
+ def _terminal_too_small_panel() -> Panel:
162
+ from rich.align import Align
163
+ from rich.text import Text
164
+
165
+ columns, rows = _patchable("_terminal_dimensions", _terminal_dimensions)()
166
+ message = Text("Terminal too small - please resize", style="bold yellow")
167
+ details = Text(
168
+ f"Current: {columns}x{rows} Minimum: {_MIN_TERMINAL_COLUMNS}x{_MIN_TERMINAL_ROWS}",
169
+ style="dim",
170
+ )
171
+ content = _table_grid(expand=True)
172
+ content.add_row("")
173
+ content.add_row(Align.center(message))
174
+ content.add_row("")
175
+ content.add_row(Align.center(details))
176
+ content.add_row("")
177
+ return _Panel(content, title="Resize Required", border_style="yellow")
178
+
179
+
180
+ def _clear_terminal_screen(*, console: Console) -> None:
181
+ stream = console.file if getattr(console, "file", None) is not None else sys.stdout
182
+ try:
183
+ stream.write("\x1b[2J\x1b[H")
184
+ stream.flush()
185
+ except Exception: # noqa: BLE001
186
+ pass
187
+
188
+
189
+ @contextmanager
190
+ def _watch_terminal_resize() -> Any:
191
+ if not hasattr(signal, "SIGWINCH"):
192
+ yield lambda: False
193
+ return
194
+
195
+ resized = threading.Event()
196
+ try:
197
+ previous_handler = signal.getsignal(signal.SIGWINCH)
198
+ except Exception: # noqa: BLE001
199
+ yield lambda: False
200
+ return
201
+
202
+ def _on_resize(_signum: int, _frame: Any) -> None:
203
+ resized.set()
204
+
205
+ installed = False
206
+ try:
207
+ signal.signal(signal.SIGWINCH, _on_resize)
208
+ installed = True
209
+ except Exception: # noqa: BLE001
210
+ installed = False
211
+
212
+ def _consume_resize() -> bool:
213
+ if resized.is_set():
214
+ resized.clear()
215
+ return True
216
+ return False
217
+
218
+ try:
219
+ yield _consume_resize
220
+ finally:
221
+ if installed:
222
+ try:
223
+ signal.signal(signal.SIGWINCH, previous_handler)
224
+ except Exception: # noqa: BLE001
225
+ pass
226
+
227
+
228
+ def _read_input_keys_with_timeout(*, input_reader: Any, timeout_s: float) -> list[Any]:
229
+ fileno_fn = getattr(input_reader, "fileno", None)
230
+ if callable(fileno_fn):
231
+ try:
232
+ fd = int(fileno_fn())
233
+ except Exception: # noqa: BLE001
234
+ fd = -1
235
+ if fd >= 0:
236
+ try:
237
+ ready, _w, _x = select.select([fd], [], [], timeout_s)
238
+ except Exception: # noqa: BLE001
239
+ ready = [fd]
240
+ if not ready:
241
+ return []
242
+ try:
243
+ keys = input_reader.read_keys()
244
+ except Exception: # noqa: BLE001
245
+ return []
246
+ if isinstance(keys, list):
247
+ return keys
248
+ return []
249
+
250
+
251
+ def _run_inline_option_selector(
252
+ *,
253
+ console: Console,
254
+ rows: list[tuple[str, str, str]],
255
+ current_value: str,
256
+ panel_builder: Callable[[str | None, bool], Any],
257
+ unavailable_label: str,
258
+ use_alt_screen: bool = True,
259
+ confirm_on_digit: bool = False,
260
+ ) -> tuple[str | None, bool]:
261
+ if _patchable("_is_non_interactive_terminal", _is_non_interactive_terminal)():
262
+ return None, False
263
+ try:
264
+ from prompt_toolkit.input import create_input
265
+ from prompt_toolkit.keys import Keys
266
+ except Exception:
267
+ return None, False
268
+
269
+ if not rows:
270
+ return None, True
271
+
272
+ selected_index = next(
273
+ (idx for idx, (value, _label, _desc) in enumerate(rows) if value == current_value),
274
+ 0,
275
+ )
276
+ if selected_index < 0 or selected_index >= len(rows):
277
+ selected_index = 0
278
+
279
+ def _render_panel() -> Any:
280
+ if _terminal_too_small():
281
+ return _terminal_too_small_panel()
282
+ return panel_builder(rows[selected_index][0], True)
283
+
284
+ narrow_token = _PICKER_NARROW_OVERRIDE.set(_picker_is_narrow_terminal())
285
+ input_reader = None
286
+ try:
287
+ input_reader = create_input()
288
+ with _watch_terminal_resize() as consume_resize:
289
+ with _Live(
290
+ _render_panel(),
291
+ console=console,
292
+ auto_refresh=False,
293
+ transient=not use_alt_screen,
294
+ screen=use_alt_screen,
295
+ ) as live:
296
+ with input_reader.raw_mode():
297
+ while True:
298
+ resized = consume_resize()
299
+ if resized:
300
+ _clear_terminal_screen(console=console)
301
+ live.update(_render_panel(), refresh=True)
302
+
303
+ key_presses = _read_input_keys_with_timeout(
304
+ input_reader=input_reader,
305
+ timeout_s=0.12,
306
+ )
307
+ if not key_presses:
308
+ continue
309
+ panel_updated = False
310
+ for key_press in key_presses:
311
+ key = key_press.key
312
+ raw_data = key_press.data
313
+ if _terminal_too_small():
314
+ if (
315
+ key == Keys.Escape
316
+ or key == Keys.ControlC
317
+ or key
318
+ in {
319
+ "escape",
320
+ "c-c",
321
+ }
322
+ ):
323
+ return None, True
324
+ continue
325
+
326
+ if key == Keys.Up or key == "up":
327
+ selected_index = (selected_index - 1) % len(rows)
328
+ panel_updated = True
329
+ continue
330
+ if key == Keys.Down or key == "down":
331
+ selected_index = (selected_index + 1) % len(rows)
332
+ panel_updated = True
333
+ continue
334
+ digit_key: str | None = None
335
+ if isinstance(key, str) and key.isdigit():
336
+ digit_key = key
337
+ elif isinstance(raw_data, str) and raw_data.isdigit():
338
+ digit_key = raw_data
339
+ if digit_key is not None:
340
+ idx = int(digit_key) - 1
341
+ if 0 <= idx < len(rows):
342
+ selected_index = idx
343
+ if confirm_on_digit:
344
+ return rows[selected_index][0], True
345
+ panel_updated = True
346
+ continue
347
+ if (
348
+ key == Keys.Enter
349
+ or key == Keys.ControlM
350
+ or raw_data in {chr(13), chr(10)}
351
+ ):
352
+ return rows[selected_index][0], True
353
+ if (
354
+ key == Keys.Escape
355
+ or key == Keys.ControlC
356
+ or key
357
+ in {
358
+ "escape",
359
+ "c-c",
360
+ }
361
+ ):
362
+ return None, True
363
+ if panel_updated:
364
+ live.update(_render_panel(), refresh=True)
365
+ except Exception as e: # noqa: BLE001
366
+ console.print(f"[yellow]{unavailable_label} unavailable:[/yellow] {e}")
367
+ return None, False
368
+ finally:
369
+ _PICKER_NARROW_OVERRIDE.reset(narrow_token)
370
+ close = getattr(input_reader, "close", None)
371
+ if callable(close):
372
+ close()
373
+ return None, True
374
+
375
+
376
+ def _guarded_workspace_panel(
377
+ *,
378
+ binding: WorkspaceBinding,
379
+ candidates: tuple[WorkspaceCandidate, ...],
380
+ allow_use_current_action: bool = True,
381
+ selected_action: str | None = None,
382
+ interactive: bool = False,
383
+ ) -> Panel:
384
+ path_label = os.fspath(binding.requested_path)
385
+ title = f"Guarded Workspace: {path_label}"
386
+ return _selectable_options_panel(
387
+ title=title,
388
+ rows=guarded_workspace_action_rows(
389
+ binding=binding,
390
+ candidates=candidates,
391
+ allow_use_current_action=allow_use_current_action,
392
+ ),
393
+ selected_value=selected_action,
394
+ interactive=interactive,
395
+ )
396
+
397
+
398
+ def _select_guarded_workspace_action_interactive(
399
+ *,
400
+ binding: WorkspaceBinding,
401
+ candidates: tuple[WorkspaceCandidate, ...],
402
+ allow_use_current_action: bool = True,
403
+ console: Console,
404
+ ) -> tuple[str | None, bool]:
405
+ rows = guarded_workspace_action_rows(
406
+ binding=binding,
407
+ candidates=candidates,
408
+ allow_use_current_action=allow_use_current_action,
409
+ )
410
+ default_value = rows[1][0] if allow_use_current_action and len(rows) > 1 else rows[0][0]
411
+ return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
412
+ console=console,
413
+ rows=rows,
414
+ current_value=default_value,
415
+ panel_builder=lambda selected, interactive: _guarded_workspace_panel(
416
+ binding=binding,
417
+ candidates=candidates,
418
+ allow_use_current_action=allow_use_current_action,
419
+ selected_action=selected,
420
+ interactive=interactive,
421
+ ),
422
+ unavailable_label="Workspace chooser",
423
+ use_alt_screen=False,
424
+ confirm_on_digit=True,
425
+ )
426
+
427
+
428
+ def _workspace_candidate_panel(
429
+ *,
430
+ base_path: Path,
431
+ candidates: tuple[WorkspaceCandidate, ...],
432
+ selected_path: Path | None = None,
433
+ interactive: bool = False,
434
+ ) -> Panel:
435
+ title = f"Project Folders Under {os.fspath(base_path)}"
436
+ selected_value = os.fspath(selected_path) if selected_path is not None else None
437
+ return _selectable_options_panel(
438
+ title=title,
439
+ rows=workspace_candidate_rows(base_path=base_path, candidates=candidates),
440
+ selected_value=selected_value,
441
+ interactive=interactive,
442
+ )
443
+
444
+
445
+ def _select_workspace_candidate_interactive(
446
+ *,
447
+ base_path: Path,
448
+ candidates: tuple[WorkspaceCandidate, ...],
449
+ console: Console,
450
+ ) -> tuple[Path | None, bool]:
451
+ rows = workspace_candidate_rows(base_path=base_path, candidates=candidates)
452
+ if not rows:
453
+ return None, True
454
+ selected_value, interactive_available = _patchable(
455
+ "_run_inline_option_selector", _run_inline_option_selector
456
+ )(
457
+ console=console,
458
+ rows=rows,
459
+ current_value=rows[0][0],
460
+ panel_builder=lambda selected, interactive: _workspace_candidate_panel(
461
+ base_path=base_path,
462
+ candidates=candidates,
463
+ selected_path=(Path(selected) if selected is not None else None),
464
+ interactive=interactive,
465
+ ),
466
+ unavailable_label="Workspace candidate picker",
467
+ use_alt_screen=False,
468
+ confirm_on_digit=True,
469
+ )
470
+ if selected_value is None:
471
+ return None, interactive_available
472
+ return Path(selected_value), interactive_available
473
+
474
+
475
+ def _guarded_workspace_prompt_text(text: str, *, default: str | None = None) -> str:
476
+ return _patchable("_prompt_text_with_escape", _prompt_text_with_escape)(
477
+ text,
478
+ default=default,
479
+ escape_hint="Esc to go back",
480
+ )
481
+
482
+
483
+ def _prompt_text_with_escape(
484
+ text: str,
485
+ *,
486
+ default: str | None = None,
487
+ escape_hint: str = "Esc to go back",
488
+ ) -> str:
489
+ if _patchable("_is_non_interactive_terminal", _is_non_interactive_terminal)():
490
+ return typer.prompt(text, default=default)
491
+ try:
492
+ from prompt_toolkit import PromptSession
493
+ from prompt_toolkit.key_binding import KeyBindings
494
+ except Exception:
495
+ return typer.prompt(text, default=default)
496
+
497
+ kb = KeyBindings()
498
+
499
+ @kb.add("escape")
500
+ def _go_back(event: Any) -> None:
501
+ event.app.exit(exception=KeyboardInterrupt())
502
+
503
+ prompt_session = PromptSession(key_bindings=kb)
504
+ prompt_label = f"{text} ({escape_hint})" if escape_hint else text
505
+ return prompt_session.prompt(
506
+ f"{prompt_label}: ",
507
+ default=default if default is not None else "",
508
+ )
509
+
510
+
511
+ def _resolve_startup_workspace_binding(
512
+ *,
513
+ requested_path: Path,
514
+ console: Console,
515
+ interactive: bool,
516
+ create_if_missing: bool = False,
517
+ allow_broad_workspace: bool = False,
518
+ source: str = "explicit_path",
519
+ action: str = WorkspaceAction.CHAT,
520
+ ) -> WorkspaceBinding:
521
+ default_select_action = _select_guarded_workspace_action_interactive
522
+ default_select_candidate = _select_workspace_candidate_interactive
523
+ default_prompt = _guarded_workspace_prompt_text
524
+ select_action = _patchable(
525
+ "_select_guarded_workspace_action_interactive",
526
+ default_select_action,
527
+ )
528
+ select_candidate = _patchable(
529
+ "_select_workspace_candidate_interactive",
530
+ default_select_candidate,
531
+ )
532
+ prompt = _patchable("_guarded_workspace_prompt_text", default_prompt)
533
+ # When the full-screen TUI is active, render the guarded-workspace steps with
534
+ # the TUI's own picker/prompt chrome instead of the classic inline ones, so the
535
+ # startup screen matches the rest of the experience. The binding *logic* is
536
+ # unchanged — only these display callbacks swap. Any import/terminal failure
537
+ # falls back to the classic selectors above.
538
+ if interactive:
539
+ try:
540
+ from ..tui import is_tui_enabled as _tui_enabled
541
+
542
+ if _tui_enabled():
543
+ from ..tui.workspace_guard import (
544
+ select_guarded_workspace_action as _tui_select_action,
545
+ select_workspace_candidate as _tui_select_candidate,
546
+ workspace_guard_prompt_text as _tui_prompt,
547
+ )
548
+
549
+ if select_action is default_select_action:
550
+ select_action = _tui_select_action
551
+ if select_candidate is default_select_candidate:
552
+ select_candidate = _tui_select_candidate
553
+ if prompt is default_prompt:
554
+ prompt = _tui_prompt
555
+ except Exception:
556
+ pass
557
+ return _resolve_startup_workspace_binding_impl(
558
+ requested_path=requested_path,
559
+ interactive=interactive,
560
+ create_if_missing=create_if_missing,
561
+ allow_broad_workspace=allow_broad_workspace,
562
+ source=source,
563
+ action=action,
564
+ console=console,
565
+ select_action_interactive=select_action,
566
+ select_candidate_interactive=select_candidate,
567
+ prompt_text=prompt,
568
+ )
569
+
570
+
571
+ def _chat_mode_rows() -> list[tuple[str, str, str]]:
572
+ return [
573
+ ("review", "1) safe (review)", "Asks before file writes and shell commands."),
574
+ (
575
+ "auto",
576
+ "2) fast (auto)",
577
+ "Auto-runs normal operations; approval-gated sensitive actions still ask.",
578
+ ),
579
+ (
580
+ "readonly",
581
+ "3) read (readonly)",
582
+ "Read-only analysis with inspection-only tools; no writes, shell, or verification.",
583
+ ),
584
+ ("fullaccess", "4) full (fullaccess)", "No write/shell safety guards or prompts."),
585
+ ]
586
+
587
+
588
+ def _chat_persona_rows(custom: Any = None) -> list[tuple[str, str, str]]:
589
+ rows = [
590
+ ("code", "1) code", "Implementation work; keeps your execution mode."),
591
+ ("architect", "2) architect", "Plans and designs; writes markdown documents only."),
592
+ ("ask", "3) ask", "Read-only questions with inspection tools."),
593
+ ("debug", "4) debug", "Reproduce-before-fix; keeps your execution mode."),
594
+ ]
595
+ if custom:
596
+ index = len(rows)
597
+ for name in sorted(custom):
598
+ definition = custom[name]
599
+ index += 1
600
+ rows.append(
601
+ (
602
+ str(name),
603
+ f"{index}) {name} (custom)",
604
+ str(getattr(definition, "description", "") or "Custom persona."),
605
+ )
606
+ )
607
+ return rows
608
+
609
+
610
+ def _chat_persona_panel(
611
+ *,
612
+ current_persona: str,
613
+ selected_persona: str | None = None,
614
+ interactive: bool = False,
615
+ custom: Any = None,
616
+ ) -> Panel:
617
+ current = current_persona.strip().lower() or "code"
618
+ return _selectable_options_panel(
619
+ title=f"Persona Options (current: {current})",
620
+ rows=_chat_persona_rows(custom=custom),
621
+ selected_value=selected_persona,
622
+ interactive=interactive,
623
+ )
624
+
625
+
626
+ def _select_chat_persona_interactive(
627
+ *,
628
+ current_persona: str,
629
+ console: Console,
630
+ custom: Any = None,
631
+ ) -> tuple[str | None, bool]:
632
+ normalized_current = current_persona.strip().lower() or "code"
633
+ return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
634
+ console=console,
635
+ rows=_chat_persona_rows(custom=custom),
636
+ current_value=normalized_current,
637
+ panel_builder=lambda selected, interactive: _chat_persona_panel(
638
+ current_persona=current_persona,
639
+ selected_persona=selected,
640
+ interactive=interactive,
641
+ ),
642
+ unavailable_label="Persona picker",
643
+ )
644
+
645
+
646
+ def _select_chat_mode_interactive(
647
+ *,
648
+ current_mode: str,
649
+ console: Console,
650
+ ) -> tuple[str | None, bool]:
651
+ normalized_current = current_mode.strip().lower()
652
+ if normalized_current not in _CHAT_MODES:
653
+ normalized_current = "review"
654
+
655
+ rows = _chat_mode_rows()
656
+ return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
657
+ console=console,
658
+ rows=rows,
659
+ current_value=normalized_current,
660
+ panel_builder=lambda selected, interactive: _chat_mode_panel(
661
+ current_mode=current_mode,
662
+ selected_mode=selected,
663
+ interactive=interactive,
664
+ ),
665
+ unavailable_label="Mode picker",
666
+ )
667
+
668
+
669
+ def _chat_mode_display(mode: str) -> str:
670
+ normalized = mode.strip().lower()
671
+ if normalized == "review":
672
+ return "safe (review)"
673
+ if normalized == "auto":
674
+ return "fast (auto)"
675
+ if normalized == "readonly":
676
+ return "read (readonly)"
677
+ if normalized == "fullaccess":
678
+ return "full (fullaccess)"
679
+ return normalized or "unknown"
680
+
681
+
682
+ def _print_fullaccess_mode_warning(*, console: Console) -> None:
683
+ console.print(
684
+ "[yellow]Warning:[/yellow] full (fullaccess) disables write/shell safety guards "
685
+ "and approval prompts."
686
+ )
687
+
688
+
689
+ def _chat_mode_panel(
690
+ *,
691
+ current_mode: str,
692
+ selected_mode: str | None = None,
693
+ interactive: bool = False,
694
+ ) -> Panel:
695
+ current = current_mode.strip().lower()
696
+ title = f"Mode Options (current: {_chat_mode_display(current)})"
697
+ return _selectable_options_panel(
698
+ title=title,
699
+ rows=_chat_mode_rows(),
700
+ selected_value=selected_mode,
701
+ interactive=interactive,
702
+ )
703
+
704
+
705
+ def _chat_trace_rows() -> list[tuple[str, str, str]]:
706
+ return [
707
+ ("off", "1) off", "Hide reasoning summaries and routine activity."),
708
+ (
709
+ "compact",
710
+ "2) compact",
711
+ "Collapsed provider summaries and key activity (recommended).",
712
+ ),
713
+ (
714
+ "full",
715
+ "3) full",
716
+ "Expanded provider summaries and detailed tool activity; never raw thinking.",
717
+ ),
718
+ ]
719
+
720
+
721
+ def _chat_trace_panel(
722
+ *,
723
+ current_level: str,
724
+ selected_level: str | None = None,
725
+ interactive: bool = False,
726
+ ) -> Panel:
727
+ current = _resolve_trace_level(current_level) or "compact"
728
+ title = f"Trace Options (current: {current})"
729
+ return _selectable_options_panel(
730
+ title=title,
731
+ rows=_chat_trace_rows(),
732
+ selected_value=selected_level,
733
+ interactive=interactive,
734
+ )
735
+
736
+
737
+ def _select_chat_trace_interactive(
738
+ *,
739
+ current_level: str,
740
+ console: Console,
741
+ ) -> tuple[str | None, bool]:
742
+ normalized_current = _resolve_trace_level(current_level) or "compact"
743
+ rows = _chat_trace_rows()
744
+ return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
745
+ console=console,
746
+ rows=rows,
747
+ current_value=normalized_current,
748
+ panel_builder=lambda selected, interactive: _chat_trace_panel(
749
+ current_level=normalized_current,
750
+ selected_level=selected,
751
+ interactive=interactive,
752
+ ),
753
+ unavailable_label="Trace picker",
754
+ )
755
+
756
+
757
+ def _chat_usage_hud_rows() -> list[tuple[str, str, str]]:
758
+ return [
759
+ ("on", "1) on", "Show usage summary continuously in the toolbar."),
760
+ ("off", "2) off", "Hide usage summary from the toolbar."),
761
+ ("status", "3) status", "Show current HUD setting without changing it."),
762
+ ]
763
+
764
+
765
+ def _chat_usage_hud_panel(
766
+ *,
767
+ current_enabled: bool,
768
+ selected_action: str | None = None,
769
+ interactive: bool = False,
770
+ ) -> Panel:
771
+ current = "on" if current_enabled else "off"
772
+ title = f"Usage HUD Options (current: {current})"
773
+ return _selectable_options_panel(
774
+ title=title,
775
+ rows=_chat_usage_hud_rows(),
776
+ selected_value=selected_action,
777
+ interactive=interactive,
778
+ )
779
+
780
+
781
+ def _select_chat_usage_hud_interactive(
782
+ *,
783
+ current_enabled: bool,
784
+ console: Console,
785
+ ) -> tuple[str | None, bool]:
786
+ rows = _chat_usage_hud_rows()
787
+ return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
788
+ console=console,
789
+ rows=rows,
790
+ current_value="on" if current_enabled else "off",
791
+ panel_builder=lambda selected, interactive: _chat_usage_hud_panel(
792
+ current_enabled=current_enabled,
793
+ selected_action=selected,
794
+ interactive=interactive,
795
+ ),
796
+ unavailable_label="Usage HUD picker",
797
+ )
798
+
799
+
800
+ def _forge_assistant_rows() -> list[tuple[str, str, str]]:
801
+ return [
802
+ ("on", "1) on", "Enable planner assistant suggestions."),
803
+ ("off", "2) off", "Disable planner assistant suggestions."),
804
+ ("status", "3) status", "Show assistant status without changing it."),
805
+ ]
806
+
807
+
808
+ def _forge_assistant_panel(
809
+ *,
810
+ enabled: bool,
811
+ selected_action: str | None = None,
812
+ interactive: bool = False,
813
+ ) -> Panel:
814
+ current = "ON" if enabled else "OFF"
815
+ title = f"Planner Assistant (current: {current})"
816
+ return _selectable_options_panel(
817
+ title=title,
818
+ rows=_forge_assistant_rows(),
819
+ selected_value=selected_action,
820
+ interactive=interactive,
821
+ )
822
+
823
+
824
+ def _select_forge_assistant_interactive(
825
+ *,
826
+ enabled: bool,
827
+ console: Console,
828
+ ) -> tuple[str | None, bool]:
829
+ rows = _forge_assistant_rows()
830
+ return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
831
+ console=console,
832
+ rows=rows,
833
+ current_value="on" if enabled else "off",
834
+ panel_builder=lambda selected, interactive: _forge_assistant_panel(
835
+ enabled=enabled,
836
+ selected_action=selected,
837
+ interactive=interactive,
838
+ ),
839
+ unavailable_label="Assistant picker",
840
+ )
841
+
842
+
843
+ def _forge_entry_plan_assistant_rows() -> list[tuple[str, str, str]]:
844
+ return [
845
+ ("yes", "1) Yes", "Enable planner assistant now."),
846
+ ("no", "2) No", "Keep assistant off and provide plan text manually."),
847
+ ]
848
+
849
+
850
+ def _forge_entry_plan_assistant_panel(
851
+ *,
852
+ selected_action: str | None = None,
853
+ interactive: bool = False,
854
+ ) -> Panel:
855
+ return _selectable_options_panel(
856
+ title="Do you want Plan Assistant?",
857
+ rows=_forge_entry_plan_assistant_rows(),
858
+ selected_value=selected_action,
859
+ interactive=interactive,
860
+ )
861
+
862
+
863
+ def _select_forge_entry_plan_assistant_interactive(*, console: Console) -> tuple[str | None, bool]:
864
+ rows = _forge_entry_plan_assistant_rows()
865
+ return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
866
+ console=console,
867
+ rows=rows,
868
+ current_value="yes",
869
+ panel_builder=lambda selected, interactive: _forge_entry_plan_assistant_panel(
870
+ selected_action=selected,
871
+ interactive=interactive,
872
+ ),
873
+ unavailable_label="Plan assistant chooser",
874
+ )
875
+
876
+
877
+ def _prompt_forge_entry_plan_assistant(*, console: Console) -> bool:
878
+ env_override = env_get("ALYSIS_FORGE_PLAN_ASSISTANT")
879
+ if env_override is not None:
880
+ parsed_override = _parse_bool_text(env_override)
881
+ if parsed_override is None:
882
+ console.print(
883
+ "[yellow]Invalid ALYSIS_FORGE_PLAN_ASSISTANT value; defaulting to OFF.[/yellow]"
884
+ )
885
+ return False
886
+ return parsed_override
887
+
888
+ if _patchable("_is_non_interactive_terminal", _is_non_interactive_terminal)():
889
+ return False
890
+
891
+ action, picker_available = _select_forge_entry_plan_assistant_interactive(console=console)
892
+ if picker_available:
893
+ return action == "yes"
894
+
895
+ try:
896
+ fallback = typer.prompt("Do you want Plan Assistant? [y/N]", default="n").strip()
897
+ except (EOFError, KeyboardInterrupt):
898
+ console.print("")
899
+ return False
900
+ parsed = _parse_bool_text(fallback)
901
+ if parsed is None:
902
+ console.print("[yellow]Invalid selection; defaulting to No.[/yellow]")
903
+ return False
904
+ return parsed
905
+
906
+
907
+ def _forge_plan_command_rows() -> list[tuple[str, str, str]]:
908
+ return [
909
+ ("tasks", "1) show summary", "Show the current plan summary and task list."),
910
+ ("markdown", "2) markdown", "Render PLAN.md for the current run."),
911
+ ("edit", "3) edit", "Edit plan.json and reload it into Forge."),
912
+ ]
913
+
914
+
915
+ def _forge_plan_command_panel(
916
+ *,
917
+ selected_action: str | None = None,
918
+ interactive: bool = False,
919
+ ) -> Panel:
920
+ return _selectable_options_panel(
921
+ title="Forge Plan",
922
+ rows=_forge_plan_command_rows(),
923
+ selected_value=selected_action,
924
+ interactive=interactive,
925
+ )
926
+
927
+
928
+ def _select_forge_plan_command_interactive(*, console: Console) -> tuple[str | None, bool]:
929
+ rows = _forge_plan_command_rows()
930
+ return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
931
+ console=console,
932
+ rows=rows,
933
+ current_value="tasks",
934
+ panel_builder=lambda selected, interactive: _forge_plan_command_panel(
935
+ selected_action=selected,
936
+ interactive=interactive,
937
+ ),
938
+ unavailable_label="Forge plan command picker",
939
+ )
940
+
941
+
942
+ __all__ = [name for name in globals() if (not name.startswith("__") or name == "__version__")]