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,293 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ import tempfile
6
+ import zipfile
7
+ from dataclasses import dataclass
8
+ from pathlib import Path, PurePosixPath
9
+ from time import strftime
10
+
11
+ from .paths import global_managed_skill_root, project_managed_skill_root, skill_bundle_dir_name
12
+ from .state import (
13
+ ManagedSkillRecord,
14
+ SkillLifecycleError,
15
+ load_global_skill_state,
16
+ load_project_skill_state,
17
+ resolve_managed_bundle_path,
18
+ save_global_skill_state,
19
+ save_project_skill_state,
20
+ with_managed_install,
21
+ without_managed_install,
22
+ )
23
+ from .transactions import commit_managed_bundle_removal, commit_managed_bundle_update
24
+ from .validation import SkillValidationResult, validate_skill_bundle
25
+
26
+ MAX_SKILL_ZIP_FILES = 256
27
+ MAX_SKILL_ZIP_UNCOMPRESSED_BYTES = 8 * 1024 * 1024
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class SkillInstallResult:
32
+ bundle_path: Path
33
+ installed_name: str
34
+ source_kind: str
35
+ source_commit: str | None = None
36
+ validation: SkillValidationResult | None = None
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class SkillRemoveResult:
41
+ bundle_path: Path | None
42
+ removed_name: str
43
+ scope: str
44
+
45
+
46
+ def install_skill_bundle(
47
+ *,
48
+ source: str,
49
+ workspace_root: Path,
50
+ project: bool = False,
51
+ subdir: str | None = None,
52
+ force: bool = False,
53
+ user_config_dir: Path | None = None,
54
+ ) -> SkillInstallResult:
55
+ with tempfile.TemporaryDirectory(prefix="alysis-skill-install-") as temp_dir:
56
+ temp_root = Path(temp_dir)
57
+ source_kind, source_root, source_commit = _materialize_install_source(
58
+ source=source,
59
+ temp_root=temp_root,
60
+ )
61
+ bundle = _resolve_install_bundle(source_root=source_root, subdir=subdir)
62
+ validation = validate_skill_bundle(bundle)
63
+ if not validation.valid:
64
+ raise SkillLifecycleError(_format_validation_failure(validation))
65
+ target_root = (
66
+ project_managed_skill_root(workspace_root)
67
+ if project
68
+ else global_managed_skill_root(user_config_dir=user_config_dir)
69
+ )
70
+ bundle_dir = skill_bundle_dir_name(validation.name)
71
+ target_path = resolve_managed_bundle_path(managed_root=target_root, bundle_dir=bundle_dir)
72
+ record = ManagedSkillRecord(
73
+ name=validation.name,
74
+ bundle_dir=bundle_dir,
75
+ scope=("project" if project else "user"),
76
+ source_kind=source_kind,
77
+ source=source,
78
+ source_subdir=(str(subdir).strip() or None),
79
+ source_commit=source_commit,
80
+ installed_at=strftime("%Y-%m-%dT%H:%M:%SZ"),
81
+ )
82
+ if project:
83
+ state = with_managed_install(load_project_skill_state(workspace_root), record)
84
+ commit_managed_bundle_update(
85
+ target_path=target_path,
86
+ force=force,
87
+ stage_bundle=lambda staged: shutil.copytree(bundle, staged),
88
+ persist_state=lambda: save_project_skill_state(workspace_root, state),
89
+ )
90
+ else:
91
+ state = with_managed_install(
92
+ load_global_skill_state(user_config_dir=user_config_dir),
93
+ record,
94
+ )
95
+ commit_managed_bundle_update(
96
+ target_path=target_path,
97
+ force=force,
98
+ stage_bundle=lambda staged: shutil.copytree(bundle, staged),
99
+ persist_state=lambda: save_global_skill_state(
100
+ state,
101
+ user_config_dir=user_config_dir,
102
+ ),
103
+ )
104
+ return SkillInstallResult(
105
+ bundle_path=target_path,
106
+ installed_name=validation.name,
107
+ source_kind=source_kind,
108
+ source_commit=source_commit,
109
+ validation=validation,
110
+ )
111
+
112
+
113
+ def remove_managed_skill(
114
+ *,
115
+ name: str,
116
+ workspace_root: Path,
117
+ project: bool = False,
118
+ user_config_dir: Path | None = None,
119
+ ) -> SkillRemoveResult:
120
+ key = str(name or "").strip().casefold()
121
+ if not key:
122
+ raise SkillLifecycleError("Skill name cannot be empty.")
123
+ if project:
124
+ state = load_project_skill_state(workspace_root)
125
+ record = state.managed_installs.get(key)
126
+ if record is None:
127
+ raise SkillLifecycleError(f"Managed project skill not found: {name}")
128
+ bundle_path = resolve_managed_bundle_path(
129
+ managed_root=project_managed_skill_root(workspace_root),
130
+ bundle_dir=record.bundle_dir,
131
+ )
132
+ commit_managed_bundle_removal(
133
+ target_path=bundle_path,
134
+ persist_state=lambda: save_project_skill_state(
135
+ workspace_root,
136
+ without_managed_install(state, name),
137
+ ),
138
+ )
139
+ return SkillRemoveResult(bundle_path=bundle_path, removed_name=record.name, scope="project")
140
+ state = load_global_skill_state(user_config_dir=user_config_dir)
141
+ record = state.managed_installs.get(key)
142
+ if record is None:
143
+ raise SkillLifecycleError(f"Managed user skill not found: {name}")
144
+ bundle_path = resolve_managed_bundle_path(
145
+ managed_root=global_managed_skill_root(user_config_dir=user_config_dir),
146
+ bundle_dir=record.bundle_dir,
147
+ )
148
+ commit_managed_bundle_removal(
149
+ target_path=bundle_path,
150
+ persist_state=lambda: save_global_skill_state(
151
+ without_managed_install(state, name),
152
+ user_config_dir=user_config_dir,
153
+ ),
154
+ )
155
+ return SkillRemoveResult(bundle_path=bundle_path, removed_name=record.name, scope="user")
156
+
157
+
158
+ def _materialize_install_source(
159
+ *,
160
+ source: str,
161
+ temp_root: Path,
162
+ ) -> tuple[str, Path, str | None]:
163
+ raw_source = str(source or "").strip()
164
+ if not raw_source:
165
+ raise SkillLifecycleError("Install source cannot be empty.")
166
+ source_path = Path(raw_source).expanduser()
167
+ if source_path.exists() and source_path.is_dir():
168
+ return "dir", source_path.resolve(), None
169
+ if source_path.exists() and source_path.is_file() and source_path.suffix.casefold() == ".zip":
170
+ extract_root = temp_root / "zip"
171
+ extract_root.mkdir(parents=True, exist_ok=True)
172
+ _extract_skill_zip(source_path.resolve(), extract_root)
173
+ return "zip", extract_root, None
174
+ return _clone_git_source(raw_source, temp_root / "git")
175
+
176
+
177
+ def _resolve_install_bundle(*, source_root: Path, subdir: str | None) -> Path:
178
+ root = source_root.resolve()
179
+ if subdir:
180
+ candidate = (root / Path(subdir)).resolve()
181
+ try:
182
+ candidate.relative_to(root)
183
+ except ValueError as exc:
184
+ raise SkillLifecycleError("Install subdir escapes the source root.") from exc
185
+ root = candidate
186
+ if (root / "SKILL.md").is_file():
187
+ return root
188
+ candidates: list[Path] = []
189
+ for entry in root.rglob("SKILL.md"):
190
+ if ".git" in entry.parts:
191
+ continue
192
+ candidates.append(entry.parent)
193
+ if not candidates:
194
+ raise SkillLifecycleError("No skill bundle found in install source.")
195
+ if len(candidates) > 1:
196
+ raise SkillLifecycleError(
197
+ "Install source contains multiple skill bundles; use --subdir to choose one."
198
+ )
199
+ return candidates[0]
200
+
201
+
202
+ def _extract_skill_zip(zip_path: Path, target_root: Path) -> None:
203
+ try:
204
+ archive = zipfile.ZipFile(zip_path)
205
+ except OSError as exc:
206
+ raise SkillLifecycleError(f"Failed to open zip archive: {zip_path}") from exc
207
+ with archive:
208
+ file_infos: list[tuple[zipfile.ZipInfo, list[str]]] = []
209
+ total_uncompressed = 0
210
+ for info in archive.infolist():
211
+ if _is_zip_symlink(info):
212
+ raise SkillLifecycleError(
213
+ "Zip archive contains symlink entries, which are not allowed."
214
+ )
215
+ if info.is_dir():
216
+ continue
217
+ parts = _sanitize_zip_parts(info.filename)
218
+ if not parts:
219
+ continue
220
+ file_infos.append((info, parts))
221
+ if len(file_infos) > MAX_SKILL_ZIP_FILES:
222
+ raise SkillLifecycleError(
223
+ f"Zip archive exceeds file-count limit ({MAX_SKILL_ZIP_FILES} files)."
224
+ )
225
+ total_uncompressed += max(0, int(info.file_size))
226
+ if total_uncompressed > MAX_SKILL_ZIP_UNCOMPRESSED_BYTES:
227
+ raise SkillLifecycleError(
228
+ "Zip archive exceeds uncompressed-size limit "
229
+ f"({MAX_SKILL_ZIP_UNCOMPRESSED_BYTES} bytes)."
230
+ )
231
+ for info, parts in file_infos:
232
+ destination = _resolve_zip_target(target_root, parts)
233
+ destination.parent.mkdir(parents=True, exist_ok=True)
234
+ with archive.open(info, "r") as src, destination.open("wb") as dst:
235
+ shutil.copyfileobj(src, dst)
236
+
237
+
238
+ def _clone_git_source(source: str, target_root: Path) -> tuple[str, Path, str | None]:
239
+ if shutil.which("git") is None:
240
+ raise SkillLifecycleError("git is required for git-based skill installs.")
241
+ try:
242
+ subprocess.run(
243
+ ["git", "clone", "--depth", "1", source, str(target_root)],
244
+ check=True,
245
+ capture_output=True,
246
+ text=True,
247
+ )
248
+ except subprocess.CalledProcessError as exc:
249
+ stderr = str(exc.stderr or "").strip()
250
+ raise SkillLifecycleError(f"Failed to clone git skill source: {stderr or source}") from exc
251
+ commit = None
252
+ try:
253
+ completed = subprocess.run(
254
+ ["git", "-C", str(target_root), "rev-parse", "HEAD"],
255
+ check=True,
256
+ capture_output=True,
257
+ text=True,
258
+ )
259
+ commit = str(completed.stdout or "").strip() or None
260
+ except subprocess.CalledProcessError:
261
+ commit = None
262
+ return "git", target_root, commit
263
+
264
+
265
+ def _is_zip_symlink(info: zipfile.ZipInfo) -> bool:
266
+ return bool((info.external_attr >> 16) & 0o170000 == 0o120000)
267
+
268
+
269
+ def _sanitize_zip_parts(name: str) -> list[str]:
270
+ clean_name = str(name or "").replace("\\", "/")
271
+ path = PurePosixPath(clean_name)
272
+ parts = [part for part in path.parts if part not in {"", "."}]
273
+ if any(part == ".." for part in parts):
274
+ raise SkillLifecycleError("Zip archive entry escapes extraction root.")
275
+ if path.is_absolute():
276
+ raise SkillLifecycleError("Zip archive contains absolute paths, which are not allowed.")
277
+ return parts
278
+
279
+
280
+ def _resolve_zip_target(base: Path, rel_parts: list[str]) -> Path:
281
+ target = base.joinpath(*rel_parts).resolve()
282
+ try:
283
+ target.relative_to(base.resolve())
284
+ except ValueError as exc:
285
+ raise SkillLifecycleError("Zip archive entry escapes extraction root.") from exc
286
+ return target
287
+
288
+
289
+ def _format_validation_failure(result: SkillValidationResult) -> str:
290
+ if not result.issues:
291
+ return f"Skill validation failed: {result.bundle_path}"
292
+ first = result.issues[0]
293
+ return f"Skill validation failed: {first.message} ({first.path})"
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from .models import SkillBundle, SkillDiscoveryIssue, SkillSourceKind, SkillSourceScope
7
+ from .validation import validate_skill_bundle
8
+
9
+
10
+ class SkillReadError(RuntimeError):
11
+ pass
12
+
13
+
14
+ def load_skill_bundle(
15
+ *,
16
+ bundle_path: Path,
17
+ source_scope: SkillSourceScope,
18
+ source_kind: SkillSourceKind,
19
+ source_family: str,
20
+ ancestor_distance: int | None,
21
+ ) -> tuple[SkillBundle | None, SkillDiscoveryIssue | None]:
22
+ validation = validate_skill_bundle(bundle_path)
23
+ if not validation.valid:
24
+ first_error = next(iter(validation.errors))
25
+ return None, SkillDiscoveryIssue(
26
+ source_path=bundle_path,
27
+ message=first_error.message,
28
+ )
29
+
30
+ bundle_name = bundle_path.name.strip() or validation.name
31
+ aliases = _ordered_unique_strings(
32
+ [validation.name, bundle_name, *_metadata_aliases(validation.metadata)]
33
+ )
34
+ return (
35
+ SkillBundle(
36
+ name=validation.name,
37
+ description=validation.description,
38
+ instructions=validation.instructions,
39
+ bundle_name=bundle_name,
40
+ bundle_path=bundle_path,
41
+ entry_path=validation.entry_path,
42
+ source_scope=source_scope,
43
+ source_kind=source_kind,
44
+ source_family=source_family,
45
+ source_path=bundle_path,
46
+ trust_level="untrusted",
47
+ ancestor_distance=ancestor_distance,
48
+ aliases=tuple(aliases),
49
+ metadata={
50
+ key: str(value)
51
+ for key, value in validation.metadata.items()
52
+ if key in {"name", "description"} and str(value).strip()
53
+ },
54
+ ),
55
+ None,
56
+ )
57
+
58
+
59
+ def read_skill_bundle_file(skill: SkillBundle, path: str | None = None) -> dict[str, Any]:
60
+ requested_path = str(path or "").strip()
61
+ if not requested_path:
62
+ target_path = skill.entry_path
63
+ relative_path = "SKILL.md"
64
+ else:
65
+ candidate = Path(requested_path)
66
+ if candidate.is_absolute():
67
+ raise SkillReadError("skill_read path must be relative to the skill bundle root")
68
+ target_path = (skill.bundle_path / candidate).resolve()
69
+ bundle_root = skill.bundle_path.resolve()
70
+ try:
71
+ target_path.relative_to(bundle_root)
72
+ except ValueError as exc:
73
+ raise SkillReadError("skill_read path escapes the skill bundle root") from exc
74
+ try:
75
+ relative_path = target_path.relative_to(bundle_root).as_posix()
76
+ except ValueError as exc:
77
+ raise SkillReadError("skill_read path escapes the skill bundle root") from exc
78
+ if not target_path.exists() or not target_path.is_file():
79
+ raise SkillReadError(f"Skill file not found: {requested_path or 'SKILL.md'}")
80
+ try:
81
+ content = target_path.read_text(encoding="utf-8")
82
+ except UnicodeDecodeError as exc:
83
+ raise SkillReadError(f"Skill file is not readable as UTF-8 text: {relative_path}") from exc
84
+ except OSError as exc:
85
+ raise SkillReadError(f"Failed to read skill file: {relative_path}") from exc
86
+ return {
87
+ "name": skill.name,
88
+ "bundle_name": skill.bundle_name,
89
+ "path": relative_path,
90
+ "content": content,
91
+ "source_scope": skill.source_scope,
92
+ "source_kind": skill.source_kind,
93
+ "source_family": skill.source_family,
94
+ "source_path": skill.source_path.as_posix(),
95
+ "trust_level": skill.trust_level,
96
+ }
97
+
98
+
99
+ def _ordered_unique_strings(values: list[str]) -> list[str]:
100
+ out: list[str] = []
101
+ seen: set[str] = set()
102
+ for raw in values:
103
+ value = str(raw or "").strip()
104
+ if not value:
105
+ continue
106
+ lowered = value.casefold()
107
+ if lowered in seen:
108
+ continue
109
+ seen.add(lowered)
110
+ out.append(value)
111
+ return out
112
+
113
+
114
+ def _metadata_aliases(metadata: dict[str, Any]) -> list[str]:
115
+ raw = metadata.get("aliases")
116
+ if isinstance(raw, list):
117
+ return [str(item).strip() for item in raw if str(item).strip()]
118
+ return []
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from .models import SkillBundle, SkillMatch
6
+
7
+ _TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9_./+-]*", re.IGNORECASE)
8
+ _STOPWORDS = {
9
+ "a",
10
+ "an",
11
+ "and",
12
+ "build",
13
+ "do",
14
+ "for",
15
+ "help",
16
+ "i",
17
+ "in",
18
+ "is",
19
+ "it",
20
+ "me",
21
+ "my",
22
+ "of",
23
+ "on",
24
+ "please",
25
+ "the",
26
+ "this",
27
+ "to",
28
+ "we",
29
+ "with",
30
+ }
31
+
32
+
33
+ def match_skills(
34
+ request: str,
35
+ *,
36
+ skills: list[SkillBundle] | tuple[SkillBundle, ...],
37
+ max_results: int = 5,
38
+ ) -> tuple[SkillMatch, ...]:
39
+ request_text = str(request or "").strip()
40
+ if not request_text:
41
+ return ()
42
+ request_tokens = _tokenize(request_text)
43
+ if not request_tokens:
44
+ return ()
45
+ matches: list[SkillMatch] = []
46
+ request_lower = request_text.casefold()
47
+ for skill in skills:
48
+ name_tokens = _tokenize(skill.name)
49
+ description_tokens = _tokenize(skill.description)
50
+ score = 0
51
+ matched_terms: list[str] = []
52
+ if skill.name.casefold() in request_lower or skill.bundle_name.casefold() in request_lower:
53
+ score += 12
54
+ matched_terms.append(skill.name)
55
+ overlap = sorted(request_tokens & name_tokens)
56
+ if overlap:
57
+ score += len(overlap) * 5
58
+ matched_terms.extend(overlap)
59
+ desc_overlap = sorted(request_tokens & description_tokens)
60
+ if desc_overlap:
61
+ score += len(desc_overlap) * 2
62
+ matched_terms.extend(desc_overlap)
63
+ if score <= 0:
64
+ continue
65
+ matches.append(
66
+ SkillMatch(
67
+ skill=skill,
68
+ score=score,
69
+ matched_terms=tuple(_ordered_unique(matched_terms)),
70
+ )
71
+ )
72
+ matches.sort(
73
+ key=lambda item: (
74
+ -item.score,
75
+ item.skill.name.casefold(),
76
+ item.skill.bundle_path.as_posix(),
77
+ )
78
+ )
79
+ return tuple(matches[: max(0, max_results)])
80
+
81
+
82
+ def _tokenize(text: str) -> set[str]:
83
+ tokens = {
84
+ token.casefold()
85
+ for token in _TOKEN_RE.findall(str(text or ""))
86
+ if token and token.casefold() not in _STOPWORDS
87
+ }
88
+ return {token for token in tokens if len(token) > 1}
89
+
90
+
91
+ def _ordered_unique(values: list[str]) -> list[str]:
92
+ out: list[str] = []
93
+ seen: set[str] = set()
94
+ for raw in values:
95
+ value = str(raw or "").strip()
96
+ if not value:
97
+ continue
98
+ lowered = value.casefold()
99
+ if lowered in seen:
100
+ continue
101
+ seen.add(lowered)
102
+ out.append(value)
103
+ return out
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Literal
6
+
7
+ SkillSourceScope = Literal["project", "user"]
8
+ SkillSourceKind = Literal["native", "interop"]
9
+ SkillTrustLevel = Literal["untrusted"]
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SkillBundle:
14
+ name: str
15
+ description: str
16
+ instructions: str
17
+ bundle_name: str
18
+ bundle_path: Path
19
+ entry_path: Path
20
+ source_scope: SkillSourceScope
21
+ source_kind: SkillSourceKind
22
+ source_family: str
23
+ source_path: Path
24
+ trust_level: SkillTrustLevel
25
+ ancestor_distance: int | None = None
26
+ aliases: tuple[str, ...] = ()
27
+ metadata: dict[str, str] = field(default_factory=dict)
28
+
29
+ @property
30
+ def source_label(self) -> str:
31
+ return f"{self.source_scope}/{self.source_kind}"
32
+
33
+ def lookup_keys(self) -> tuple[str, ...]:
34
+ keys: list[str] = []
35
+ for value in (self.name, self.bundle_name, *self.aliases):
36
+ candidate = str(value or "").strip()
37
+ if not candidate:
38
+ continue
39
+ lowered = candidate.casefold()
40
+ if lowered in keys:
41
+ continue
42
+ keys.append(lowered)
43
+ return tuple(keys)
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class SkillDiscoveryIssue:
48
+ source_path: Path
49
+ message: str
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class DiscoveredSkills:
54
+ skills: dict[str, SkillBundle]
55
+ ordered: tuple[SkillBundle, ...]
56
+ issues: tuple[SkillDiscoveryIssue, ...] = ()
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class SkillMatch:
61
+ skill: SkillBundle
62
+ score: int
63
+ matched_terms: tuple[str, ...] = ()
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class ConventionDocument:
68
+ name: str
69
+ path: Path
70
+ content: str
71
+ trust_level: SkillTrustLevel = "untrusted"
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Literal
6
+
7
+ from ..branding import canonical_user_config_dir
8
+
9
+ ProjectSkillFamily = Literal["native", "agents", "claude", "github"]
10
+
11
+ _PROJECT_FAMILY_ROOTS: dict[ProjectSkillFamily, tuple[str, ...]] = {
12
+ "native": (".alysis_skills",),
13
+ "agents": (".agents", "skills"),
14
+ "claude": (".claude", "skills"),
15
+ "github": (".github", "skills"),
16
+ }
17
+
18
+
19
+ def global_managed_skill_root(*, user_config_dir: Path | None = None) -> Path:
20
+ base = (
21
+ user_config_dir.expanduser().resolve() if user_config_dir else canonical_user_config_dir()
22
+ )
23
+ return base / "skills"
24
+
25
+
26
+ def global_skills_state_path(*, user_config_dir: Path | None = None) -> Path:
27
+ base = (
28
+ user_config_dir.expanduser().resolve() if user_config_dir else canonical_user_config_dir()
29
+ )
30
+ return base / "skills.json"
31
+
32
+
33
+ def project_managed_skill_root(workspace_root: Path) -> Path:
34
+ return workspace_root.expanduser().resolve() / ".alysis_skills"
35
+
36
+
37
+ def project_skills_state_path(workspace_root: Path) -> Path:
38
+ return workspace_root.expanduser().resolve() / ".alysis" / "skills.json"
39
+
40
+
41
+ def project_skill_root_for_family(workspace_root: Path, *, family: ProjectSkillFamily) -> Path:
42
+ return workspace_root.expanduser().resolve().joinpath(*_PROJECT_FAMILY_ROOTS[family])
43
+
44
+
45
+ def normalize_project_skill_family(raw: str | None) -> ProjectSkillFamily:
46
+ normalized = str(raw or "native").strip().lower()
47
+ if normalized in _PROJECT_FAMILY_ROOTS:
48
+ return normalized # type: ignore[return-value]
49
+ raise ValueError("Unsupported skill family. Expected one of: native, agents, claude, github.")
50
+
51
+
52
+ def skill_bundle_dir_name(raw_name: str) -> str:
53
+ candidate = re.sub(r"[^a-z0-9._-]+", "-", str(raw_name or "").strip().casefold()).strip("-")
54
+ if not candidate:
55
+ raise ValueError("Skill name must contain at least one letter or digit.")
56
+ return candidate