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,1118 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Annotated, Any
10
+
11
+ import typer
12
+
13
+ from ... import __version__
14
+ from ...auth_diagnostics import auth_doctor_payload
15
+ from ...build_identity import decide_clean_build, version_line
16
+ from ...config import (
17
+ AppConfig,
18
+ ConfigError,
19
+ config_path,
20
+ load_config,
21
+ resolve_model_access_api_key,
22
+ resolve_web_search_policy,
23
+ )
24
+ from ...provider_diagnostics import (
25
+ build_provider_diagnostics,
26
+ probe_reasoning_suppression_live,
27
+ validate_active_provider_live,
28
+ validate_web_search_live,
29
+ )
30
+ from ...provider_telemetry import (
31
+ diagnostic_bundle_payload,
32
+ last_provider_call_summary,
33
+ last_web_search_summary,
34
+ )
35
+ from ...sandbox_doctor import diagnose_sandbox
36
+ from ...skills import resolve_skills_enabled
37
+ from ...step_budget import normalize_step_budget_policy
38
+ from ...tools.availability import get_tool_availability
39
+ from ...tools.registry import iter_builtin_tool_metadata
40
+ from ...tools.web_search import resolve_web_search_runtime_status
41
+ from ..assets_cli import assets_app as forge_assets_app
42
+ from . import _patchable
43
+ from ._shared import (
44
+ Mode,
45
+ _console,
46
+ _Table,
47
+ )
48
+ from .auth import JSON_OPTION_HELP as _JSON_OPTION_HELP
49
+ from .auth import _status_payload as _auth_status_payload
50
+ from .auth import auth_app
51
+ from .config import config_app
52
+ from .conventions import conventions_app
53
+ from .extensions import ext_app
54
+ from .forge import forge_app
55
+ from .hooks import hooks_app
56
+ from .ide_bridge import ide_bridge_app
57
+ from .mcp import mcp_app, mcp_auth_app, mcp_prompts_app
58
+ from .profile import profile_app
59
+ from .report import report_app
60
+ from .sandbox import sandbox_app
61
+ from .server import server_app
62
+ from .sessions import sessions_app
63
+ from .skills import skill_app
64
+ from .tools import tool_app
65
+ from .update import (
66
+ _BACKGROUND_UPDATE_SUBCOMMANDS,
67
+ _cached_update_status_summary,
68
+ _start_background_update_check,
69
+ maybe_prompt_update_at_startup,
70
+ update_app,
71
+ )
72
+
73
+ if TYPE_CHECKING:
74
+ from rich.table import Table
75
+
76
+
77
+ def _cli_module() -> Any:
78
+ module = sys.modules.get("alysis_code.cli")
79
+ if module is not None:
80
+ return module
81
+ from ... import cli
82
+
83
+ return cli
84
+
85
+
86
+ app = typer.Typer(add_completion=False, help="Local CLI coding agent (multi-provider).")
87
+ app.add_typer(auth_app, name="auth")
88
+ app.add_typer(config_app, name="config")
89
+ app.add_typer(profile_app, name="profile")
90
+ app.add_typer(update_app, name="update")
91
+ app.add_typer(sessions_app, name="sessions")
92
+ app.add_typer(forge_app, name="forge")
93
+ forge_app.add_typer(forge_assets_app, name="assets")
94
+ app.add_typer(server_app, name="server")
95
+ app.add_typer(ext_app, name="ext")
96
+ app.add_typer(report_app, name="report")
97
+ app.add_typer(mcp_app, name="mcp")
98
+ app.add_typer(skill_app, name="skill")
99
+ app.add_typer(conventions_app, name="conventions")
100
+ app.add_typer(tool_app, name="tool")
101
+ app.add_typer(hooks_app, name="hooks")
102
+ app.add_typer(sandbox_app, name="sandbox")
103
+ app.add_typer(ide_bridge_app, name="ide-bridge")
104
+ mcp_app.add_typer(mcp_prompts_app, name="prompts")
105
+ mcp_app.add_typer(mcp_auth_app, name="auth")
106
+
107
+
108
+ #: A build that cannot identify itself is a configuration problem, not a task
109
+ #: failure, so it gets its own code rather than the generic 1.
110
+ UNIDENTIFIABLE_BUILD_EXIT_CODE = 2
111
+
112
+
113
+ def _show_version(value: bool) -> None:
114
+ if not value:
115
+ return
116
+ # Version, commit, build timestamp and dirty flag on one line, version
117
+ # first. Three behaviourally different builds once all reported "0.9.8",
118
+ # which made every score they produced unattributable; the bare version is
119
+ # kept as the leading token because the VS Code extension, the managed-CLI
120
+ # smoke test and the release distribution validator all read this output.
121
+ typer.echo(version_line(__version__))
122
+ raise typer.Exit()
123
+
124
+
125
+ def _enforce_clean_build(flag: bool) -> None:
126
+ """Refuse to start when the build cannot prove what it is.
127
+
128
+ Benchmark harnesses set ``ALYSIS_REQUIRE_CLEAN_BUILD=1`` so a campaign
129
+ cannot silently run against an unpinned or locally-modified build. One
130
+ earlier campaign did exactly that -- it ran against "latest main" -- and
131
+ none of its results can now be tied to a source tree.
132
+
133
+ Deliberately after ``--version``, which is eager: diagnosing a refusal
134
+ starts by asking the binary what it thinks it is, and that has to keep
135
+ working.
136
+ """
137
+ decision = decide_clean_build(flag=flag)
138
+ if decision.allowed:
139
+ return
140
+ typer.echo(f"alysis: {decision.message}", err=True)
141
+ raise typer.Exit(code=UNIDENTIFIABLE_BUILD_EXIT_CODE)
142
+
143
+
144
+ @app.callback(invoke_without_command=True)
145
+ def main(
146
+ ctx: typer.Context,
147
+ version: Annotated[
148
+ bool,
149
+ typer.Option(
150
+ "--version",
151
+ callback=_show_version,
152
+ is_eager=True,
153
+ help="Show the installed Alysis Code version, commit and build stamp, then exit.",
154
+ ),
155
+ ] = False,
156
+ require_clean_build: Annotated[
157
+ bool,
158
+ typer.Option(
159
+ "--require-clean-build",
160
+ help=(
161
+ "Refuse to run unless this build recorded a commit and a clean tree at "
162
+ "build time. Also enabled by ALYSIS_REQUIRE_CLEAN_BUILD=1."
163
+ ),
164
+ ),
165
+ ] = False,
166
+ ) -> None:
167
+ # Before anything else, and before the subcommand dispatch below, so that
168
+ # `alysis run ...` under a benchmark harness is refused just as the bare
169
+ # invocation is.
170
+ _enforce_clean_build(require_clean_build)
171
+ cli = _cli_module()
172
+ if ctx.invoked_subcommand is not None:
173
+ if ctx.invoked_subcommand in _BACKGROUND_UPDATE_SUBCOMMANDS:
174
+ _start_background_update_check()
175
+ return
176
+ console = cli._console()
177
+ if cli._is_interactive_terminal():
178
+ _start_background_update_check()
179
+ if cli._is_non_interactive_terminal():
180
+ console.print(cli._home_panel())
181
+ return
182
+ maybe_prompt_update_at_startup()
183
+ if not cli._home_prompt_enabled():
184
+ if not cli._maybe_run_first_run_setup_wizard():
185
+ return
186
+ cli._maybe_run_startup_config_menu()
187
+ cli._run_default_chat_action()
188
+ return
189
+ console.print(cli._home_panel())
190
+ try:
191
+ action = (
192
+ typer.prompt(
193
+ "Action [1=chat|2=run|3=setup|4=doctor|5=plan|6=quit]",
194
+ default="1",
195
+ )
196
+ .strip()
197
+ .lower()
198
+ )
199
+ except (EOFError, KeyboardInterrupt):
200
+ console.print("")
201
+ return
202
+ action = cli._HOME_ACTION_ALIASES.get(action, action)
203
+
204
+ if action in {"quit", "q", "exit"}:
205
+ return
206
+ if action in {"chat", "c"}:
207
+ cli._maybe_run_startup_config_menu()
208
+ cli._run_default_chat_action()
209
+ return
210
+ if action in {"run", "r"}:
211
+ instruction = typer.prompt("Instruction").strip()
212
+ if instruction:
213
+ cli._run_default_run_action(instruction)
214
+ return
215
+ if action in {"setup", "s"}:
216
+ cli.setup()
217
+ return
218
+ if action in {"doctor", "d"}:
219
+ cli.doctor()
220
+ return
221
+ if action in {"plan", "p"}:
222
+ cli.forge_plan(path=Path("."))
223
+ return
224
+ console.print("[yellow]Unknown action.[/yellow] Run `alysis --help`.")
225
+
226
+
227
+ def _require_active_subscription_ready(
228
+ *,
229
+ model: str | None,
230
+ base_url: str | None,
231
+ require_ready: bool = True,
232
+ ) -> None:
233
+ _cli_module()._require_active_subscription_ready(
234
+ model=model,
235
+ base_url=base_url,
236
+ require_ready=require_ready,
237
+ )
238
+
239
+
240
+ @app.command()
241
+ def run(
242
+ ctx: typer.Context = None,
243
+ instruction: str = typer.Argument(..., help="What you want the agent to do."),
244
+ path: Path = typer.Option(Path("."), "--path", help="Working directory/root."),
245
+ create_path: bool = typer.Option(
246
+ False,
247
+ "--create-path",
248
+ help="Create --path if it does not exist before binding the workspace.",
249
+ ),
250
+ allow_broad_workspace: bool = typer.Option(
251
+ False,
252
+ "--allow-broad-workspace",
253
+ help="Allow guarded broad workspaces in non-interactive startup flows.",
254
+ ),
255
+ image: list[Path] | None = typer.Option(
256
+ None,
257
+ "--image",
258
+ help="Attach image path(s). Repeat --image for multiple files.",
259
+ ),
260
+ mode: Mode | None = typer.Option(None, "--mode", help="Mode override."),
261
+ persona: str | None = typer.Option(
262
+ None,
263
+ "--persona",
264
+ help=("Persona: code, architect, ask, debug, or a custom persona from .alysis_personas."),
265
+ ),
266
+ model: str | None = typer.Option(None, "--model", help="Model override."),
267
+ base_url: str | None = typer.Option(None, "--base-url", help="Base URL override."),
268
+ temperature: float | None = typer.Option(None, "--temperature", help="Sampling temperature."),
269
+ stream: bool | None = typer.Option(
270
+ None,
271
+ "--stream/--no-stream",
272
+ help="Enable streamed assistant output.",
273
+ ),
274
+ max_steps: int | None = typer.Option(
275
+ None,
276
+ "--max-steps",
277
+ help="Optional safety limit on agent iterations.",
278
+ ),
279
+ subagents: bool | None = typer.Option(
280
+ None,
281
+ "--subagents/--no-subagents",
282
+ help="Enable or disable subagent delegation for this session.",
283
+ ),
284
+ no_log: bool = typer.Option(False, "--no-log", help="Disable JSONL session logging."),
285
+ verify_cmd: list[str] | None = typer.Option(
286
+ None,
287
+ "--verify-cmd",
288
+ help="Override verification command for this run (repeatable).",
289
+ ),
290
+ api_key_env: str | None = typer.Option(
291
+ None,
292
+ "--api-key-env",
293
+ help=(
294
+ "Read API key from this environment variable (overrides ALYSIS_API_KEY/OPENAI_API_KEY)."
295
+ ),
296
+ ),
297
+ api_key_stdin: bool = typer.Option(
298
+ False,
299
+ "--api-key-stdin",
300
+ help="Prompt for API key (hidden input). Key is kept in memory for this run only.",
301
+ ),
302
+ api_key: str | None = typer.Option(
303
+ None,
304
+ "--api-key",
305
+ help=(
306
+ "UNSAFE: Provide API key via CLI argument (may leak via shell history / process list). "
307
+ "Prefer --api-key-stdin or --api-key-env."
308
+ ),
309
+ ),
310
+ yes: bool = typer.Option(
311
+ False,
312
+ "--yes",
313
+ help="In auto mode, skip confirmations for sensitive commands (hard blocks still apply).",
314
+ ),
315
+ benchmark: bool = typer.Option(
316
+ False,
317
+ "--benchmark",
318
+ help=(
319
+ "Use the raw benchmark/autonomy run profile: auto mode, code-only routing, "
320
+ "longer fixed step budget, and no subagents/skills/custom tools/web by default."
321
+ ),
322
+ ),
323
+ deadline_seconds: float | None = typer.Option(
324
+ None,
325
+ "--deadline-seconds",
326
+ help=(
327
+ "Stop this one-shot run after the given invocation-wide wall-clock seconds. "
328
+ "Defaults to 3600 (60 min); use --no-deadline to run unbounded."
329
+ ),
330
+ ),
331
+ no_deadline: bool = typer.Option(
332
+ False,
333
+ "--no-deadline",
334
+ help="Run without any wall-clock budget, overriding the default one-shot deadline.",
335
+ ),
336
+ require_deadline: bool = typer.Option(
337
+ False,
338
+ "--require-deadline",
339
+ help=(
340
+ "Require a finite one-shot run deadline from CLI, environment, or config. "
341
+ "Intended for managed hosts."
342
+ ),
343
+ ),
344
+ diagnostic_log: Path | None = typer.Option(
345
+ None,
346
+ "--diagnostic-log",
347
+ help="Append minimal crash-safe diagnostic events to this JSONL path.",
348
+ ),
349
+ ) -> None:
350
+ if not instruction.strip():
351
+ typer.echo("Missing argument 'INSTRUCTION': instruction is empty.", err=True)
352
+ raise typer.Exit(1)
353
+ _require_active_subscription_ready(model=model, base_url=base_url)
354
+
355
+ from ..chat import run_impl
356
+
357
+ return run_impl(
358
+ _cli_module(),
359
+ instruction,
360
+ path,
361
+ create_path,
362
+ allow_broad_workspace,
363
+ image,
364
+ mode,
365
+ persona,
366
+ model,
367
+ base_url,
368
+ temperature,
369
+ stream,
370
+ max_steps,
371
+ subagents,
372
+ no_log,
373
+ verify_cmd,
374
+ api_key_env,
375
+ api_key_stdin,
376
+ api_key,
377
+ yes,
378
+ benchmark,
379
+ deadline_seconds,
380
+ no_deadline,
381
+ require_deadline,
382
+ diagnostic_log,
383
+ cli_ctx=ctx,
384
+ )
385
+
386
+
387
+ @app.command()
388
+ def chat(
389
+ ctx: typer.Context = None,
390
+ path: Path = typer.Option(Path("."), "--path", help="Working directory/root."),
391
+ create_path: bool = typer.Option(
392
+ False,
393
+ "--create-path",
394
+ help="Create --path if it does not exist before binding the workspace.",
395
+ ),
396
+ allow_broad_workspace: bool = typer.Option(
397
+ False,
398
+ "--allow-broad-workspace",
399
+ help="Allow guarded broad workspaces in non-interactive startup flows.",
400
+ ),
401
+ image: list[Path] | None = typer.Option(
402
+ None,
403
+ "--image",
404
+ help="Queue image path(s) for the next message. Repeat --image for multiple files.",
405
+ ),
406
+ mode: Mode | None = typer.Option(None, "--mode", help="Mode override."),
407
+ persona: str | None = typer.Option(
408
+ None,
409
+ "--persona",
410
+ help=("Persona: code, architect, ask, debug, or a custom persona from .alysis_personas."),
411
+ ),
412
+ model: str | None = typer.Option(None, "--model", help="Model override."),
413
+ base_url: str | None = typer.Option(None, "--base-url", help="Base URL override."),
414
+ temperature: float | None = typer.Option(None, "--temperature", help="Sampling temperature."),
415
+ stream: bool | None = typer.Option(
416
+ None,
417
+ "--stream/--no-stream",
418
+ help="Enable streamed assistant output.",
419
+ ),
420
+ max_steps: int | None = typer.Option(
421
+ None,
422
+ "--max-steps",
423
+ help="Optional safety limit on agent iterations for each user turn.",
424
+ ),
425
+ subagents: bool | None = typer.Option(
426
+ None,
427
+ "--subagents/--no-subagents",
428
+ help="Enable or disable subagent delegation for this session.",
429
+ ),
430
+ no_log: bool = typer.Option(False, "--no-log", help="Disable JSONL session logging."),
431
+ verify_cmd: list[str] | None = typer.Option(
432
+ None,
433
+ "--verify-cmd",
434
+ help="Override verification command for this chat session (repeatable).",
435
+ ),
436
+ api_key_env: str | None = typer.Option(
437
+ None,
438
+ "--api-key-env",
439
+ help=(
440
+ "Read API key from this environment variable (overrides ALYSIS_API_KEY/OPENAI_API_KEY)."
441
+ ),
442
+ ),
443
+ api_key_stdin: bool = typer.Option(
444
+ False,
445
+ "--api-key-stdin",
446
+ help="Prompt for API key (hidden input). Key is kept in memory for this run only.",
447
+ ),
448
+ api_key: str | None = typer.Option(
449
+ None,
450
+ "--api-key",
451
+ help=(
452
+ "UNSAFE: Provide API key via CLI argument (may leak via shell history / process list). "
453
+ "Prefer --api-key-stdin or --api-key-env."
454
+ ),
455
+ ),
456
+ yes: bool = typer.Option(
457
+ False,
458
+ "--yes",
459
+ help="In auto mode, skip confirmations for sensitive commands.",
460
+ ),
461
+ diagnostic_log: Path | None = typer.Option(
462
+ None,
463
+ "--diagnostic-log",
464
+ help="Append minimal crash-safe diagnostic events to this JSONL path.",
465
+ ),
466
+ ) -> None:
467
+ _require_active_subscription_ready(
468
+ model=model,
469
+ base_url=base_url,
470
+ require_ready=False,
471
+ )
472
+
473
+ from ..chat import chat_impl
474
+
475
+ return chat_impl(
476
+ _cli_module(),
477
+ path,
478
+ create_path,
479
+ allow_broad_workspace,
480
+ image,
481
+ mode,
482
+ persona,
483
+ model,
484
+ base_url,
485
+ temperature,
486
+ stream,
487
+ max_steps,
488
+ subagents,
489
+ no_log,
490
+ verify_cmd,
491
+ api_key_env,
492
+ api_key_stdin,
493
+ api_key,
494
+ yes,
495
+ diagnostic_log,
496
+ cli_ctx=ctx,
497
+ )
498
+
499
+
500
+ def _doctor_table(cfg: AppConfig) -> Table:
501
+ table = _Table(title="alysis doctor")
502
+ table.add_column("check")
503
+ table.add_column("result")
504
+ api_key = _cli_module()._resolved_api_key_value()
505
+
506
+ table.add_row("python", sys.version.split()[0])
507
+
508
+ def _cmd_exists(cmd: str) -> bool:
509
+ return shutil.which(cmd) is not None
510
+
511
+ table.add_row("git", "ok" if _cmd_exists("git") else "missing")
512
+ table.add_row(
513
+ "rg",
514
+ "ok" if _cmd_exists("rg") else "missing (search fallback will be slower)",
515
+ )
516
+ table.add_row("config_path", os.fspath(config_path()))
517
+ table.add_row(
518
+ "model_set",
519
+ "yes" if bool(cfg.model) else "no (run: alysis config set model <MODEL>)",
520
+ )
521
+ table.add_row(
522
+ "api_key_set",
523
+ (
524
+ f"yes ({_cli_module()._api_key_source_label(api_key.source)})"
525
+ if api_key.key
526
+ else "no (run: alysis config set-api-key)"
527
+ ),
528
+ )
529
+ table.add_row("base_url", cfg.base_url)
530
+ table.add_row("update", _cached_update_status_summary(cfg))
531
+ web_search_status = resolve_web_search_runtime_status(cfg=cfg, api_key=api_key.key)
532
+ web_search_policy = resolve_web_search_policy(cfg)
533
+ web_search_label = (
534
+ "policy-disabled" if web_search_policy == "off" else web_search_status.availability_label
535
+ )
536
+ web_search_setup = (
537
+ "Set `alysis config set web_search_policy auto` to expose search to the model."
538
+ if web_search_policy == "off"
539
+ else web_search_status.setup_hint
540
+ )
541
+ table.add_row("web_search", web_search_label)
542
+ table.add_row("web_search_policy", web_search_policy)
543
+ table.add_row("web_search_provider", web_search_status.provider or "(none)")
544
+ table.add_row("web_search_setup", web_search_setup)
545
+ try:
546
+ sandbox_status = _patchable("diagnose_sandbox", diagnose_sandbox)(
547
+ cfg, include_smoke=False, include_server_image=False
548
+ )
549
+ sandbox_label = (
550
+ "ready" if sandbox_status.ready else "not ready (run: alysis doctor sandbox)"
551
+ )
552
+ table.add_row("sandbox", sandbox_label)
553
+ table.add_row("sandbox_backend", sandbox_status.selected_backend or "(none)")
554
+ except ConfigError as exc:
555
+ table.add_row("sandbox", f"config error ({exc})")
556
+ table.add_row("temperature", str(cfg.temperature))
557
+ table.add_row("coding_temperature", str(cfg.coding_temperature))
558
+ table.add_row("chat_temperature", str(cfg.chat_temperature))
559
+ execution_policy = normalize_step_budget_policy(cfg.step_budget_policy)
560
+ table.add_row("execution_policy", execution_policy)
561
+ if execution_policy == "autonomous":
562
+ table.add_row("step_limit", "unlimited")
563
+ else:
564
+ table.add_row("chat_step_limit", str(cfg.max_steps))
565
+ table.add_row("task_step_limit", str(cfg.task_max_steps))
566
+ table.add_row("subagent_step_limit", str(cfg.subagent_max_steps))
567
+ table.add_row("custom_tools_enabled", "yes" if cfg.custom_tools_enabled else "no")
568
+ table.add_row("stream", "yes" if cfg.stream else "no")
569
+ return table
570
+
571
+
572
+ def _provider_doctor_table(cfg: AppConfig) -> Table:
573
+ diagnostics = build_provider_diagnostics(cfg)
574
+ table = _Table(title="alysis doctor providers")
575
+ table.add_column("field")
576
+ table.add_column("value")
577
+ for key, value in diagnostics.rows():
578
+ table.add_row(key, value)
579
+ last_call = last_provider_call_summary()
580
+ if last_call:
581
+ table.add_row("last_call_provider", str(last_call.get("provider_key") or "(unknown)"))
582
+ table.add_row("last_call_protocol", str(last_call.get("protocol") or "(unknown)"))
583
+ table.add_row("last_call_status", str(last_call.get("status_category") or "(unknown)"))
584
+ table.add_row("last_call_latency_ms", str(last_call.get("latency_ms") or 0))
585
+ table.add_row("last_call_stream", "yes" if last_call.get("stream") else "no")
586
+ table.add_row(
587
+ "last_call_web_search",
588
+ str((last_call.get("web_search") or {}).get("backend_kind") or "off"),
589
+ )
590
+ last_search = last_web_search_summary()
591
+ if last_search:
592
+ table.add_row("last_web_search_adapter", str(last_search.get("web_search_adapter") or ""))
593
+ table.add_row(
594
+ "last_web_search_hosted",
595
+ "yes" if last_search.get("provider_hosted_search") else "no",
596
+ )
597
+ table.add_row("last_web_search_sources", str(last_search.get("source_count") or 0))
598
+ return table
599
+
600
+
601
+ def _auth_doctor_table() -> Table:
602
+ payload = auth_doctor_payload()
603
+ context = payload["context"]
604
+ environment = payload["environment"]
605
+ path = environment["path"]
606
+ keyring = payload["keyring"]
607
+ table = _Table(title="alysis doctor auth")
608
+ table.add_column("check")
609
+ table.add_column("result")
610
+ table.add_row("context", str(context["kind"]))
611
+ table.add_row(
612
+ "tty",
613
+ f"stdin={_yes_no(context['stdin_tty'])} stdout={_yes_no(context['stdout_tty'])} "
614
+ f"stderr={_yes_no(context['stderr_tty'])}",
615
+ )
616
+ table.add_row("term", str(context["term"] or "(unset)"))
617
+ table.add_row("ci_env", ", ".join(context["ci_env"]) or "(none)")
618
+ table.add_row("home", str(environment["home"]))
619
+ table.add_row("home_env", str(environment["home_env"] or "(unset)"))
620
+ table.add_row("config_dir", str(environment["config_dir"]))
621
+ table.add_row("config_dir_override", str(environment["config_dir_override"] or "(unset)"))
622
+ table.add_row("config_exists", _yes_no(environment["config_exists"]))
623
+ table.add_row(
624
+ "path",
625
+ f"{path['entry_count']} entries"
626
+ + (" (looks minimal)" if path["looks_minimal"] else "")
627
+ + f" · cli={path['resolved_cli'] or '(not on PATH)'}",
628
+ )
629
+ table.add_row("keyring_backend", str(keyring["backend"] or "(unresolved)"))
630
+ table.add_row("keyring_available", _yes_no(keyring["available"]))
631
+ table.add_row("keyring_env_override", str(keyring["env_override"] or "(unset)"))
632
+ if not keyring["available"]:
633
+ table.add_row("keyring_reason", str(keyring["reason"] or "(unknown)"))
634
+ for store in payload["credential_stores"]:
635
+ summary = "missing (no credentials stored yet)" if not store["exists"] else "readable"
636
+ if store["exists"] and not store["readable"]:
637
+ summary = f"UNREADABLE — {store['error']}"
638
+ elif store["exists"]:
639
+ summary = (
640
+ f"readable · {store['entry_count']} entries · "
641
+ f"key_source={store['key_source'] or '(unknown)'}"
642
+ )
643
+ table.add_row(f"store:{store['name']}", summary)
644
+ table.add_row(f"store:{store['name']}:next_write", str(store["planned_key_source"]))
645
+ return table
646
+
647
+
648
+ def _yes_no(value: object) -> str:
649
+ return "yes" if value else "no"
650
+
651
+
652
+ def _doctor_bundle_payload(cfg: AppConfig) -> dict[str, Any]:
653
+ diagnostics = build_provider_diagnostics(cfg)
654
+ return diagnostic_bundle_payload(
655
+ provider_diagnostics={key: value for key, value in diagnostics.rows()}
656
+ )
657
+
658
+
659
+ def _provider_live_validation_table(cfg: AppConfig, *, timeout_s: float = 15.0) -> Table:
660
+ validation = _patchable("validate_active_provider_live", validate_active_provider_live)(
661
+ cfg,
662
+ timeout_s=timeout_s,
663
+ )
664
+ table = _Table(title="alysis doctor providers --live")
665
+ table.add_column("field")
666
+ table.add_column("value")
667
+ for key, value in validation.rows():
668
+ table.add_row(key, value)
669
+ return table
670
+
671
+
672
+ def _web_search_live_validation_table(cfg: AppConfig, *, timeout_s: float = 15.0) -> Table:
673
+ validation = _patchable("validate_web_search_live", validate_web_search_live)(
674
+ cfg,
675
+ timeout_s=timeout_s,
676
+ )
677
+ table = _Table(title="web_search live check")
678
+ table.add_column("field")
679
+ table.add_column("value")
680
+ for key, value in validation.rows():
681
+ table.add_row(key, value)
682
+ return table
683
+
684
+
685
+ def _reasoning_suppression_table(cfg: AppConfig, *, timeout_s: float = 15.0) -> Table:
686
+ report = _patchable("probe_reasoning_suppression_live", probe_reasoning_suppression_live)(
687
+ cfg,
688
+ timeout_s=timeout_s,
689
+ )
690
+ table = _Table(title="reasoning-off live check")
691
+ table.add_column("field")
692
+ table.add_column("value")
693
+ for key, value in report.rows():
694
+ table.add_row(key, value)
695
+ return table
696
+
697
+
698
+ @dataclass(frozen=True)
699
+ class _ToolAvailabilityRow:
700
+ name: str
701
+ categories: str
702
+ status: str
703
+ purpose: str
704
+ notes: str
705
+
706
+
707
+ def _default_session_api_key(cfg: AppConfig | None = None) -> str | None:
708
+ if cfg is not None:
709
+ try:
710
+ return resolve_model_access_api_key(cfg)
711
+ except ConfigError:
712
+ return None
713
+ return _cli_module()._resolved_api_key_value().key
714
+
715
+
716
+ def _tool_availability_rows(cfg: AppConfig) -> list[_ToolAvailabilityRow]:
717
+ rows: list[_ToolAvailabilityRow] = []
718
+ main_api_key = _default_session_api_key(cfg)
719
+
720
+ for spec in iter_builtin_tool_metadata():
721
+ status = "available"
722
+ notes: list[str] = []
723
+
724
+ availability = get_tool_availability(spec.name)
725
+ if spec.optional and availability is not None and availability.unavailable_reason:
726
+ status = "optional-unavailable"
727
+ notes.append(f"reason={availability.unavailable_reason}")
728
+
729
+ if spec.name == "web_search":
730
+ runtime = resolve_web_search_runtime_status(cfg=cfg, api_key=main_api_key)
731
+ policy = resolve_web_search_policy(cfg)
732
+ status = "policy-disabled" if policy == "off" else runtime.availability_label
733
+ notes.append(f"policy={policy}")
734
+ notes.append(f"mode={runtime.mode}")
735
+ notes.append(f"provider={runtime.provider or '(none)'}")
736
+ if runtime.registration_ready and policy != "off":
737
+ notes.append("ready for registration in main agent sessions")
738
+ elif runtime.registration_ready:
739
+ notes.append("backend ready but web_search_policy=off prevents registration")
740
+ if runtime.provider == "openai_responses":
741
+ notes.append(
742
+ "OpenAI Responses readiness is conservative: explicit web_search_base_url or first-party OpenAI base_url"
743
+ )
744
+ elif runtime.provider in {
745
+ "xai_responses",
746
+ "anthropic_messages",
747
+ "gemini_grounding",
748
+ "openrouter_web",
749
+ "perplexity_sonar",
750
+ "groq_compound",
751
+ "mistral_conversations",
752
+ "moonshot_kimi",
753
+ "zhipu_web_search",
754
+ "volcengine_web_search",
755
+ "minimax_coding_plan",
756
+ "cohere_web_search",
757
+ }:
758
+ notes.append(f"available via {runtime.provider} provider adapter")
759
+ elif runtime.provider == "dashscope_chat":
760
+ notes.append(
761
+ "available via DashScope Chat Completions enable_search or Responses web_search"
762
+ )
763
+ elif runtime.provider == "tavily":
764
+ notes.append("available via model-independent Tavily adapter")
765
+ elif runtime.provider == "ddgs":
766
+ notes.append("available via keyless DuckDuckGo metasearch (ddgs) adapter")
767
+ notes.extend(runtime.notes)
768
+ if policy == "off":
769
+ notes.append("setup: set web_search_policy=auto to expose search to the model")
770
+ elif not runtime.registration_ready:
771
+ notes.append(f"setup: {runtime.setup_hint}")
772
+ elif spec.name == "skill_read":
773
+ if not resolve_skills_enabled(cfg):
774
+ status = "optional-disabled"
775
+ notes.append("set skills_enabled=true to advertise skills and register skill_read")
776
+ else:
777
+ status = "contextual"
778
+ notes.append("registered only when discovered skill bundles are available")
779
+ elif spec.name == "subagent_run" and not bool(getattr(cfg, "subagents_enabled", False)):
780
+ status = "optional-disabled"
781
+ notes.append("set subagents_enabled=true or use --subagents for top-level runs")
782
+
783
+ if spec.built_in_subagent_exposure.strip().lower() == "hidden":
784
+ notes.append("hidden from built-in readonly subagents")
785
+
786
+ rows.append(
787
+ _ToolAvailabilityRow(
788
+ name=spec.name,
789
+ categories=", ".join(spec.categories),
790
+ status=status,
791
+ purpose=spec.description.strip(),
792
+ notes="; ".join(note for note in notes if note.strip()) or "-",
793
+ )
794
+ )
795
+ return rows
796
+
797
+
798
+ def _tools_table(cfg: AppConfig) -> Table:
799
+ table = _Table(title="alysis tools")
800
+ table.add_column("tool")
801
+ table.add_column("categories")
802
+ table.add_column("status")
803
+ table.add_column("purpose")
804
+ table.add_column("notes")
805
+
806
+ for row in _tool_availability_rows(cfg):
807
+ table.add_row(
808
+ row.name,
809
+ row.categories,
810
+ row.status,
811
+ row.purpose,
812
+ row.notes,
813
+ )
814
+ return table
815
+
816
+
817
+ @app.command()
818
+ def setup(
819
+ section: str | None = typer.Argument(
820
+ None,
821
+ help="Optional setup target. Use `sandbox` to prepare the safe command runner.",
822
+ ),
823
+ ) -> None:
824
+ """Run first-time setup, or prepare a named setup target."""
825
+ if section is not None:
826
+ target = section.strip().lower()
827
+ if target != "sandbox":
828
+ _console().print("[red]Unknown setup target.[/red] Use: alysis setup sandbox")
829
+ raise typer.Exit(code=2)
830
+ _cli_module()._run_sandbox_setup_command(pull=True)
831
+ return
832
+
833
+ from ..setup_wizard import run_setup_wizard
834
+
835
+ # `alysis setup` defaults to the interactive alt-screen wizard (arrow-key
836
+ # selection) regardless of ALYSIS_TUI; it only falls back to the classic
837
+ # Rich wizard when the TUI can't run (and says why).
838
+ tui_result = _cli_module()._try_setup_tui(require_flag=False, announce_fallback=True)
839
+ if tui_result is None:
840
+ # Classic Rich wizard (non-interactive terminal or TUI failure): configure
841
+ # only, then surface any remaining sandbox setup.
842
+ if run_setup_wizard():
843
+ console = _console()
844
+ configured = _patchable("load_config", load_config)()
845
+ if configured.execution.backend == "delegated":
846
+ return
847
+ _cli_module()._maybe_run_startup_config_menu()
848
+ configured = _patchable("load_config", load_config)()
849
+ try:
850
+ result = _patchable("diagnose_sandbox", diagnose_sandbox)(
851
+ configured,
852
+ include_smoke=False,
853
+ include_server_image=False,
854
+ )
855
+ except ConfigError:
856
+ return
857
+ if not result.ready:
858
+ console.print()
859
+ console.print(
860
+ "[yellow]Safe runner setup is not complete yet. "
861
+ "Run `alysis doctor sandbox` for details or `alysis setup sandbox` "
862
+ "after installing/starting Docker.[/yellow]"
863
+ )
864
+ return
865
+
866
+ # The interactive TUI ran (it already walked the user through sandbox). On a
867
+ # saved setup, flow straight into chat — the final screen promised "Press
868
+ # Enter to start chatting" — in the workspace they just configured (so the
869
+ # broad-workspace guard does not re-ask about it). On cancel, back to shell.
870
+ if tui_result:
871
+ configured = _patchable("load_config", load_config)()
872
+ if configured.execution.backend == "delegated":
873
+ return
874
+ _cli_module()._maybe_run_startup_config_menu()
875
+ _cli_module()._run_chat_after_setup()
876
+
877
+
878
+ @app.command()
879
+ def doctor(
880
+ section: str | None = typer.Argument(
881
+ None,
882
+ help="Optional check group. Use `sandbox`, `providers`, `auth`, or `bundle`.",
883
+ ),
884
+ smoke: bool = typer.Option(
885
+ True,
886
+ "--smoke/--no-smoke",
887
+ help="Run a sandbox smoke command when checking `doctor sandbox`.",
888
+ ),
889
+ env: bool = typer.Option(
890
+ False,
891
+ "--env",
892
+ help="Show sandbox-related environment variable overrides.",
893
+ ),
894
+ live: bool = typer.Option(
895
+ False,
896
+ "--live",
897
+ help="Run a minimal live text request for `doctor providers` after confirmation.",
898
+ ),
899
+ yes: bool = typer.Option(
900
+ False,
901
+ "--yes",
902
+ help="Confirm live provider validation without prompting.",
903
+ ),
904
+ live_timeout: float = typer.Option(
905
+ 15.0,
906
+ "--live-timeout",
907
+ min=1.0,
908
+ help="Timeout in seconds for `doctor providers --live`.",
909
+ ),
910
+ redacted: bool = typer.Option(
911
+ True,
912
+ "--redacted/--no-redacted",
913
+ help="Emit only redacted diagnostic data for `doctor bundle`.",
914
+ ),
915
+ json_output: bool = typer.Option(
916
+ False,
917
+ "--json",
918
+ help=(
919
+ "Emit `doctor auth` as one machine-readable JSON object on stdout. "
920
+ "Rejected for the check groups that have no JSON form."
921
+ ),
922
+ ),
923
+ ) -> None:
924
+ if section is not None:
925
+ target = section.strip().lower()
926
+ if target == "auth":
927
+ if json_output is True:
928
+ typer.echo(json.dumps(auth_doctor_payload(), sort_keys=True))
929
+ return
930
+ _console().print(_auth_doctor_table())
931
+ return
932
+ if json_output is True and target != "bundle":
933
+ # Silently printing a table to a caller that asked for JSON would hand
934
+ # it unparseable output. `bundle` is already JSON-only.
935
+ _console().print(
936
+ "[red]--json is only supported for `doctor auth` and `doctor bundle`.[/red]"
937
+ )
938
+ raise typer.Exit(code=2)
939
+ if target == "sandbox":
940
+ _cli_module()._run_sandbox_doctor_command(include_smoke=smoke, include_env=env)
941
+ return
942
+ if target in {"provider", "providers"}:
943
+ cfg = _patchable("load_config", load_config)()
944
+ console = _console()
945
+ console.print(_provider_doctor_table(cfg))
946
+ if live:
947
+ _require_active_subscription_ready(model=None, base_url=None)
948
+ console.print(
949
+ "[yellow]Live provider validation sends a few minimal requests (one text "
950
+ "request, one reasoning-off request, and one web search when enabled) and "
951
+ "may incur provider cost or rate-limit usage.[/yellow]"
952
+ )
953
+ if not yes and not typer.confirm(
954
+ "Run live provider validation for the active profile?", default=False
955
+ ):
956
+ console.print("Live provider validation cancelled.")
957
+ return
958
+ console.print(_provider_live_validation_table(cfg, timeout_s=live_timeout))
959
+ console.print(_reasoning_suppression_table(cfg, timeout_s=live_timeout))
960
+ console.print(_web_search_live_validation_table(cfg, timeout_s=live_timeout))
961
+ return
962
+ if target == "bundle":
963
+ if not redacted:
964
+ _console().print("[red]Only redacted doctor bundles are supported.[/red]")
965
+ raise typer.Exit(code=2)
966
+ cfg = _patchable("load_config", load_config)()
967
+ typer.echo(json.dumps(_doctor_bundle_payload(cfg), indent=2, sort_keys=True))
968
+ return
969
+ if target != "sandbox":
970
+ _console().print(
971
+ "[red]Unknown doctor target.[/red] Use: alysis doctor sandbox|providers|auth|bundle"
972
+ )
973
+ raise typer.Exit(code=2)
974
+ if json_output is True:
975
+ _console().print(
976
+ "[red]--json is only supported for `doctor auth` and `doctor bundle`.[/red]"
977
+ )
978
+ raise typer.Exit(code=2)
979
+ console = _console()
980
+ cfg = _patchable("load_config", load_config)()
981
+ console.print(_doctor_table(cfg))
982
+
983
+
984
+ @app.command()
985
+ def tools() -> None:
986
+ console = _console()
987
+ cfg = _patchable("load_config", load_config)()
988
+ console.print(_tools_table(cfg))
989
+ console.print(
990
+ "[dim]`alysis tools` shows the built-in catalog plus config-dependent availability. "
991
+ "Use `/status` inside chat for session-specific details.[/dim]"
992
+ )
993
+ console.print(
994
+ "[dim]`web_search` discovers candidate sources; `web_fetch` retrieves a specific chosen URL.[/dim]"
995
+ )
996
+ console.print(
997
+ "[dim]Top-level readonly/Plan sessions can use ready web tools; nested readonly subagents keep them hidden.[/dim]"
998
+ )
999
+ console.print(
1000
+ "[dim]Use `web_search_policy=off|auto` for model web-search access, "
1001
+ "`web_search_mode=off|auto|native|external` for backend selection, and optional "
1002
+ "`web_search_adapter`. "
1003
+ "`auto` can use OpenAI Responses, xAI, Anthropic, Gemini, OpenRouter, DashScope "
1004
+ "Chat/Qwen, Kimi, Zhipu/GLM, Doubao, Perplexity, Groq, Mistral, or Tavily when "
1005
+ "`ALYSIS_WEB_SEARCH_API_KEY` or `TAVILY_API_KEY` is set. `native` never uses "
1006
+ "Tavily; `external` uses only external "
1007
+ "search adapters. Legacy `on` and `web_search_enabled` values still load as `auto`.[/dim]"
1008
+ )
1009
+ console.print(
1010
+ "[dim]Custom tools are managed separately via `alysis tool list|info|trust|untrust`.[/dim]"
1011
+ )
1012
+
1013
+
1014
+ @app.command()
1015
+ def login() -> None:
1016
+ """Connect your Alysis Code account."""
1017
+ from ... import account_login
1018
+
1019
+ console = _console()
1020
+ cfg = _patchable("load_config", load_config)()
1021
+ try:
1022
+ result = account_login.login(
1023
+ cfg, output_write=lambda message: console.print(message, highlight=False)
1024
+ )
1025
+ except (account_login.AlysisLoginError, ConfigError) as exc:
1026
+ console.print(f"[red]{exc}[/red]")
1027
+ raise typer.Exit(code=1) from exc
1028
+
1029
+ who = f" as [bold]{result.email}[/bold]" if result.email else ""
1030
+ console.print(f"[green]Logged in{who}.[/green] Your Alysis Code account is connected.")
1031
+ if result.model:
1032
+ console.print(
1033
+ f"Active profile: [bold]{result.profile_name}[/bold] · default model: "
1034
+ f"[bold]{result.model}[/bold]"
1035
+ )
1036
+ console.print("[dim]Switch model anytime with /model in chat, or `alysis config`.[/dim]")
1037
+ else:
1038
+ # No model is auto-selected on first login (the free MiMo default was
1039
+ # removed); guide the user to pick one before the "Model is not set" guard.
1040
+ console.print(
1041
+ f"Active profile: [bold]{result.profile_name}[/bold] · no model selected yet."
1042
+ )
1043
+ console.print(
1044
+ "[dim]Pick one with /model in chat, or `alysis config set model <MODEL>`.[/dim]"
1045
+ )
1046
+ console.print("[dim]Run `alysis chat` to start. Use `alysis logout` to disconnect.[/dim]")
1047
+
1048
+
1049
+ @app.command()
1050
+ def logout() -> None:
1051
+ """Disconnect your Alysis Code account (forgets the stored access key)."""
1052
+ from ... import account_login
1053
+
1054
+ console = _console()
1055
+ cfg = _patchable("load_config", load_config)()
1056
+ if account_login.logout(cfg):
1057
+ console.print("[green]Logged out.[/green] Your stored Alysis Code key was removed.")
1058
+ else:
1059
+ console.print("You're not logged in to an Alysis Code account.")
1060
+
1061
+
1062
+ def _whoami_payload(cfg: AppConfig) -> dict[str, Any]:
1063
+ """Build the Alysis Code account status contract for a supervising app."""
1064
+ from ... import account_login
1065
+
1066
+ status = account_login.login_status(cfg)
1067
+ trial = account_login.fetch_trial_status(cfg) if status.logged_in else None
1068
+ detail: str | None = None
1069
+ if not status.logged_in:
1070
+ detail = "No Alysis Code account is connected."
1071
+ elif trial is None:
1072
+ from ...alysis_cloud import site_host
1073
+
1074
+ detail = f"Connected. Manage your plan and credits at {site_host()}/account."
1075
+ else:
1076
+ detail = account_login.format_trial_status_line(trial) or None
1077
+ return _auth_status_payload(
1078
+ connection="alysis",
1079
+ authenticated=status.logged_in,
1080
+ account_label=(trial.email if trial is not None else None),
1081
+ method=("access-key" if status.logged_in else None),
1082
+ detail=detail,
1083
+ transport=status.base_url,
1084
+ profile_name=status.profile_name,
1085
+ profile_active=bool(status.active),
1086
+ plan=(trial.plan if trial is not None else None),
1087
+ )
1088
+
1089
+
1090
+ @app.command()
1091
+ def whoami(
1092
+ json_output: bool = typer.Option(False, "--json", help=_JSON_OPTION_HELP),
1093
+ ) -> None:
1094
+ """Show your Alysis Code login status."""
1095
+ from ... import account_login
1096
+
1097
+ console = _console()
1098
+ cfg = _patchable("load_config", load_config)()
1099
+ if json_output is True:
1100
+ typer.echo(json.dumps(_whoami_payload(cfg), sort_keys=True))
1101
+ return
1102
+ status = account_login.login_status(cfg)
1103
+ if not status.logged_in:
1104
+ console.print("Not logged in. Run `alysis login` to connect your Alysis Code account.")
1105
+ return
1106
+ active = "active" if status.active else "not active"
1107
+ console.print("[green]Logged in[/green] to the Alysis Code MiMo trial.")
1108
+ console.print(
1109
+ f"Profile: [bold]{status.profile_name}[/bold] ({active}) · key {status.key_preview}"
1110
+ )
1111
+ console.print(f"Proxy: {status.base_url}")
1112
+ trial = account_login.fetch_trial_status(cfg)
1113
+ if trial is not None:
1114
+ line = account_login.format_trial_status_line(trial)
1115
+ if line:
1116
+ console.print(line)
1117
+ else:
1118
+ console.print("[dim](Could not reach the trial service for live status.)[/dim]")