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,441 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path, PurePosixPath
6
+ from typing import Literal
7
+
8
+ from ..atomic_io import atomic_write_json
9
+ from .models import DiscoveredSkills, SkillBundle
10
+ from .paths import global_skills_state_path, project_skills_state_path
11
+
12
+ SkillInstallSourceKind = Literal["dir", "zip", "git", "scaffold"]
13
+
14
+
15
+ class SkillLifecycleError(RuntimeError):
16
+ pass
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class SkillLifecycleIssue:
21
+ source_path: Path
22
+ message: str
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ManagedSkillRecord:
27
+ name: str
28
+ bundle_dir: str
29
+ scope: Literal["project", "user"]
30
+ source_kind: SkillInstallSourceKind | None = None
31
+ source: str | None = None
32
+ source_subdir: str | None = None
33
+ source_commit: str | None = None
34
+ installed_at: str | None = None
35
+ family: str = "native"
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class SkillLifecycleState:
40
+ managed_installs: dict[str, ManagedSkillRecord] = field(default_factory=dict)
41
+ disabled_names: tuple[str, ...] = ()
42
+ enabled_names: tuple[str, ...] = ()
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class SkillCatalogEntry:
47
+ skill: SkillBundle
48
+ enabled: bool
49
+ managed: bool
50
+ disabled_by: str | None = None
51
+ install_record: ManagedSkillRecord | None = None
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class SkillCatalog:
56
+ effective: DiscoveredSkills
57
+ entries: tuple[SkillCatalogEntry, ...]
58
+ global_state: SkillLifecycleState
59
+ project_state: SkillLifecycleState
60
+ lifecycle_issues: tuple[SkillLifecycleIssue, ...] = ()
61
+
62
+
63
+ def normalize_skill_name(raw: str) -> str:
64
+ candidate = str(raw or "").strip().casefold()
65
+ if not candidate:
66
+ raise SkillLifecycleError("Skill name cannot be empty.")
67
+ return candidate
68
+
69
+
70
+ def load_global_skill_state(*, user_config_dir: Path | None = None) -> SkillLifecycleState:
71
+ return _load_state(global_skills_state_path(user_config_dir=user_config_dir))
72
+
73
+
74
+ def load_global_skill_state_tolerant(
75
+ *,
76
+ user_config_dir: Path | None = None,
77
+ ) -> tuple[SkillLifecycleState, tuple[SkillLifecycleIssue, ...]]:
78
+ return _load_state_tolerant(global_skills_state_path(user_config_dir=user_config_dir))
79
+
80
+
81
+ def save_global_skill_state(
82
+ state: SkillLifecycleState,
83
+ *,
84
+ user_config_dir: Path | None = None,
85
+ ) -> None:
86
+ _save_state(global_skills_state_path(user_config_dir=user_config_dir), state)
87
+
88
+
89
+ def load_project_skill_state(workspace_root: Path) -> SkillLifecycleState:
90
+ return _load_state(project_skills_state_path(workspace_root))
91
+
92
+
93
+ def load_project_skill_state_tolerant(
94
+ workspace_root: Path,
95
+ ) -> tuple[SkillLifecycleState, tuple[SkillLifecycleIssue, ...]]:
96
+ return _load_state_tolerant(project_skills_state_path(workspace_root))
97
+
98
+
99
+ def save_project_skill_state(workspace_root: Path, state: SkillLifecycleState) -> None:
100
+ _save_state(project_skills_state_path(workspace_root), state)
101
+
102
+
103
+ def with_managed_install(
104
+ state: SkillLifecycleState,
105
+ record: ManagedSkillRecord,
106
+ ) -> SkillLifecycleState:
107
+ managed = dict(state.managed_installs)
108
+ managed[normalize_skill_name(record.name)] = record
109
+ return SkillLifecycleState(
110
+ managed_installs=dict(sorted(managed.items(), key=lambda item: item[0])),
111
+ disabled_names=state.disabled_names,
112
+ enabled_names=state.enabled_names,
113
+ )
114
+
115
+
116
+ def without_managed_install(state: SkillLifecycleState, name: str) -> SkillLifecycleState:
117
+ managed = dict(state.managed_installs)
118
+ managed.pop(normalize_skill_name(name), None)
119
+ return SkillLifecycleState(
120
+ managed_installs=dict(sorted(managed.items(), key=lambda item: item[0])),
121
+ disabled_names=tuple(
122
+ item for item in state.disabled_names if item != normalize_skill_name(name)
123
+ ),
124
+ enabled_names=tuple(
125
+ item for item in state.enabled_names if item != normalize_skill_name(name)
126
+ ),
127
+ )
128
+
129
+
130
+ def set_global_skill_disabled(
131
+ state: SkillLifecycleState, *, name: str, disabled: bool
132
+ ) -> SkillLifecycleState:
133
+ key = normalize_skill_name(name)
134
+ disabled_names = set(state.disabled_names)
135
+ if disabled:
136
+ disabled_names.add(key)
137
+ else:
138
+ disabled_names.discard(key)
139
+ return SkillLifecycleState(
140
+ managed_installs=state.managed_installs,
141
+ disabled_names=tuple(sorted(disabled_names)),
142
+ enabled_names=(),
143
+ )
144
+
145
+
146
+ def set_project_skill_override(
147
+ state: SkillLifecycleState,
148
+ *,
149
+ name: str,
150
+ enabled: bool,
151
+ ) -> SkillLifecycleState:
152
+ key = normalize_skill_name(name)
153
+ disabled_names = set(state.disabled_names)
154
+ enabled_names = set(state.enabled_names)
155
+ if enabled:
156
+ disabled_names.discard(key)
157
+ enabled_names.add(key)
158
+ else:
159
+ enabled_names.discard(key)
160
+ disabled_names.add(key)
161
+ return SkillLifecycleState(
162
+ managed_installs=state.managed_installs,
163
+ disabled_names=tuple(sorted(disabled_names)),
164
+ enabled_names=tuple(sorted(enabled_names)),
165
+ )
166
+
167
+
168
+ def resolve_skill_catalog(
169
+ *,
170
+ discovered: DiscoveredSkills,
171
+ workspace_root: Path,
172
+ user_config_dir: Path | None = None,
173
+ ) -> SkillCatalog:
174
+ global_state, global_issues = load_global_skill_state_tolerant(user_config_dir=user_config_dir)
175
+ project_state, project_issues = load_project_skill_state_tolerant(workspace_root)
176
+ entries: list[SkillCatalogEntry] = []
177
+ effective_skills: dict[str, SkillBundle] = {}
178
+ effective_ordered: list[SkillBundle] = []
179
+ for skill in discovered.ordered:
180
+ key = normalize_skill_name(skill.name)
181
+ global_record = global_state.managed_installs.get(key)
182
+ project_record = project_state.managed_installs.get(key)
183
+ install_record = project_record if skill.source_scope == "project" else global_record
184
+ enabled = True
185
+ disabled_by: str | None = None
186
+ if skill.source_scope == "user" and key in global_state.disabled_names:
187
+ enabled = False
188
+ disabled_by = "global"
189
+ if key in project_state.enabled_names:
190
+ enabled = True
191
+ disabled_by = None
192
+ if key in project_state.disabled_names:
193
+ enabled = False
194
+ disabled_by = "project"
195
+ entry = SkillCatalogEntry(
196
+ skill=skill,
197
+ enabled=enabled,
198
+ managed=install_record is not None,
199
+ disabled_by=disabled_by,
200
+ install_record=install_record,
201
+ )
202
+ entries.append(entry)
203
+ if enabled:
204
+ effective_skills[key] = skill
205
+ effective_ordered.append(skill)
206
+ return SkillCatalog(
207
+ effective=DiscoveredSkills(
208
+ skills=dict(sorted(effective_skills.items(), key=lambda item: item[0])),
209
+ ordered=tuple(effective_ordered),
210
+ issues=discovered.issues,
211
+ ),
212
+ entries=tuple(entries),
213
+ global_state=global_state,
214
+ project_state=project_state,
215
+ lifecycle_issues=(*global_issues, *project_issues),
216
+ )
217
+
218
+
219
+ def resolve_skill_catalog_entry(
220
+ *,
221
+ entries: tuple[SkillCatalogEntry, ...],
222
+ raw_name: str,
223
+ ) -> SkillCatalogEntry | None:
224
+ needle = str(raw_name or "").strip().casefold()
225
+ if not needle:
226
+ return None
227
+ for entry in entries:
228
+ if needle in entry.skill.lookup_keys():
229
+ return entry
230
+ return None
231
+
232
+
233
+ def _load_state(path: Path) -> SkillLifecycleState:
234
+ if not path.exists():
235
+ return SkillLifecycleState()
236
+ try:
237
+ raw_text = path.read_text(encoding="utf-8")
238
+ except OSError as exc:
239
+ raise SkillLifecycleError(f"Failed to read skills state: {path}") from exc
240
+ except UnicodeDecodeError as exc:
241
+ raise SkillLifecycleError(f"Invalid UTF-8 in skills state: {path}") from exc
242
+ try:
243
+ raw = json.loads(raw_text)
244
+ except json.JSONDecodeError as exc:
245
+ raise SkillLifecycleError(f"Invalid JSON in skills state: {path}") from exc
246
+ return _parse_state_payload(raw, path=path)
247
+
248
+
249
+ def _load_state_tolerant(path: Path) -> tuple[SkillLifecycleState, tuple[SkillLifecycleIssue, ...]]:
250
+ if not path.exists():
251
+ return SkillLifecycleState(), ()
252
+ try:
253
+ raw_text = path.read_text(encoding="utf-8")
254
+ except OSError:
255
+ return SkillLifecycleState(), (
256
+ SkillLifecycleIssue(
257
+ source_path=path,
258
+ message="Failed to read lifecycle state; using defaults.",
259
+ ),
260
+ )
261
+ except UnicodeDecodeError:
262
+ return SkillLifecycleState(), (
263
+ SkillLifecycleIssue(
264
+ source_path=path,
265
+ message="Invalid UTF-8 in lifecycle state; using defaults.",
266
+ ),
267
+ )
268
+ try:
269
+ raw = json.loads(raw_text)
270
+ except json.JSONDecodeError:
271
+ return SkillLifecycleState(), (
272
+ SkillLifecycleIssue(
273
+ source_path=path,
274
+ message="Invalid JSON in lifecycle state; using defaults.",
275
+ ),
276
+ )
277
+ return _parse_state_payload_tolerant(raw, path=path)
278
+
279
+
280
+ def resolve_managed_bundle_path(*, managed_root: Path, bundle_dir: str) -> Path:
281
+ safe_dir = _validated_bundle_dir_name(bundle_dir)
282
+ resolved_root = managed_root.expanduser().resolve()
283
+ target = (resolved_root / safe_dir).resolve()
284
+ try:
285
+ target.relative_to(resolved_root)
286
+ except ValueError as exc:
287
+ raise SkillLifecycleError(
288
+ f"Managed skill bundle escapes managed root: {bundle_dir!r}"
289
+ ) from exc
290
+ return target
291
+
292
+
293
+ def _parse_state_payload(raw: object, *, path: Path) -> SkillLifecycleState:
294
+ if not isinstance(raw, dict):
295
+ raise SkillLifecycleError(f"Invalid skills state payload: {path}")
296
+ installs_raw = raw.get("managed_installs", {})
297
+ if not isinstance(installs_raw, dict):
298
+ raise SkillLifecycleError(f"Invalid managed_installs payload in skills state: {path}")
299
+ installs, _issues = _parse_managed_installs(installs_raw, path=path, tolerant=False)
300
+ return SkillLifecycleState(
301
+ managed_installs=dict(sorted(installs.items(), key=lambda item: item[0])),
302
+ disabled_names=_normalized_name_tuple(raw.get("disabled_names")),
303
+ enabled_names=_normalized_name_tuple(raw.get("enabled_names")),
304
+ )
305
+
306
+
307
+ def _parse_state_payload_tolerant(
308
+ raw: object,
309
+ *,
310
+ path: Path,
311
+ ) -> tuple[SkillLifecycleState, tuple[SkillLifecycleIssue, ...]]:
312
+ if not isinstance(raw, dict):
313
+ return SkillLifecycleState(), (
314
+ SkillLifecycleIssue(
315
+ source_path=path,
316
+ message="Invalid lifecycle state payload; using defaults.",
317
+ ),
318
+ )
319
+ installs_raw = raw.get("managed_installs", {})
320
+ issues: list[SkillLifecycleIssue] = []
321
+ installs: dict[str, ManagedSkillRecord] = {}
322
+ if not isinstance(installs_raw, dict):
323
+ issues.append(
324
+ SkillLifecycleIssue(
325
+ source_path=path,
326
+ message="Invalid managed_installs payload in lifecycle state; ignoring it.",
327
+ )
328
+ )
329
+ else:
330
+ installs, install_issues = _parse_managed_installs(installs_raw, path=path, tolerant=True)
331
+ issues.extend(install_issues)
332
+ return (
333
+ SkillLifecycleState(
334
+ managed_installs=dict(sorted(installs.items(), key=lambda item: item[0])),
335
+ disabled_names=_normalized_name_tuple(raw.get("disabled_names")),
336
+ enabled_names=_normalized_name_tuple(raw.get("enabled_names")),
337
+ ),
338
+ tuple(issues),
339
+ )
340
+
341
+
342
+ def _parse_managed_installs(
343
+ installs_raw: dict[object, object],
344
+ *,
345
+ path: Path,
346
+ tolerant: bool,
347
+ ) -> tuple[dict[str, ManagedSkillRecord], tuple[SkillLifecycleIssue, ...]]:
348
+ installs: dict[str, ManagedSkillRecord] = {}
349
+ issues: list[SkillLifecycleIssue] = []
350
+ for key, payload in installs_raw.items():
351
+ if not isinstance(payload, dict):
352
+ if tolerant:
353
+ issues.append(
354
+ SkillLifecycleIssue(
355
+ source_path=path,
356
+ message=f"Skipping invalid managed skill record {key!r}; expected an object.",
357
+ )
358
+ )
359
+ continue
360
+ raise SkillLifecycleError(f"Invalid managed skill record in skills state: {path}")
361
+ try:
362
+ normalized = normalize_skill_name(str(payload.get("name") or key))
363
+ bundle_dir = _validated_bundle_dir_name(payload.get("bundle_dir") or normalized)
364
+ except SkillLifecycleError as exc:
365
+ if tolerant:
366
+ issues.append(
367
+ SkillLifecycleIssue(
368
+ source_path=path,
369
+ message=f"Skipping invalid managed skill record {key!r}: {exc}",
370
+ )
371
+ )
372
+ continue
373
+ raise SkillLifecycleError(f"{exc} ({path})") from exc
374
+ installs[normalized] = ManagedSkillRecord(
375
+ name=str(payload.get("name") or normalized),
376
+ bundle_dir=bundle_dir,
377
+ scope=("project" if str(payload.get("scope") or "") == "project" else "user"),
378
+ source_kind=_optional_source_kind(payload.get("source_kind")),
379
+ source=str(payload.get("source") or "").strip() or None,
380
+ source_subdir=str(payload.get("source_subdir") or "").strip() or None,
381
+ source_commit=str(payload.get("source_commit") or "").strip() or None,
382
+ installed_at=str(payload.get("installed_at") or "").strip() or None,
383
+ family=str(payload.get("family") or "native").strip() or "native",
384
+ )
385
+ return installs, tuple(issues)
386
+
387
+
388
+ def _save_state(path: Path, state: SkillLifecycleState) -> None:
389
+ path.parent.mkdir(parents=True, exist_ok=True)
390
+ payload = {
391
+ "version": 1,
392
+ "managed_installs": {
393
+ name: {
394
+ "name": record.name,
395
+ "bundle_dir": record.bundle_dir,
396
+ "scope": record.scope,
397
+ "source_kind": record.source_kind,
398
+ "source": record.source,
399
+ "source_subdir": record.source_subdir,
400
+ "source_commit": record.source_commit,
401
+ "installed_at": record.installed_at,
402
+ "family": record.family,
403
+ }
404
+ for name, record in sorted(state.managed_installs.items(), key=lambda item: item[0])
405
+ },
406
+ "disabled_names": list(state.disabled_names),
407
+ "enabled_names": list(state.enabled_names),
408
+ }
409
+ atomic_write_json(path, payload)
410
+
411
+
412
+ def _normalized_name_tuple(raw: object) -> tuple[str, ...]:
413
+ if not isinstance(raw, list):
414
+ return ()
415
+ out: list[str] = []
416
+ seen: set[str] = set()
417
+ for item in raw:
418
+ candidate = str(item or "").strip().casefold()
419
+ if not candidate or candidate in seen:
420
+ continue
421
+ seen.add(candidate)
422
+ out.append(candidate)
423
+ return tuple(sorted(out))
424
+
425
+
426
+ def _optional_source_kind(raw: object) -> SkillInstallSourceKind | None:
427
+ candidate = str(raw or "").strip().lower()
428
+ if candidate in {"dir", "zip", "git", "scaffold"}:
429
+ return candidate # type: ignore[return-value]
430
+ return None
431
+
432
+
433
+ def _validated_bundle_dir_name(raw: object) -> str:
434
+ candidate = str(raw or "").strip()
435
+ if not candidate:
436
+ raise SkillLifecycleError("Managed skill record is missing bundle_dir.")
437
+ pure = PurePosixPath(candidate.replace("\\", "/"))
438
+ parts = [part for part in pure.parts if part not in {"", "."}]
439
+ if pure.is_absolute() or any(part == ".." for part in parts) or len(parts) != 1:
440
+ raise SkillLifecycleError(f"Managed skill record has invalid bundle_dir: {candidate!r}")
441
+ return parts[0]
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import tempfile
6
+ from collections.abc import Callable
7
+ from contextlib import suppress
8
+ from pathlib import Path
9
+ from uuid import uuid4
10
+
11
+ from .state import SkillLifecycleError
12
+
13
+
14
+ def commit_managed_bundle_update(
15
+ *,
16
+ target_path: Path,
17
+ force: bool,
18
+ stage_bundle: Callable[[Path], None],
19
+ persist_state: Callable[[], None],
20
+ ) -> None:
21
+ existing_target = target_path.exists() or target_path.is_symlink()
22
+ if existing_target and not force:
23
+ raise SkillLifecycleError(f"Managed skill already exists: {target_path}")
24
+
25
+ target_path.parent.mkdir(parents=True, exist_ok=True)
26
+ backup_path: Path | None = None
27
+ installed_target = False
28
+ with tempfile.TemporaryDirectory(
29
+ prefix=f".{target_path.name}.stage-",
30
+ dir=target_path.parent,
31
+ ) as stage_dir:
32
+ staged_path = Path(stage_dir) / target_path.name
33
+ stage_bundle(staged_path)
34
+ if not (staged_path.exists() or staged_path.is_symlink()):
35
+ raise SkillLifecycleError(f"Staged managed skill bundle was not created: {staged_path}")
36
+ try:
37
+ if existing_target:
38
+ backup_path = _backup_path_for(target_path)
39
+ os.replace(target_path, backup_path)
40
+ os.replace(staged_path, target_path)
41
+ installed_target = True
42
+ persist_state()
43
+ except Exception as exc: # noqa: BLE001
44
+ _rollback_managed_bundle_update(
45
+ target_path=target_path,
46
+ backup_path=backup_path,
47
+ installed_target=installed_target,
48
+ cause=exc,
49
+ )
50
+ raise
51
+ else:
52
+ if backup_path is not None:
53
+ _best_effort_remove(backup_path)
54
+
55
+
56
+ def commit_managed_bundle_removal(
57
+ *,
58
+ target_path: Path,
59
+ persist_state: Callable[[], None],
60
+ ) -> None:
61
+ if not (target_path.exists() or target_path.is_symlink()):
62
+ persist_state()
63
+ return
64
+
65
+ backup_path = _backup_path_for(target_path)
66
+ try:
67
+ os.replace(target_path, backup_path)
68
+ persist_state()
69
+ except Exception as exc: # noqa: BLE001
70
+ rollback_errors: list[str] = []
71
+ if backup_path.exists() or backup_path.is_symlink():
72
+ try:
73
+ os.replace(backup_path, target_path)
74
+ except Exception as restore_exc: # noqa: BLE001
75
+ rollback_errors.append(f"failed to restore backup: {restore_exc}")
76
+ if rollback_errors:
77
+ raise SkillLifecycleError(
78
+ "Managed skill remove failed and rollback also failed: "
79
+ + "; ".join(rollback_errors)
80
+ ) from exc
81
+ raise
82
+ else:
83
+ _best_effort_remove(backup_path)
84
+
85
+
86
+ def _rollback_managed_bundle_update(
87
+ *,
88
+ target_path: Path,
89
+ backup_path: Path | None,
90
+ installed_target: bool,
91
+ cause: Exception,
92
+ ) -> None:
93
+ rollback_errors: list[str] = []
94
+ if installed_target and (target_path.exists() or target_path.is_symlink()):
95
+ try:
96
+ _remove_path(target_path)
97
+ except Exception as cleanup_exc: # noqa: BLE001
98
+ rollback_errors.append(f"failed to remove staged bundle: {cleanup_exc}")
99
+ if backup_path is not None and (backup_path.exists() or backup_path.is_symlink()):
100
+ try:
101
+ os.replace(backup_path, target_path)
102
+ except Exception as restore_exc: # noqa: BLE001
103
+ rollback_errors.append(f"failed to restore previous bundle: {restore_exc}")
104
+ if rollback_errors:
105
+ raise SkillLifecycleError(
106
+ "Managed skill update failed and rollback also failed: " + "; ".join(rollback_errors)
107
+ ) from cause
108
+
109
+
110
+ def _backup_path_for(target_path: Path) -> Path:
111
+ return target_path.parent / f".{target_path.name}.backup-{uuid4().hex}"
112
+
113
+
114
+ def _remove_path(path: Path) -> None:
115
+ if not path.exists() and not path.is_symlink():
116
+ return
117
+ if path.is_dir() and not path.is_symlink():
118
+ shutil.rmtree(path)
119
+ return
120
+ path.unlink()
121
+
122
+
123
+ def _best_effort_remove(path: Path) -> None:
124
+ with suppress(Exception): # noqa: BLE001
125
+ _remove_path(path)