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,946 @@
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 dataclasses import dataclass
6
+
7
+ from .cli_common import *
8
+ from .update import _cached_update_notice
9
+
10
+
11
+ def _is_narrow_terminal() -> bool:
12
+ return _terminal_width() < 88
13
+
14
+
15
+ def _apply_temperature_override(cfg: AppConfig, temperature: float) -> None:
16
+ cfg.temperature = temperature
17
+ cfg.coding_temperature = temperature
18
+ cfg.review_temperature = temperature
19
+ cfg.planner_temperature = temperature
20
+ cfg.conflict_review_temperature = temperature
21
+ cfg.compactor_temperature = temperature
22
+ cfg.chat_temperature = temperature
23
+
24
+
25
+ def _home_panel() -> Panel:
26
+ lines = [
27
+ "Quick actions:",
28
+ "- 1) chat interactive chat loop",
29
+ "- 2) run one-shot agent execution (you will be prompted for instruction)",
30
+ "- 3) setup first-run setup wizard",
31
+ "- 4) doctor environment checks",
32
+ "- 5) plan Forge plan command",
33
+ "- 6) quit exit home prompt",
34
+ "- forge (in chat) enter planning mode with /forge",
35
+ "- forge show summarize current plan artifacts",
36
+ ]
37
+ update_notice = _cached_update_notice()
38
+ if update_notice:
39
+ lines.append("")
40
+ lines.append(update_notice)
41
+ content = "\n".join(lines)
42
+ return _Panel(content, title="Alysis Code Home", border_style="bright_black")
43
+
44
+
45
+ def _home_prompt_enabled() -> bool:
46
+ raw = str(env_get("ALYSIS_HOME_PROMPT") or "").strip().lower()
47
+ return raw in {"1", "true", "yes", "on"}
48
+
49
+
50
+ def _run_default_chat_action(
51
+ *,
52
+ path: Path | None = None,
53
+ allow_broad_workspace: bool = False,
54
+ mode: str | None = None,
55
+ model: str | None = None,
56
+ base_url: str | None = None,
57
+ temperature: float | None = None,
58
+ stream: bool | None = None,
59
+ max_steps: int | None = None,
60
+ subagents: bool | None = None,
61
+ no_log: bool = False,
62
+ verify_cmd: list[str] | None = None,
63
+ yes: bool = False,
64
+ ) -> None:
65
+ # Execution-posture flags (mode/yes/no_log/verify_cmd/…) default to the plain
66
+ # launch values, but callers may forward the current session's flags so a
67
+ # relaunch (e.g. /config → switch project) preserves the user's chosen posture
68
+ # instead of silently resetting approval policy/mode/logging.
69
+ _patchable("chat", chat)(
70
+ path=path or Path("."),
71
+ create_path=False,
72
+ allow_broad_workspace=allow_broad_workspace,
73
+ image=None,
74
+ mode=mode,
75
+ model=model,
76
+ base_url=base_url,
77
+ temperature=temperature,
78
+ stream=stream,
79
+ max_steps=max_steps,
80
+ subagents=subagents,
81
+ no_log=no_log,
82
+ verify_cmd=verify_cmd,
83
+ api_key_env=None,
84
+ api_key_stdin=False,
85
+ api_key=None,
86
+ yes=yes,
87
+ diagnostic_log=None,
88
+ )
89
+
90
+
91
+ def _configured_default_workspace() -> Path | None:
92
+ """The workspace folder setup persisted (``default_workspace_path``), if any."""
93
+ try:
94
+ cfg = _patchable("load_config", load_config)()
95
+ raw = str((cfg.extra_fields or {}).get("default_workspace_path") or "").strip()
96
+ except Exception:
97
+ return None
98
+ return Path(raw).expanduser() if raw else None
99
+
100
+
101
+ def _run_chat_after_setup() -> None:
102
+ """Launch chat right after setup, in the workspace the user just configured.
103
+
104
+ The broad-workspace consent was already given during setup (the wizard binds
105
+ with ``allow_broad_workspace=True``), so we don't re-prompt the guard for that
106
+ same folder. Falls back to the current directory when no workspace was saved.
107
+ """
108
+ workspace = _configured_default_workspace()
109
+ _run_default_chat_action(path=workspace, allow_broad_workspace=workspace is not None)
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class _SubscriptionAvailability:
114
+ active: bool = False
115
+ ready: bool = True
116
+ provider_id: str = ""
117
+ message: str = ""
118
+ is_error: bool = False
119
+ selection_required: bool = False
120
+
121
+
122
+ def _subscription_availability(cfg: AppConfig) -> _SubscriptionAvailability:
123
+ """Return subscription readiness without deciding whether the UI may open."""
124
+
125
+ try:
126
+ from ...profiles import active_subscription_selection_ready, get_active_profile
127
+
128
+ active_profile = get_active_profile(cfg)
129
+ provider_id = str(active_profile.auth_provider or "").strip()
130
+ except Exception as exc:
131
+ return _SubscriptionAvailability(
132
+ active=True,
133
+ ready=False,
134
+ message=f"Model access configuration is invalid: {exc}",
135
+ is_error=True,
136
+ )
137
+ if not provider_id:
138
+ return _SubscriptionAvailability()
139
+ try:
140
+ from ...provider_auth import create_provider_auth
141
+
142
+ adapter = create_provider_auth(provider_id)
143
+ if active_profile.protocol != adapter.protocol or active_profile.base_url.rstrip(
144
+ "/"
145
+ ) != adapter.base_url.rstrip("/"):
146
+ return _SubscriptionAvailability(
147
+ active=True,
148
+ ready=False,
149
+ provider_id=provider_id,
150
+ message=(
151
+ "The subscription connection profile is incompatible with its provider "
152
+ "adapter. Reconnect it through `alysis setup`."
153
+ ),
154
+ is_error=True,
155
+ )
156
+ status = adapter.account_status()
157
+ except Exception as exc: # noqa: BLE001
158
+ return _SubscriptionAvailability(
159
+ active=True,
160
+ ready=False,
161
+ provider_id=provider_id,
162
+ message=f"Subscription status check failed: {exc}",
163
+ is_error=True,
164
+ )
165
+ if status.connected:
166
+ if active_subscription_selection_ready(cfg):
167
+ return _SubscriptionAvailability(
168
+ active=True,
169
+ ready=True,
170
+ provider_id=provider_id,
171
+ )
172
+ return _SubscriptionAvailability(
173
+ active=True,
174
+ ready=False,
175
+ provider_id=provider_id,
176
+ message=(
177
+ "Choose the subscription model and reasoning effort in "
178
+ "/config → Default Model before sending a message."
179
+ ),
180
+ selection_required=True,
181
+ )
182
+ detail = f" ({status.detail})" if status.detail else ""
183
+ if not status.verified:
184
+ return _SubscriptionAvailability(
185
+ active=True,
186
+ ready=False,
187
+ provider_id=provider_id,
188
+ message=(
189
+ "The AI subscription status is temporarily unavailable"
190
+ f"{detail}. Try again shortly; your saved connection was not removed."
191
+ ),
192
+ )
193
+ return _SubscriptionAvailability(
194
+ active=True,
195
+ ready=False,
196
+ provider_id=provider_id,
197
+ message=(
198
+ "The selected AI subscription is not connected"
199
+ f"{detail}. Type /login in the TUI and choose the connection, or run "
200
+ f"`alysis auth login {provider_id}`; use /config to change model access."
201
+ ),
202
+ )
203
+
204
+
205
+ def _provider_auth_ready_for_chat() -> bool:
206
+ """Report whether a model call can start; this must not gate opening the TUI."""
207
+
208
+ try:
209
+ availability = _subscription_availability(_patchable("load_config", load_config)())
210
+ except Exception as exc:
211
+ _console().print(f"[red]Model access configuration is invalid:[/red] {exc}")
212
+ return False
213
+ if availability.ready:
214
+ return True
215
+ style = "red" if availability.is_error else "yellow"
216
+ _console().print(f"[{style}]{availability.message}[/{style}]")
217
+ return False
218
+
219
+
220
+ def _require_active_subscription_ready(
221
+ *,
222
+ model: str | None,
223
+ base_url: str | None,
224
+ require_ready: bool = True,
225
+ ) -> None:
226
+ """Apply the subscription selection/account gate to any LLM-facing command."""
227
+
228
+ try:
229
+ from ...profiles import get_active_profile
230
+
231
+ auth_profile = get_active_profile(_patchable("load_config", load_config)())
232
+ except Exception:
233
+ auth_profile = None
234
+ if auth_profile is None or not auth_profile.auth_provider:
235
+ return
236
+ model_is_override = model is not None and str(model).strip() != auth_profile.default_model
237
+ base_url_is_override = base_url is not None and (
238
+ str(base_url).strip().rstrip("/") != auth_profile.base_url.rstrip("/")
239
+ )
240
+ if model_is_override or base_url_is_override:
241
+ _console().print(
242
+ "[red]Subscription model and endpoint overrides are managed in "
243
+ "`/config` so model and reasoning stay compatible.[/red]"
244
+ )
245
+ raise typer.Exit(code=2)
246
+ if require_ready and not _provider_auth_ready_for_chat():
247
+ raise typer.Exit(code=1)
248
+
249
+
250
+ def _maybe_run_startup_config_menu() -> None:
251
+ cfg = _patchable("load_config", load_config)()
252
+ execution = getattr(cfg, "execution", None)
253
+ if str(getattr(execution, "backend", "native") or "native") == "delegated":
254
+ runtime_id = str(getattr(execution, "runtime", None) or "").strip()
255
+ if runtime_id and runtime_id in (getattr(cfg, "agent_runtimes", {}) or {}):
256
+ return
257
+ from ..config_menu import run_config_menu
258
+
259
+ run_config_menu(cfg=cfg, auto_focus="execution")
260
+ return
261
+ api_key_present = bool(_patchable("resolve_api_key", resolve_api_key)().key)
262
+ provider_auth_present = False
263
+ try:
264
+ from ...profiles import get_active_profile
265
+
266
+ active_profile = get_active_profile(cfg)
267
+ provider_auth_present = bool(active_profile.auth_provider)
268
+ except Exception:
269
+ provider_auth_present = False
270
+ if provider_auth_present:
271
+ return
272
+ if cfg.model and (api_key_present or provider_auth_present):
273
+ return
274
+ from ..config_menu import run_config_menu
275
+
276
+ focus = "api_key" if not api_key_present else "model"
277
+ run_config_menu(cfg=cfg, auto_focus=focus)
278
+
279
+
280
+ # Persisted by setup (see setup_wizard._ONBOARDED_KEY) once first-run onboarding
281
+ # completes. The first-run gate keys on this marker rather than on "a model is
282
+ # configured", so a genuine first launch is routed to setup even when a config
283
+ # that already carries a model exists on disk (e.g. a pre-staged or partially
284
+ # written config) — that case used to fall straight through to chat and land the
285
+ # user on the guarded-workspace picker instead of setup.
286
+ _ONBOARDED_KEY = "onboarded"
287
+
288
+
289
+ def _is_onboarded() -> bool:
290
+ """Whether the user has already completed first-run onboarding.
291
+
292
+ True when the explicit ``onboarded`` marker is present. For configs that
293
+ predate the marker we also treat a persisted ``default_workspace_path`` as an
294
+ equivalent signal: that key is only ever written by a *completed* setup (or by
295
+ explicitly choosing a default workspace in ``/config``), so existing users who
296
+ already finished setup are never re-sent through it. A config that merely has a
297
+ model set (but no completed setup) is intentionally *not* considered onboarded.
298
+ """
299
+ try:
300
+ cfg = _patchable("load_config", load_config)()
301
+ except Exception:
302
+ return False
303
+ extra = cfg.extra_fields or {}
304
+ if extra.get(_ONBOARDED_KEY):
305
+ return True
306
+ return bool(str(extra.get("default_workspace_path") or "").strip())
307
+
308
+
309
+ def _should_run_first_run_setup_wizard() -> bool:
310
+ return not _is_onboarded()
311
+
312
+
313
+ def _try_setup_tui(*, require_flag: bool = True, announce_fallback: bool = False) -> bool | None:
314
+ """Run the alt-screen setup wizard (arrow-key selection screens).
315
+
316
+ Returns ``True``/``False`` on a completed run (saved / cancelled), or
317
+ ``None`` when the TUI could not run so the caller falls back to the classic
318
+ Rich wizard. ``require_flag`` honors ``ALYSIS_TUI=0`` as an explicit
319
+ opt-out; the explicit ``alysis setup`` command passes
320
+ ``require_flag=False`` so it still shows the interactive screens. When
321
+ ``announce_fallback`` is set, the reason for dropping to the classic wizard
322
+ is printed (dim) so a launch failure is visible rather than silent.
323
+ """
324
+ reason: str | None = None
325
+ try:
326
+ from ..tui import is_tui_enabled
327
+ except Exception as exc: # noqa: BLE001 - the TUI is optional
328
+ reason = f"the TUI module is unavailable ({exc})"
329
+ else:
330
+ if require_flag and not is_tui_enabled():
331
+ reason = "ALYSIS_TUI is off"
332
+ # ``_is_non_interactive_terminal`` is defined in this module (via the
333
+ # cli_common surface); call it directly — there is no ``cli_impl.cli``.
334
+ elif _is_non_interactive_terminal():
335
+ reason = "this terminal is not interactive"
336
+
337
+ if reason is None:
338
+ try:
339
+ from ..tui.setup_app import run_setup_tui
340
+
341
+ return bool(run_setup_tui())
342
+ except Exception as exc: # noqa: BLE001 - any prompt_toolkit/terminal failure
343
+ # app.run restored the terminal on its way out, so printing is safe.
344
+ reason = f"the setup TUI failed to start ({exc})"
345
+
346
+ if announce_fallback and reason:
347
+ try:
348
+ _console().print(f"[dim]Using the classic setup wizard — {reason}.[/dim]")
349
+ except Exception:
350
+ pass
351
+ return None
352
+
353
+
354
+ def _maybe_run_first_run_setup_wizard() -> bool:
355
+ if not _should_run_first_run_setup_wizard():
356
+ return True
357
+ tui_result = _try_setup_tui()
358
+ if tui_result is True:
359
+ return True
360
+ if tui_result is False:
361
+ _console().print("[yellow]Exiting without starting chat.[/yellow]")
362
+ return False
363
+ from ..setup_wizard import run_setup_wizard
364
+
365
+ if run_setup_wizard():
366
+ return True
367
+ _console().print("[yellow]Exiting without starting chat.[/yellow]")
368
+ return False
369
+
370
+
371
+ def _run_default_run_action(instruction: str) -> None:
372
+ _patchable("run", run)(
373
+ instruction=instruction,
374
+ path=Path("."),
375
+ create_path=False,
376
+ allow_broad_workspace=False,
377
+ image=None,
378
+ mode=None,
379
+ model=None,
380
+ base_url=None,
381
+ temperature=None,
382
+ stream=None,
383
+ max_steps=None,
384
+ subagents=None,
385
+ no_log=False,
386
+ verify_cmd=None,
387
+ api_key_env=None,
388
+ api_key_stdin=False,
389
+ api_key=None,
390
+ yes=False,
391
+ benchmark=False,
392
+ deadline_seconds=None,
393
+ require_deadline=False,
394
+ diagnostic_log=None,
395
+ )
396
+
397
+
398
+ def _current_branch_label(root: Path) -> str:
399
+ if shutil.which("git") is None:
400
+ return "-"
401
+ proc = subprocess.run(
402
+ ["git", "-C", os.fspath(root), "rev-parse", "--abbrev-ref", "HEAD"],
403
+ check=False,
404
+ capture_output=True,
405
+ text=True,
406
+ )
407
+ if proc.returncode != 0:
408
+ return "-"
409
+ branch = proc.stdout.strip()
410
+ return branch or "-"
411
+
412
+
413
+ def _is_git_dirty(root: Path) -> bool:
414
+ if shutil.which("git") is None:
415
+ return False
416
+ proc = subprocess.run(
417
+ ["git", "-C", os.fspath(root), "status", "--porcelain"],
418
+ check=False,
419
+ capture_output=True,
420
+ text=True,
421
+ )
422
+ if proc.returncode != 0:
423
+ return False
424
+ return bool(proc.stdout.strip())
425
+
426
+
427
+ def _is_interactive_terminal() -> bool:
428
+ return bool(sys.stdin.isatty() and sys.stdout.isatty())
429
+
430
+
431
+ def _is_non_interactive_terminal() -> bool:
432
+ return not _is_interactive_terminal()
433
+
434
+
435
+ def _resolve_api_key_override(
436
+ *,
437
+ api_key: str | None,
438
+ api_key_env: str | None,
439
+ api_key_stdin: bool,
440
+ ) -> str | None:
441
+ # Per-command overrides stay in-memory; persistent storage is handled only
442
+ # by explicit config credentials commands / setup flow.
443
+ selected = sum(bool(x) for x in [api_key is not None, api_key_env is not None, api_key_stdin])
444
+ if selected > 1:
445
+ raise ConfigError("Use only one of: --api-key, --api-key-env, --api-key-stdin")
446
+
447
+ if api_key is not None:
448
+ v = api_key.strip()
449
+ if not v:
450
+ raise ConfigError("API key is empty.")
451
+ return v
452
+
453
+ if api_key_env is not None:
454
+ name = api_key_env.strip()
455
+ if not name:
456
+ raise ConfigError("--api-key-env must be non-empty.")
457
+ v = (env_get(name) or "").strip()
458
+ if not v:
459
+ raise ConfigError(f"Environment variable {name} is not set.")
460
+ return v
461
+
462
+ if api_key_stdin:
463
+ v = typer.prompt("API key", hide_input=True).strip()
464
+ if not v:
465
+ raise ConfigError("API key is empty.")
466
+ return v
467
+
468
+ return None
469
+
470
+
471
+ def _api_key_source_label(source: str) -> str:
472
+ normalized = str(source or "").strip()
473
+ if normalized == "env:ALYSIS_API_KEY":
474
+ return "env (ALYSIS_API_KEY)"
475
+ if normalized == "env:OPENAI_API_KEY":
476
+ return "env (OPENAI_API_KEY)"
477
+ if normalized.startswith("env:"):
478
+ return f"env ({normalized.removeprefix('env:')})"
479
+ if normalized.startswith("stored:profile="):
480
+ return f"stored ({normalized.removeprefix('stored:profile=')})"
481
+ if normalized in {"stored", "stored:legacy"}:
482
+ return "stored"
483
+ return "missing"
484
+
485
+
486
+ def _resolved_api_key_value() -> ApiKeyResolution:
487
+ return resolve_api_key()
488
+
489
+
490
+ def _parse_bool_text(value: str) -> bool | None:
491
+ v = value.strip().lower()
492
+ if v in {"1", "true", "yes", "on"}:
493
+ return True
494
+ if v in {"0", "false", "no", "off"}:
495
+ return False
496
+ return None
497
+
498
+
499
+ def _safe_component(raw: str) -> str:
500
+ clean = re.sub(r"[^A-Za-z0-9_-]", "_", str(raw).strip())
501
+ return clean or "x"
502
+
503
+
504
+ def _resolve_usage_hud_default(cfg: AppConfig) -> bool:
505
+ raw_env = env_get("ALYSIS_SHOW_USAGE_HUD")
506
+ if raw_env is not None:
507
+ parsed_env = _parse_bool_text(raw_env)
508
+ if parsed_env is not None:
509
+ return parsed_env
510
+ raw_cfg = cfg.extra_fields.get("show_usage_hud")
511
+ if isinstance(raw_cfg, bool):
512
+ return raw_cfg
513
+ if isinstance(raw_cfg, str):
514
+ parsed_cfg = _parse_bool_text(raw_cfg)
515
+ if parsed_cfg is not None:
516
+ return parsed_cfg
517
+ return True
518
+
519
+
520
+ def _chat_usage_hud_enabled(session: Any) -> bool:
521
+ raw = getattr(session, "_usage_hud_enabled", True)
522
+ return bool(raw)
523
+
524
+
525
+ def _set_chat_usage_hud_enabled(session: Any, enabled: bool) -> None:
526
+ session._usage_hud_enabled = bool(enabled)
527
+
528
+
529
+ def _refresh_chat_hud_context_cache(session: Any) -> None:
530
+ context_fn = getattr(session, "context_left", None)
531
+ if callable(context_fn):
532
+ try:
533
+ session._hud_context_cache = context_fn()
534
+ except Exception: # noqa: BLE001
535
+ session._hud_context_cache = None
536
+
537
+
538
+ def _chat_context_hud_value(session: Any) -> str:
539
+ value = _chat_context_percent_value(session)
540
+ if value is None:
541
+ return "n/a"
542
+ label = f"{value:.1f}%"
543
+ if value < 10.0:
544
+ return f"{label} !!"
545
+ if value < 25.0:
546
+ return f"{label} !"
547
+ return label
548
+
549
+
550
+ def _format_cost_with_unknown(
551
+ *,
552
+ known_cost: float | None,
553
+ unknown_calls: int,
554
+ style: str,
555
+ ) -> str:
556
+ base = format_usd(known_cost, style=style)
557
+ if unknown_calls <= 0:
558
+ return base
559
+ return f"{base} (+{unknown_calls} unknown)"
560
+
561
+
562
+ def _known_cost_value(data: dict[str, Any]) -> float | None:
563
+ if int(data.get("known_cost_calls") or 0) <= 0:
564
+ return None
565
+ raw = data.get("cost_usd")
566
+ if raw is None:
567
+ return None
568
+ try:
569
+ return float(raw)
570
+ except (TypeError, ValueError):
571
+ return None
572
+
573
+
574
+ def _format_exact_token_count(value: Any) -> str:
575
+ try:
576
+ parsed = int(value or 0)
577
+ except (TypeError, ValueError):
578
+ parsed = 0
579
+ return f"{max(0, parsed):,}"
580
+
581
+
582
+ def _format_compact_token_count(value: Any) -> str:
583
+ try:
584
+ parsed = int(value or 0)
585
+ except (TypeError, ValueError):
586
+ parsed = 0
587
+ count = max(0, parsed)
588
+ if count < 10_000:
589
+ return f"{count:,}"
590
+ if count < 1_000_000:
591
+ return f"{count / 1_000:.1f}k"
592
+ return f"{count / 1_000_000:.1f}M"
593
+
594
+
595
+ def _chat_context_percent_value(session: Any) -> float | None:
596
+ ctx = getattr(session, "_hud_context_cache", None)
597
+ if ctx is None:
598
+ return None
599
+ # The primary HUD must describe provider-usable capacity. The former
600
+ # baseline-subtracted conversation percentage always started at 100%, even
601
+ # when bootstrap prompts and tool schemas already occupied a substantial
602
+ # part of the model window.
603
+ percent = getattr(ctx, "effective_percent_left", None)
604
+ if percent is None:
605
+ percent = getattr(ctx, "context_window_percent_left", None)
606
+ if percent is None:
607
+ percent = getattr(ctx, "percent_left", None)
608
+ if percent is None:
609
+ return None
610
+ try:
611
+ return float(percent)
612
+ except (TypeError, ValueError):
613
+ return None
614
+
615
+
616
+ def _chat_effective_budget_percent_value(session: Any) -> float | None:
617
+ ctx = getattr(session, "_hud_context_cache", None)
618
+ if ctx is None:
619
+ return None
620
+ percent = getattr(ctx, "effective_percent_left", None)
621
+ if percent is None:
622
+ percent = getattr(ctx, "percent_left", None)
623
+ if percent is None:
624
+ return None
625
+ try:
626
+ return float(percent)
627
+ except (TypeError, ValueError):
628
+ return None
629
+
630
+
631
+ def _format_chat_context_percent(percent: float | None) -> str:
632
+ if percent is None:
633
+ return "n/a"
634
+ return f"{percent:.1f}%"
635
+
636
+
637
+ def _chat_usage_warning_line(percent: float | None) -> str | None:
638
+ if percent is None:
639
+ return None
640
+ if percent < 10.0:
641
+ return "\u26a0\ufe0e Critical input budget — run /compact to reduce context"
642
+ if percent < 20.0:
643
+ return "\u26a0\ufe0e Low input budget — run /compact to reduce context"
644
+ return None
645
+
646
+
647
+ def _chat_turn_usage_style(session: Any) -> str:
648
+ percent = _chat_effective_budget_percent_value(session)
649
+ if percent is None:
650
+ return "dim"
651
+ if percent < 10.0:
652
+ return "bold red"
653
+ if percent < 20.0:
654
+ return "yellow"
655
+ if percent < 50.0:
656
+ return STYLE_CONTENT
657
+ return "dim"
658
+
659
+
660
+ def _format_usage_billing_for_display(row: dict[str, Any], *, compact: bool = False) -> str:
661
+ """Render charge provenance without presenting estimates as exact bills."""
662
+
663
+ known_cost = _known_cost_value(row)
664
+ unknown_calls = int(row.get("unknown_cost_calls", row.get("unknown_cost_count", 0)) or 0)
665
+ estimate_calls = int(row.get("catalog_estimated_cost_calls") or 0)
666
+ subscription_calls = int(row.get("subscription_calls") or 0)
667
+ included_calls = int(row.get("included_calls") or 0)
668
+ local_calls = int(row.get("local_calls") or 0)
669
+
670
+ if known_cost is not None:
671
+ value = format_usd(known_cost, style="table")
672
+ if estimate_calls > 0:
673
+ value = f"~{value}"
674
+ suffixes: list[str] = []
675
+ if unknown_calls > 0:
676
+ suffixes.append(f"{unknown_calls} cost unknown")
677
+ if subscription_calls > 0:
678
+ suffixes.append("subscription")
679
+ if included_calls > 0:
680
+ suffixes.append("included")
681
+ if local_calls > 0:
682
+ suffixes.append("local")
683
+ if suffixes:
684
+ joiner = "+" if compact else ", "
685
+ value = f"{value} {joiner.join(suffixes)}"
686
+ return value
687
+
688
+ labels = [
689
+ label
690
+ for count, label in (
691
+ (subscription_calls, "subscription"),
692
+ (included_calls, "included"),
693
+ (local_calls, "local"),
694
+ )
695
+ if count > 0
696
+ ]
697
+ if unknown_calls > 0:
698
+ labels.append("cost n/a" if compact else f"unknown ({unknown_calls} cost unknown)")
699
+ if labels:
700
+ return ("+" if compact else ", ").join(labels)
701
+ return "$0.0000"
702
+
703
+
704
+ def _format_reported_token_total(
705
+ row: dict[str, Any],
706
+ *,
707
+ token_key: str,
708
+ reported_calls_key: str,
709
+ derived_calls_key: str | None = None,
710
+ ) -> str:
711
+ """Keep provider omission, a provider-reported zero, and a derived figure apart.
712
+
713
+ A derived figure is arithmetic over counts the provider did send, so it is
714
+ worth showing — but it is not itself reported, and labelling it as such
715
+ would overstate what the provider actually told us.
716
+ """
717
+
718
+ total_calls = int(row.get("api_usage_calls") or 0) + int(row.get("estimate_usage_calls") or 0)
719
+ reported_calls = int(row.get(reported_calls_key) or 0)
720
+ derived_calls = int(row.get(derived_calls_key) or 0) if derived_calls_key else 0
721
+ known_calls = reported_calls + derived_calls
722
+ if total_calls <= 0 or known_calls <= 0:
723
+ return "not reported"
724
+ value = _format_exact_token_count(row.get(token_key))
725
+ if reported_calls <= 0:
726
+ suffix = " (derived)"
727
+ elif derived_calls > 0:
728
+ suffix = " (partly derived)"
729
+ else:
730
+ suffix = ""
731
+ if known_calls < total_calls:
732
+ return f"{value} (partial {known_calls}/{total_calls}){suffix}"
733
+ return f"{value}{suffix}"
734
+
735
+
736
+ def _format_usage_source_for_display(row: dict[str, Any]) -> str:
737
+ provider_calls = int(row.get("api_usage_calls") or 0)
738
+ fallback_calls = int(row.get("estimate_usage_calls") or 0)
739
+ corrected_calls = int(row.get("corrected_usage_calls") or 0)
740
+ parts = [f"provider {provider_calls}", f"fallback {fallback_calls}"]
741
+ if corrected_calls > 0:
742
+ parts.append(f"corrected {corrected_calls}")
743
+ return " | ".join(parts)
744
+
745
+
746
+ def _resolve_chat_mode_alias(value: str) -> str | None:
747
+ key = value.strip().lower()
748
+ return _CHAT_MODE_ALIASES.get(key)
749
+
750
+
751
+ def _resolve_trace_level(value: str) -> str | None:
752
+ normalized = value.strip().lower()
753
+ if normalized in _CHAT_TRACE_LEVELS:
754
+ return normalized
755
+ return None
756
+
757
+
758
+ def _chat_trace_level(session: Any) -> str:
759
+ surface = getattr(session, "surface", None)
760
+ level = getattr(surface, "trace_level", None)
761
+ if isinstance(level, str):
762
+ normalized = level.strip().lower()
763
+ if normalized in _CHAT_TRACE_LEVELS:
764
+ return normalized
765
+ return "compact"
766
+
767
+
768
+ def _record_chat_session_setting(*, session: Any, setting: str, value: Any) -> None:
769
+ store = getattr(session, "store", None)
770
+ append = getattr(store, "append", None)
771
+ if not callable(append):
772
+ return
773
+ try:
774
+ append(
775
+ "session_setting_changed",
776
+ {"setting": str(setting), "value": value},
777
+ )
778
+ except Exception:
779
+ pass
780
+
781
+
782
+ def _set_chat_stream_enabled(*, session: Any, enabled: bool) -> bool:
783
+ next_value = bool(enabled)
784
+ previous = bool(getattr(session, "stream", True))
785
+ session.stream = next_value
786
+ cfg = getattr(session, "cfg", None)
787
+ if cfg is not None:
788
+ cfg.stream = next_value
789
+ if next_value != previous:
790
+ _record_chat_session_setting(
791
+ session=session,
792
+ setting="stream",
793
+ value=next_value,
794
+ )
795
+ return next_value
796
+
797
+
798
+ def _session_skill_listing(session: Any) -> tuple[bool, tuple[Any, ...], tuple[Any, ...]]:
799
+ cfg = getattr(session, "cfg", None)
800
+ enabled = resolve_skills_enabled(cfg) if isinstance(cfg, AppConfig) else True
801
+ ordered_obj = getattr(session, "skills_ordered", None)
802
+ ordered = (
803
+ tuple(item for item in ordered_obj if item is not None)
804
+ if isinstance(ordered_obj, tuple)
805
+ else tuple(item for item in (ordered_obj or []) if item is not None)
806
+ )
807
+ registry_obj = getattr(session, "skill_registry", None)
808
+ if ordered or isinstance(registry_obj, dict):
809
+ issues = tuple(getattr(session, "skill_discovery_issues", ()) or ())
810
+ return enabled, ordered, issues
811
+ discovered = _discover_skills_for_path(path=Path(getattr(session, "root", Path("."))))
812
+ return enabled, tuple(discovered.ordered), tuple(discovered.issues)
813
+
814
+
815
+ def _set_chat_trace_level(*, session: Any, level: str) -> str:
816
+ normalized = _resolve_trace_level(level) or "compact"
817
+ previous = _chat_trace_level(session)
818
+ surface = getattr(session, "surface", None)
819
+ if surface is None:
820
+ return normalized
821
+ setter = getattr(surface, "set_trace_level", None)
822
+ if callable(setter):
823
+ applied = setter(normalized)
824
+ if isinstance(applied, str):
825
+ applied_normalized = _resolve_trace_level(applied)
826
+ if applied_normalized is not None:
827
+ if applied_normalized != previous:
828
+ _record_chat_session_setting(
829
+ session=session,
830
+ setting="trace_level",
831
+ value=applied_normalized,
832
+ )
833
+ return applied_normalized
834
+ return normalized
835
+ try:
836
+ surface.trace_level = normalized
837
+ except Exception: # noqa: BLE001
838
+ return normalized
839
+ applied = _resolve_trace_level(str(getattr(surface, "trace_level", normalized))) or normalized
840
+ if applied != previous:
841
+ _record_chat_session_setting(
842
+ session=session,
843
+ setting="trace_level",
844
+ value=applied,
845
+ )
846
+ return applied
847
+
848
+
849
+ def _emit_plan_mode_trace(
850
+ *,
851
+ session: Any,
852
+ message: str,
853
+ full_only: bool = False,
854
+ source: str = "plan_mode",
855
+ ) -> None:
856
+ clean = message.strip()
857
+ if not clean:
858
+ return
859
+ trace_level = _chat_trace_level(session)
860
+ if trace_level == "off":
861
+ return
862
+ if full_only and trace_level != "full":
863
+ return
864
+
865
+ store = getattr(session, "store", None)
866
+ append = getattr(store, "append", None)
867
+ if callable(append):
868
+ try:
869
+ append("progress", {"message": clean, "source": source})
870
+ except Exception: # noqa: BLE001
871
+ pass
872
+
873
+ surface = getattr(session, "surface", None)
874
+ handler = getattr(surface, "on_progress_update", None)
875
+ if callable(handler):
876
+ try:
877
+ handler(clean)
878
+ except Exception: # noqa: BLE001
879
+ return
880
+
881
+
882
+ def _emit_forge_planner_trace(
883
+ *,
884
+ session: Any,
885
+ message: str,
886
+ full_only: bool = False,
887
+ ) -> None:
888
+ _emit_plan_mode_trace(
889
+ session=session,
890
+ message=message,
891
+ full_only=full_only,
892
+ source="forge_planner",
893
+ )
894
+
895
+
896
+ def _make_forge_swarm_trace_sink(
897
+ *,
898
+ session: Any,
899
+ paths: Any,
900
+ console: Console,
901
+ ) -> SerializedSwarmTraceSink:
902
+ surface = getattr(session, "surface", None)
903
+ store = getattr(session, "store", None)
904
+ return SerializedSwarmTraceSink(
905
+ artifact_path=Path(paths.execution_dir) / "trace" / "swarm_trace.jsonl",
906
+ trace_level=_chat_trace_level(session),
907
+ surface=surface,
908
+ session_store=store,
909
+ console=console,
910
+ store_source="forge_swarm",
911
+ )
912
+
913
+
914
+ def _make_plan_mode_delta_trace_callback(*, session: Any) -> Callable[[str], None] | None:
915
+ trace_level = _chat_trace_level(session)
916
+ if trace_level == "off":
917
+ return None
918
+
919
+ seen_chars = 0
920
+ started = False
921
+ last_full_bucket = -1
922
+
923
+ def _on_text_delta(delta: str) -> None:
924
+ nonlocal seen_chars, started, last_full_bucket
925
+ if not delta:
926
+ return
927
+ seen_chars += len(delta)
928
+ if not started:
929
+ _emit_plan_mode_trace(session=session, message="Receiving planner output...")
930
+ started = True
931
+ if trace_level != "full":
932
+ return
933
+ bucket = seen_chars // 320
934
+ if bucket <= last_full_bucket:
935
+ return
936
+ last_full_bucket = bucket
937
+ _emit_plan_mode_trace(
938
+ session=session,
939
+ message=f"Planner draft progress: ~{seen_chars} chars captured.",
940
+ full_only=True,
941
+ )
942
+
943
+ return _on_text_delta
944
+
945
+
946
+ __all__ = [name for name in globals() if (not name.startswith("__") or name == "__version__")]