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,537 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from ..assets import (
14
+ AssetAlreadyExistsError,
15
+ AssetError,
16
+ AssetSurface,
17
+ AssetSurfaceDetail,
18
+ AssetSurfaceEntry,
19
+ asset_reference_check,
20
+ bind_asset_to_matching_tasks,
21
+ )
22
+ from ..assets.surface import build_asset_surface
23
+ from ..config import AppConfig, load_config
24
+ from ..forge import ForgeError, RunPaths, load_current_run_paths, load_plan, save_plan
25
+ from ..surface.console import make_console
26
+
27
+ assets_app = typer.Typer(add_completion=False, help="Manage first-class forge assets.")
28
+
29
+
30
+ @assets_app.command("list")
31
+ def assets_list(
32
+ path: Path = typer.Option(
33
+ Path("."), "--path", help="Workspace path or repository subdirectory."
34
+ ),
35
+ include_deleted: bool = typer.Option(
36
+ False,
37
+ "--include-deleted",
38
+ help="Include tombstoned assets.",
39
+ ),
40
+ output_format: str = typer.Option(
41
+ "table",
42
+ "--format",
43
+ help="Output format: table or json.",
44
+ ),
45
+ ) -> None:
46
+ surface = _load_surface_or_exit(path)
47
+ entries = surface.list_assets(include_deleted=include_deleted)
48
+ fmt = _normalize_format(output_format, allowed={"table", "json"})
49
+ if fmt == "json":
50
+ _print_json(
51
+ {
52
+ "run_id": surface.run_paths.run_id,
53
+ "assets": [_entry_payload(entry) for entry in entries],
54
+ }
55
+ )
56
+ return
57
+ table = Table(title=f"forge assets · {surface.run_paths.run_id}")
58
+ table.add_column("status")
59
+ table.add_column("kind")
60
+ table.add_column("pinned")
61
+ table.add_column("id")
62
+ table.add_column("title")
63
+ table.add_column("size")
64
+ for entry in entries:
65
+ table.add_row(
66
+ entry.comprehension_status,
67
+ entry.record.kind,
68
+ "yes" if entry.record.pinned else "no",
69
+ entry.record.id,
70
+ entry.record.title,
71
+ _format_size(entry.record.size_bytes),
72
+ )
73
+ _console().print(table)
74
+
75
+
76
+ @assets_app.command("show")
77
+ def assets_show(
78
+ asset_id: str = typer.Argument(..., help="Asset id."),
79
+ path: Path = typer.Option(
80
+ Path("."), "--path", help="Workspace path or repository subdirectory."
81
+ ),
82
+ output_format: str = typer.Option("text", "--format", help="Output format: text or json."),
83
+ ) -> None:
84
+ surface = _load_surface_or_exit(path)
85
+ try:
86
+ detail = surface.show_asset(asset_id)
87
+ except AssetError as exc:
88
+ _asset_error(str(exc))
89
+ fmt = _normalize_format(output_format, allowed={"text", "json"})
90
+ if fmt == "json":
91
+ _print_json(_detail_payload(detail))
92
+ return
93
+ record = detail.record
94
+ status = "deleted" if record.deleted_at else detail.comprehension_status
95
+ _console().print(f"Asset {record.id}")
96
+ _console().print(f"- title: {record.title}")
97
+ _console().print(f"- status: {status}")
98
+ _console().print(f"- kind: {record.kind}")
99
+ _console().print(f"- size: {record.size_bytes} bytes")
100
+ _console().print(f"- pinned: {'yes' if record.pinned else 'no'}")
101
+ _console().print(f"- original file: {record.original_filename}")
102
+ if detail.comprehension is not None:
103
+ _console().print(f"- source: {detail.comprehension.source}")
104
+ _console().print(f"- detected language: {detail.comprehension.detected_language or '-'}")
105
+ summary = detail.comprehension.data.semantic_summary.strip()
106
+ if summary:
107
+ _console().print(f"- summary: {summary}")
108
+ if detail.versions:
109
+ _console().print(f"- versions: {', '.join(str(version) for version in detail.versions)}")
110
+
111
+
112
+ @assets_app.command("add")
113
+ def assets_add(
114
+ file: Path = typer.Argument(..., help="File to ingest."),
115
+ path: Path = typer.Option(
116
+ Path("."), "--path", help="Workspace path or repository subdirectory."
117
+ ),
118
+ title: str | None = typer.Option(None, "--title", help="Asset title."),
119
+ description: str = typer.Option("", "--description", help="Asset description."),
120
+ pinned: bool = typer.Option(False, "--pinned", help="Pin asset to all tasks."),
121
+ wait: bool = typer.Option(False, "--wait", help="Wait for comprehension to complete."),
122
+ link: bool = typer.Option(False, "--link", help="Return existing asset on dedupe collision."),
123
+ ) -> None:
124
+ clean_title = _resolve_title(title)
125
+ surface = _load_surface_or_exit(path)
126
+ if wait:
127
+ _require_subscription_comprehension(surface.cfg)
128
+ try:
129
+ result = surface.add_asset(
130
+ file,
131
+ title=clean_title,
132
+ description=description,
133
+ pinned=pinned,
134
+ added_by={"phase": "cli", "command": "forge assets add"},
135
+ comprehend="sync" if wait else "skip",
136
+ dedupe_policy="link" if link else "reject",
137
+ )
138
+ except AssetAlreadyExistsError as exc:
139
+ _asset_error(f"{exc} existing_id={exc.existing_id}")
140
+ except AssetError as exc:
141
+ _asset_error(str(exc))
142
+ record = result.record
143
+ _console().print(f"Asset: {record.id}")
144
+ _console().print(f"- title: {record.title}")
145
+ _console().print(f"- kind: {record.kind}")
146
+ _console().print(f"- pinned: {'yes' if record.pinned else 'no'}")
147
+ if result.comprehension_record is not None:
148
+ _console().print(f"- comprehension: {result.comprehension_record.status}")
149
+ elif wait:
150
+ _console().print(f"- comprehension: {record.comprehension_status}")
151
+ else:
152
+ _console().print("- comprehension: pending")
153
+ _console().print(f"- next: alysis forge assets refresh {record.id} --path {path} --wait")
154
+ bound_task_ids = _bind_asset_to_current_plan(surface=surface, record=record)
155
+ if bound_task_ids:
156
+ _console().print("- bound tasks: " + ", ".join(bound_task_ids))
157
+
158
+
159
+ @assets_app.command("delete")
160
+ def assets_delete(
161
+ asset_id: str = typer.Argument(..., help="Asset id."),
162
+ path: Path = typer.Option(
163
+ Path("."), "--path", help="Workspace path or repository subdirectory."
164
+ ),
165
+ yes: bool = typer.Option(False, "--yes", help="Skip confirmation."),
166
+ ) -> None:
167
+ if not yes:
168
+ if not sys.stdin.isatty():
169
+ _asset_error("--yes is required when stdin is not a terminal")
170
+ if not typer.confirm(f"Delete asset {asset_id}?", default=False):
171
+ _console().print("Cancelled.")
172
+ return
173
+ surface = _load_surface_or_exit(path)
174
+ try:
175
+ deleted = surface.delete_asset(asset_id)
176
+ except AssetError as exc:
177
+ _asset_error(str(exc))
178
+ _console().print(f"Deleted asset: {deleted.id}")
179
+
180
+
181
+ @assets_app.command("edit")
182
+ def assets_edit(
183
+ asset_id: str = typer.Argument(..., help="Asset id."),
184
+ path: Path = typer.Option(
185
+ Path("."), "--path", help="Workspace path or repository subdirectory."
186
+ ),
187
+ title: str | None = typer.Option(None, "--title", help="New title."),
188
+ description: str | None = typer.Option(None, "--description", help="New description."),
189
+ pinned: bool | None = typer.Option(None, "--pin/--unpin", help="Set pinned state."),
190
+ refresh: bool = typer.Option(False, "--refresh", help="Refresh comprehension after edit."),
191
+ ) -> None:
192
+ if title is None and description is None and pinned is None and not refresh:
193
+ _asset_error("Nothing to edit. Use --title, --description, --pin, --unpin, or --refresh.")
194
+ surface = _load_surface_or_exit(path)
195
+ if refresh:
196
+ _require_subscription_comprehension(surface.cfg)
197
+ try:
198
+ detail = surface.edit_asset(
199
+ asset_id,
200
+ title=title,
201
+ description=description,
202
+ pinned=pinned,
203
+ retrigger_comprehension=False,
204
+ )
205
+ refreshed_record = (
206
+ surface.refresh_comprehension(asset_id, mode="sync").join() if refresh else None
207
+ )
208
+ except AssetError as exc:
209
+ _asset_error(str(exc))
210
+ _console().print(f"Updated asset: {detail.record.id}")
211
+ _console().print(f"- pinned: {'yes' if detail.record.pinned else 'no'}")
212
+ if refreshed_record is not None:
213
+ _console().print(f"- comprehension: {refreshed_record.status}")
214
+
215
+
216
+ @assets_app.command("refresh")
217
+ def assets_refresh(
218
+ asset_id: str = typer.Argument(..., help="Asset id."),
219
+ path: Path = typer.Option(
220
+ Path("."), "--path", help="Workspace path or repository subdirectory."
221
+ ),
222
+ wait: bool = typer.Option(False, "--wait", help="Wait for comprehension to complete."),
223
+ ) -> None:
224
+ surface = _load_surface_or_exit(path)
225
+ if not wait:
226
+ _asset_error(
227
+ "CLI refresh requires --wait. Use /assets for live background refresh in chat."
228
+ )
229
+ _require_subscription_comprehension(surface.cfg)
230
+ try:
231
+ handle = surface.refresh_comprehension(asset_id, mode="sync")
232
+ except AssetError as exc:
233
+ _asset_error(str(exc))
234
+ record = handle.join() if wait else None
235
+ _console().print(f"Refresh started: {asset_id}")
236
+ if record is not None:
237
+ _console().print(f"- comprehension: {record.status}")
238
+
239
+
240
+ @assets_app.command("cancel-pending")
241
+ def assets_cancel_pending(
242
+ path: Path = typer.Option(
243
+ Path("."), "--path", help="Workspace path or repository subdirectory."
244
+ ),
245
+ ) -> None:
246
+ _ = _load_surface_or_exit(path)
247
+ _console().print(
248
+ "No persistent CLI background comprehensions are running. "
249
+ "Use /assets to cancel live modal refreshes."
250
+ )
251
+
252
+
253
+ @assets_app.command("check-plan")
254
+ def assets_check_plan(
255
+ path: Path = typer.Option(
256
+ Path("."), "--path", help="Workspace path or repository subdirectory."
257
+ ),
258
+ output_format: str = typer.Option("text", "--format", help="Output format: text or json."),
259
+ ) -> None:
260
+ surface = _load_surface_or_exit(path)
261
+ fmt = _normalize_format(output_format, allowed={"text", "json"})
262
+ try:
263
+ plan = load_plan(surface.run_paths)
264
+ except ForgeError as exc:
265
+ _asset_error(str(exc))
266
+ report = asset_reference_check(plan, surface)
267
+ payload = {
268
+ "deleted_referenced": [
269
+ {"task_id": task_id, "asset_id": asset_id}
270
+ for task_id, asset_id in report.deleted_referenced
271
+ ],
272
+ "missing_referenced": [
273
+ {"task_id": task_id, "asset_id": asset_id}
274
+ for task_id, asset_id in report.missing_referenced
275
+ ],
276
+ "pinned_added": report.pinned_added,
277
+ }
278
+ if fmt == "json":
279
+ _print_json(payload)
280
+ else:
281
+ _console().print("Plan asset reference check")
282
+ _console().print(f"- deleted references: {len(report.deleted_referenced)}")
283
+ _console().print(f"- missing references: {len(report.missing_referenced)}")
284
+ _console().print(f"- pinned assets not bound: {len(report.pinned_added)}")
285
+ if report.deleted_referenced or report.missing_referenced:
286
+ raise typer.Exit(code=1)
287
+
288
+
289
+ @assets_app.command("prune-legacy")
290
+ def assets_prune_legacy(
291
+ path: Path = typer.Option(
292
+ Path("."), "--path", help="Workspace path or repository subdirectory."
293
+ ),
294
+ yes: bool = typer.Option(False, "--yes", help="Skip confirmation."),
295
+ output_format: str = typer.Option("text", "--format", help="Output format: text or json."),
296
+ ) -> None:
297
+ fmt = _normalize_format(output_format, allowed={"text", "json"})
298
+ try:
299
+ cfg = load_config()
300
+ run_paths = load_current_run_paths(path)
301
+ plan = load_plan(run_paths, migrate_legacy=False)
302
+ surface = build_surface(cfg=cfg, run_paths=run_paths)
303
+ except (ForgeError, AssetError) as exc:
304
+ _asset_error(str(exc))
305
+ schema_version = _plan_schema_version(plan)
306
+ if schema_version < 2:
307
+ _asset_error("Legacy asset pruning requires plan schema_version=2 after migration.")
308
+ legacy_files = _legacy_asset_files(run_paths)
309
+ known_hashes = {
310
+ record.sha256
311
+ for record in surface.index.records(include_deleted=True)
312
+ if str(record.sha256 or "").strip()
313
+ }
314
+ verified: list[str] = []
315
+ unverified: list[str] = []
316
+ for file_path in legacy_files:
317
+ digest = _sha256_file(file_path)
318
+ rel = file_path.relative_to(run_paths.root).as_posix()
319
+ if digest in known_hashes:
320
+ verified.append(rel)
321
+ else:
322
+ unverified.append(rel)
323
+ payload = {
324
+ "run_id": run_paths.run_id,
325
+ "verified": verified,
326
+ "unverified": unverified,
327
+ "deleted": [],
328
+ }
329
+ if unverified:
330
+ if fmt == "json":
331
+ _print_json(payload)
332
+ else:
333
+ _console().print("Refusing to prune unverified legacy asset files:")
334
+ for item in unverified:
335
+ _console().print(f"- {item}")
336
+ raise typer.Exit(code=1)
337
+ if not verified:
338
+ if fmt == "json":
339
+ _print_json(payload)
340
+ else:
341
+ _console().print("No legacy asset files to prune.")
342
+ return
343
+ if not yes:
344
+ if not sys.stdin.isatty():
345
+ _asset_error("--yes is required when stdin is not a terminal")
346
+ if not typer.confirm(
347
+ f"Delete {len(verified)} verified legacy asset file(s)?", default=False
348
+ ):
349
+ _console().print("Cancelled.")
350
+ return
351
+ deleted: list[str] = []
352
+ for file_path in legacy_files:
353
+ rel = file_path.relative_to(run_paths.root).as_posix()
354
+ if rel not in verified:
355
+ continue
356
+ try:
357
+ file_path.unlink()
358
+ except OSError as exc:
359
+ _asset_error(f"Failed to delete {rel}: {exc}")
360
+ deleted.append(rel)
361
+ _remove_empty_legacy_dirs(run_paths)
362
+ payload["deleted"] = deleted
363
+ if fmt == "json":
364
+ _print_json(payload)
365
+ else:
366
+ _console().print(f"Pruned {len(deleted)} verified legacy asset file(s).")
367
+
368
+
369
+ def _load_surface_or_exit(path: Path) -> AssetSurface:
370
+ try:
371
+ cfg = load_config()
372
+ run_paths = load_current_run_paths(path)
373
+ load_plan(run_paths)
374
+ return build_surface(cfg=cfg, run_paths=run_paths)
375
+ except (ForgeError, AssetError) as exc:
376
+ _asset_error(str(exc))
377
+
378
+
379
+ def build_surface(*, cfg: AppConfig, run_paths: RunPaths) -> AssetSurface:
380
+ return build_asset_surface(cfg=cfg, run_paths=run_paths)
381
+
382
+
383
+ def _require_subscription_comprehension(cfg: AppConfig) -> None:
384
+ from ..profiles import active_subscription_selection_ready, get_active_profile
385
+
386
+ try:
387
+ profile = get_active_profile(cfg)
388
+ except Exception:
389
+ return
390
+ provider_id = str(profile.auth_provider or "").strip()
391
+ if not provider_id:
392
+ return
393
+ if not active_subscription_selection_ready(cfg):
394
+ _asset_error(
395
+ "Choose the subscription model and reasoning effort in "
396
+ "/config → Default Model before running asset comprehension."
397
+ )
398
+ try:
399
+ from ..provider_auth import create_provider_auth
400
+
401
+ adapter = create_provider_auth(provider_id)
402
+ compatible = profile.protocol == adapter.protocol and profile.base_url.rstrip(
403
+ "/"
404
+ ) == adapter.base_url.rstrip("/")
405
+ status = adapter.account_status()
406
+ except Exception as exc: # noqa: BLE001
407
+ _asset_error(f"Subscription status check failed: {exc}")
408
+ if not compatible:
409
+ _asset_error("The subscription profile is incompatible; reconnect it through setup.")
410
+ if not status.connected:
411
+ detail = f" ({status.detail})" if status.detail else ""
412
+ _asset_error(
413
+ f"The selected AI subscription is not connected{detail}. "
414
+ f"Run `alysis auth login {provider_id}`."
415
+ )
416
+
417
+
418
+ def _bind_asset_to_current_plan(*, surface: AssetSurface, record: Any) -> list[str]:
419
+ try:
420
+ plan = load_plan(surface.run_paths)
421
+ active_records = surface.index.records(include_deleted=False)
422
+ bound_task_ids = bind_asset_to_matching_tasks(
423
+ plan=plan,
424
+ record=record,
425
+ active_records=active_records,
426
+ )
427
+ if bound_task_ids:
428
+ save_plan(surface.run_paths, plan)
429
+ return bound_task_ids
430
+ except (ForgeError, AssetError):
431
+ return []
432
+
433
+
434
+ def _resolve_title(title: str | None) -> str:
435
+ if title is None:
436
+ if not sys.stdin.isatty():
437
+ _asset_error("--title is required when stdin is not a terminal")
438
+ title = typer.prompt("Title")
439
+ clean = str(title or "").strip()
440
+ if not clean:
441
+ _asset_error("Asset title is required.")
442
+ return clean
443
+
444
+
445
+ def _normalize_format(value: str, *, allowed: set[str]) -> str:
446
+ normalized = str(value or "").strip().lower()
447
+ if normalized not in allowed:
448
+ _asset_error(f"--format must be one of: {', '.join(sorted(allowed))}")
449
+ return normalized
450
+
451
+
452
+ def _entry_payload(entry: AssetSurfaceEntry) -> dict[str, Any]:
453
+ return {
454
+ "record": entry.record.to_dict(),
455
+ "comprehension_status": entry.comprehension_status,
456
+ "comprehension_source": entry.comprehension_source,
457
+ "comprehension_summary_preview": entry.comprehension_summary_preview,
458
+ "detected_language": entry.detected_language,
459
+ }
460
+
461
+
462
+ def _detail_payload(detail: AssetSurfaceDetail) -> dict[str, Any]:
463
+ return {
464
+ "record": detail.record.to_dict(),
465
+ "comprehension_status": detail.comprehension_status,
466
+ "comprehension": (
467
+ detail.comprehension.to_dict() if detail.comprehension is not None else None
468
+ ),
469
+ "versions": detail.versions,
470
+ "extracted_text_preview": detail.extracted_text_preview,
471
+ }
472
+
473
+
474
+ def _print_json(payload: dict[str, Any]) -> None:
475
+ typer.echo(json.dumps(payload, ensure_ascii=False, sort_keys=True))
476
+
477
+
478
+ def _format_size(size_bytes: int) -> str:
479
+ size = float(size_bytes)
480
+ for unit in ("B", "KB", "MB", "GB"):
481
+ if size < 1024 or unit == "GB":
482
+ return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
483
+ size /= 1024
484
+ return f"{size_bytes} B"
485
+
486
+
487
+ def _console() -> Console:
488
+ return make_console()
489
+
490
+
491
+ def _asset_error(message: str) -> None:
492
+ typer.echo(f"Asset error: {message}", err=True)
493
+ raise typer.Exit(code=2)
494
+
495
+
496
+ def _plan_schema_version(plan: dict[str, Any]) -> int:
497
+ try:
498
+ return int(plan.get("schema_version", 1) or 1)
499
+ except (TypeError, ValueError):
500
+ return 1
501
+
502
+
503
+ def _legacy_asset_files(run_paths: RunPaths) -> list[Path]:
504
+ roots = [run_paths.assets_dir, run_paths.assets_text_dir]
505
+ files: list[Path] = []
506
+ for root in roots:
507
+ if not root.exists():
508
+ continue
509
+ files.extend(path for path in sorted(root.rglob("*")) if path.is_file())
510
+ return files
511
+
512
+
513
+ def _sha256_file(path: Path) -> str:
514
+ digest = hashlib.sha256()
515
+ with path.open("rb") as handle:
516
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
517
+ digest.update(chunk)
518
+ return digest.hexdigest()
519
+
520
+
521
+ def _remove_empty_legacy_dirs(run_paths: RunPaths) -> None:
522
+ for root in (run_paths.assets_text_dir, run_paths.assets_dir):
523
+ if not root.exists():
524
+ continue
525
+ for directory in sorted(
526
+ [path for path in root.rglob("*") if path.is_dir()],
527
+ key=lambda item: len(item.parts),
528
+ reverse=True,
529
+ ):
530
+ try:
531
+ directory.rmdir()
532
+ except OSError:
533
+ pass
534
+ try:
535
+ root.rmdir()
536
+ except OSError:
537
+ pass