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,190 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from .git_ops import (
9
+ GitOpsError,
10
+ branch_exists,
11
+ ensure_git_available,
12
+ ensure_git_repo,
13
+ ensure_runtime_artifact_excludes,
14
+ status_porcelain,
15
+ )
16
+ from .git_safe import build_git_cmd
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class WorktreeInfo:
21
+ path: Path
22
+ branch: str
23
+ base_branch: str
24
+ created_branch: bool
25
+ reused_existing_worktree: bool
26
+
27
+
28
+ def _run_git_checked(
29
+ root: Path,
30
+ args: list[str],
31
+ *,
32
+ error_message: str,
33
+ ) -> subprocess.CompletedProcess[str]:
34
+ try:
35
+ cp = subprocess.run(
36
+ build_git_cmd(root, args),
37
+ check=False,
38
+ capture_output=True,
39
+ text=True,
40
+ )
41
+ except OSError as e:
42
+ raise GitOpsError("failed to run git") from e
43
+ if cp.returncode != 0:
44
+ detail = (cp.stderr or cp.stdout).strip()
45
+ raise GitOpsError(f"{error_message}: {detail or 'unknown error'}")
46
+ return cp
47
+
48
+
49
+ def _is_valid_existing_worktree(
50
+ *,
51
+ worktree_repo_path: Path,
52
+ expected_branch: str,
53
+ ) -> tuple[bool, str]:
54
+ try:
55
+ inside = subprocess.run(
56
+ build_git_cmd(worktree_repo_path, ["rev-parse", "--is-inside-work-tree"]),
57
+ check=False,
58
+ capture_output=True,
59
+ text=True,
60
+ )
61
+ except OSError as e:
62
+ raise GitOpsError("failed to run git") from e
63
+ if inside.returncode != 0 or inside.stdout.strip().lower() != "true":
64
+ return False, "path exists but is not a valid git worktree"
65
+
66
+ try:
67
+ branch_cp = subprocess.run(
68
+ build_git_cmd(worktree_repo_path, ["rev-parse", "--abbrev-ref", "HEAD"]),
69
+ check=False,
70
+ capture_output=True,
71
+ text=True,
72
+ )
73
+ except OSError as e:
74
+ raise GitOpsError("failed to run git") from e
75
+ if branch_cp.returncode != 0:
76
+ return False, "failed to resolve worktree branch"
77
+ current = branch_cp.stdout.strip()
78
+ if current != expected_branch:
79
+ return (
80
+ False,
81
+ f"worktree branch mismatch (expected {expected_branch}, got {current or '(empty)'})",
82
+ )
83
+ ensure_runtime_artifact_excludes(worktree_repo_path)
84
+ status_lines = status_porcelain(worktree_repo_path)
85
+ if status_lines:
86
+ preview = ", ".join(status_lines[:5])
87
+ if len(status_lines) > 5:
88
+ preview += ", ..."
89
+ return False, f"worktree has stale filesystem state ({preview})"
90
+ return True, ""
91
+
92
+
93
+ def _cleanup_existing_worktree_path(*, root: Path, worktree_repo_path: Path) -> None:
94
+ remove_cp = subprocess.run(
95
+ build_git_cmd(root, ["worktree", "remove", str(worktree_repo_path), "--force"]),
96
+ check=False,
97
+ capture_output=True,
98
+ text=True,
99
+ )
100
+ # Best-effort cleanup via git first; path cleanup below handles leftovers.
101
+ _ = remove_cp
102
+ if worktree_repo_path.exists():
103
+ if worktree_repo_path.is_dir():
104
+ shutil.rmtree(worktree_repo_path, ignore_errors=True)
105
+ else:
106
+ try:
107
+ worktree_repo_path.unlink()
108
+ except OSError:
109
+ pass
110
+ if worktree_repo_path.exists():
111
+ raise GitOpsError(f"failed to cleanup existing worktree path {worktree_repo_path}")
112
+
113
+
114
+ def ensure_task_worktree(
115
+ *,
116
+ root: Path,
117
+ worktree_repo_path: Path,
118
+ branch: str,
119
+ base_branch: str,
120
+ ) -> WorktreeInfo:
121
+ ensure_git_available()
122
+ ensure_git_repo(root)
123
+ worktree_repo_path = worktree_repo_path.resolve()
124
+ worktree_repo_path.parent.mkdir(parents=True, exist_ok=True)
125
+
126
+ if worktree_repo_path.exists():
127
+ valid, _reason = _is_valid_existing_worktree(
128
+ worktree_repo_path=worktree_repo_path,
129
+ expected_branch=branch,
130
+ )
131
+ if valid:
132
+ return WorktreeInfo(
133
+ path=worktree_repo_path,
134
+ branch=branch,
135
+ base_branch=base_branch,
136
+ created_branch=False,
137
+ reused_existing_worktree=True,
138
+ )
139
+ _cleanup_existing_worktree_path(root=root, worktree_repo_path=worktree_repo_path)
140
+
141
+ if branch_exists(root, branch):
142
+ _run_git_checked(
143
+ root,
144
+ ["worktree", "add", str(worktree_repo_path), branch],
145
+ error_message=f"failed to add worktree for branch {branch}",
146
+ )
147
+ return WorktreeInfo(
148
+ path=worktree_repo_path,
149
+ branch=branch,
150
+ base_branch=base_branch,
151
+ created_branch=False,
152
+ reused_existing_worktree=False,
153
+ )
154
+
155
+ _run_git_checked(
156
+ root,
157
+ ["worktree", "add", "-b", branch, str(worktree_repo_path), base_branch],
158
+ error_message=f"failed to create worktree branch {branch} from {base_branch}",
159
+ )
160
+ return WorktreeInfo(
161
+ path=worktree_repo_path,
162
+ branch=branch,
163
+ base_branch=base_branch,
164
+ created_branch=True,
165
+ reused_existing_worktree=False,
166
+ )
167
+
168
+
169
+ def remove_task_worktree(
170
+ *,
171
+ root: Path,
172
+ worktree_repo_path: Path,
173
+ force: bool = True,
174
+ ) -> None:
175
+ args = ["worktree", "remove", str(worktree_repo_path)]
176
+ if force:
177
+ args.append("--force")
178
+ _run_git_checked(
179
+ root,
180
+ args,
181
+ error_message=f"failed to remove worktree {worktree_repo_path}",
182
+ )
183
+
184
+
185
+ def prune_worktrees(root: Path) -> None:
186
+ _run_git_checked(
187
+ root,
188
+ ["worktree", "prune"],
189
+ error_message="failed to prune worktrees",
190
+ )
@@ -0,0 +1,33 @@
1
+ # Lifecycle Hooks
2
+
3
+ This package implements command-based lifecycle hooks around sessions, prompts,
4
+ and tool calls.
5
+
6
+ ## Contents
7
+
8
+ - `config.py` loads user, project, and local hook configuration.
9
+ - `models.py` defines hook specs and event payloads.
10
+ - `dispatcher.py` executes hooks and interprets decisions.
11
+ - `trust.py` records trust for project hook configuration.
12
+ - `audit.py` writes hook audit artifacts.
13
+
14
+ ## Scope
15
+
16
+ Project hook configuration is executable policy and is not trusted by default.
17
+ Edits to that configuration require a new trust decision.
18
+
19
+ Blocking events can stop prompts or tool calls. Non-blocking events record hook
20
+ output but continue execution. Keep that distinction explicit in code and docs.
21
+
22
+ Hooks receive a filtered environment by default; avoid broadening environment
23
+ inheritance unless the security impact is clear.
24
+
25
+ ## Development
26
+
27
+ When changing event payloads or dispatcher behavior, update the hook protocol
28
+ tests and public hook documentation together.
29
+
30
+ ## See Also
31
+
32
+ - [Lifecycle hooks](../../../docs/hooks.md)
33
+ - [Security model](../../../docs/security_model.md)
@@ -0,0 +1,67 @@
1
+ from .audit import (
2
+ HOOK_AUDIT_ARTIFACT_PARTS,
3
+ HookAuditLogger,
4
+ build_hook_audit_event,
5
+ hook_audit_artifact_path,
6
+ hook_audit_log_path,
7
+ read_hook_audit_events,
8
+ )
9
+ from .config import (
10
+ ResolvedHookConfig,
11
+ ResolvedHookMatcherGroup,
12
+ load_hook_config_file,
13
+ load_resolved_hooks_config,
14
+ project_hooks_config_path,
15
+ project_local_hooks_config_path,
16
+ user_hooks_config_path,
17
+ )
18
+ from .dispatcher import HookDispatcher
19
+ from .models import (
20
+ CanonicalHookEventName,
21
+ HookConfigFile,
22
+ HookDispatchResult,
23
+ HookEventName,
24
+ canonicalize_hook_event_name,
25
+ )
26
+ from .trust import (
27
+ ProjectHooksTrustKey,
28
+ ProjectHooksTrustState,
29
+ is_project_hooks_config_trusted,
30
+ load_trust_state,
31
+ project_hooks_trust_key,
32
+ save_trust_state,
33
+ trust_project_hooks_config,
34
+ trust_state_path,
35
+ untrust_project_hooks_config,
36
+ )
37
+
38
+ __all__ = [
39
+ "CanonicalHookEventName",
40
+ "HOOK_AUDIT_ARTIFACT_PARTS",
41
+ "HookAuditLogger",
42
+ "HookConfigFile",
43
+ "HookDispatchResult",
44
+ "HookDispatcher",
45
+ "HookEventName",
46
+ "ProjectHooksTrustKey",
47
+ "ProjectHooksTrustState",
48
+ "ResolvedHookConfig",
49
+ "ResolvedHookMatcherGroup",
50
+ "build_hook_audit_event",
51
+ "canonicalize_hook_event_name",
52
+ "hook_audit_artifact_path",
53
+ "hook_audit_log_path",
54
+ "is_project_hooks_config_trusted",
55
+ "load_hook_config_file",
56
+ "load_resolved_hooks_config",
57
+ "load_trust_state",
58
+ "project_hooks_config_path",
59
+ "project_hooks_trust_key",
60
+ "project_local_hooks_config_path",
61
+ "read_hook_audit_events",
62
+ "save_trust_state",
63
+ "trust_project_hooks_config",
64
+ "trust_state_path",
65
+ "untrust_project_hooks_config",
66
+ "user_hooks_config_path",
67
+ ]
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Iterable
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..error_text import sanitize_error_summary
10
+ from ..session_store import sanitize_session_id
11
+ from .models import HookInvocationContext
12
+
13
+ HOOK_AUDIT_ARTIFACT_PARTS: tuple[str, str] = ("hooks", "hook_runs.jsonl")
14
+ _MAX_WARNING_PREVIEW_ITEMS = 3
15
+ _MAX_WARNING_PREVIEW_CHARS = 160
16
+
17
+
18
+ def _now_ts() -> str:
19
+ return datetime.now(UTC).isoformat()
20
+
21
+
22
+ def _truncate_preview(text: str, *, max_chars: int = _MAX_WARNING_PREVIEW_CHARS) -> str:
23
+ return sanitize_error_summary(str(text or ""), max_chars=max_chars)
24
+
25
+
26
+ def hook_audit_log_path(*, sessions_dir: Path, session_id: str) -> Path:
27
+ return (
28
+ sessions_dir
29
+ / sanitize_session_id(session_id)
30
+ / HOOK_AUDIT_ARTIFACT_PARTS[0]
31
+ / HOOK_AUDIT_ARTIFACT_PARTS[1]
32
+ )
33
+
34
+
35
+ def hook_audit_artifact_path(*, sessions_dir: Path, session_id: str) -> Path:
36
+ return hook_audit_log_path(sessions_dir=sessions_dir, session_id=session_id)
37
+
38
+
39
+ def _redacted_hook_payload(payload: dict[str, Any]) -> dict[str, Any]:
40
+ warning_items = payload.get("warnings")
41
+ warning_list = [str(item) for item in warning_items] if isinstance(warning_items, list) else []
42
+ return {
43
+ "event_name": str(payload.get("event_name") or ""),
44
+ "source_path": str(payload.get("source_path") or ""),
45
+ "source_scope": str(payload.get("source_scope") or ""),
46
+ "matcher": str(payload.get("matcher") or ""),
47
+ "hook_id": str(payload.get("hook_id") or ""),
48
+ "priority": int(payload.get("priority") or 0),
49
+ "failure_policy": str(payload.get("failure_policy") or ""),
50
+ "command_preview": _truncate_preview(
51
+ str(payload.get("command") or ""),
52
+ max_chars=180,
53
+ ),
54
+ "timeout_s": float(payload.get("timeout_s") or 0.0),
55
+ "trusted": bool(payload.get("trusted")),
56
+ "returncode": payload.get("returncode"),
57
+ "blocked": bool(payload.get("blocked")),
58
+ "modified_input": bool(payload.get("modified_input")),
59
+ "modified_input_fields": [
60
+ str(item)
61
+ for item in (payload.get("modified_input_fields") or [])
62
+ if str(item or "").strip()
63
+ ],
64
+ "modified_prompt": bool(payload.get("modified_prompt")),
65
+ "modified_prompt_chars": int(payload.get("modified_prompt_chars") or 0),
66
+ "additional_system_message_count": int(payload.get("additional_system_message_count") or 0),
67
+ "additional_user_message_count": int(payload.get("additional_user_message_count") or 0),
68
+ "system_notices_count": int(payload.get("system_notices_count") or 0),
69
+ "halt_requested": bool(payload.get("halt_requested")),
70
+ "allow_short_circuited": bool(payload.get("allow_short_circuited")),
71
+ "suppress_output": bool(payload.get("suppress_output")),
72
+ "permission_decision": str(payload.get("permission_decision") or ""),
73
+ "stop_reason_preview": _truncate_preview(
74
+ str(payload.get("stop_reason") or ""),
75
+ max_chars=180,
76
+ ),
77
+ "stdout_chars": int(payload.get("stdout_chars") or 0),
78
+ "stderr_chars": int(payload.get("stderr_chars") or 0),
79
+ "duration_ms": int(payload.get("duration_ms") or 0),
80
+ "status": str(payload.get("status") or ""),
81
+ "warning_count": len(warning_list),
82
+ "warnings_preview": [
83
+ _truncate_preview(item) for item in warning_list[:_MAX_WARNING_PREVIEW_ITEMS]
84
+ ],
85
+ "payload_truncated": bool(payload.get("payload_truncated")),
86
+ "payload_bytes": int(payload.get("payload_bytes") or 0),
87
+ }
88
+
89
+
90
+ class HookAuditLogger:
91
+ def __init__(self, *, path: Path, session_id: str) -> None:
92
+ self.path = path
93
+ self.session_id = session_id
94
+
95
+ def append(self, event: dict[str, Any]) -> None:
96
+ self.path.parent.mkdir(parents=True, exist_ok=True)
97
+ with self.path.open("a", encoding="utf-8") as handle:
98
+ handle.write(json.dumps(event, ensure_ascii=True) + "\n")
99
+
100
+ def close(self) -> None:
101
+ return None
102
+
103
+
104
+ def read_hook_audit_events(path: Path) -> Iterable[dict[str, Any]]:
105
+ if not path.exists():
106
+ return
107
+ for line in path.read_text(encoding="utf-8").splitlines():
108
+ line = line.strip()
109
+ if not line:
110
+ continue
111
+ try:
112
+ obj = json.loads(line)
113
+ except json.JSONDecodeError:
114
+ continue
115
+ if isinstance(obj, dict):
116
+ yield obj
117
+
118
+
119
+ def build_hook_audit_event(
120
+ *,
121
+ session_id: str,
122
+ context: HookInvocationContext,
123
+ ) -> dict[str, Any]:
124
+ payload = {
125
+ "event_name": context.event_name,
126
+ "source_path": context.source_path,
127
+ "source_scope": context.source_scope,
128
+ "matcher": context.matcher,
129
+ "hook_id": context.hook_id,
130
+ "priority": context.priority,
131
+ "failure_policy": context.failure_policy,
132
+ "command": context.command,
133
+ "timeout_s": context.timeout_s,
134
+ "trusted": context.trusted,
135
+ "returncode": context.returncode,
136
+ "blocked": context.blocked,
137
+ "modified_input": context.modified_input,
138
+ "modified_input_fields": list(context.modified_input_fields),
139
+ "modified_prompt": context.modified_prompt,
140
+ "modified_prompt_chars": context.modified_prompt_chars,
141
+ "additional_system_message_count": context.additional_system_message_count,
142
+ "additional_user_message_count": context.additional_user_message_count,
143
+ "system_notices_count": context.system_notices_count,
144
+ "halt_requested": context.halt_requested,
145
+ "allow_short_circuited": context.allow_short_circuited,
146
+ "suppress_output": context.suppress_output,
147
+ "permission_decision": context.permission_decision,
148
+ "stop_reason": context.stop_reason,
149
+ "stdout_chars": context.stdout_chars,
150
+ "stderr_chars": context.stderr_chars,
151
+ "duration_ms": context.duration_ms,
152
+ "status": context.status,
153
+ "warnings": list(context.warnings),
154
+ "payload_truncated": context.payload_truncated,
155
+ "payload_bytes": context.payload_bytes,
156
+ }
157
+ redacted = _redacted_hook_payload(payload)
158
+ redacted["type"] = "hook_command"
159
+ redacted["ts"] = _now_ts()
160
+ redacted["session_id"] = session_id
161
+ return redacted
162
+
163
+
164
+ __all__ = [
165
+ "HOOK_AUDIT_ARTIFACT_PARTS",
166
+ "HookAuditLogger",
167
+ "build_hook_audit_event",
168
+ "hook_audit_artifact_path",
169
+ "hook_audit_log_path",
170
+ "read_hook_audit_events",
171
+ ]
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from pydantic import ValidationError
9
+
10
+ from ..branding import canonical_user_config_dir, env_get
11
+ from ..config import ConfigError
12
+ from .models import (
13
+ CanonicalHookEventName,
14
+ CommandHookSpec,
15
+ HookConfigFile,
16
+ HookEventName,
17
+ canonicalize_hook_event_name,
18
+ )
19
+ from .trust import is_project_hooks_config_trusted, load_trust_state
20
+
21
+
22
+ def _hooks_config_dir() -> Path:
23
+ override = env_get("ALYSIS_CONFIG_DIR")
24
+ if override:
25
+ return Path(override)
26
+ return canonical_user_config_dir()
27
+
28
+
29
+ def user_hooks_config_path() -> Path:
30
+ return _hooks_config_dir() / "hooks.json"
31
+
32
+
33
+ def project_hooks_config_path(workspace_root: Path) -> Path:
34
+ return workspace_root.resolve() / ".alysis" / "hooks.json"
35
+
36
+
37
+ def project_local_hooks_config_path(workspace_root: Path) -> Path:
38
+ return workspace_root.resolve() / ".alysis" / "hooks.local.json"
39
+
40
+
41
+ def _read_json_object(path: Path) -> dict[str, Any]:
42
+ try:
43
+ raw = json.loads(path.read_text(encoding="utf-8"))
44
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError) as exc:
45
+ raise ConfigError(f"Failed to read hooks config: {path}") from exc
46
+ if not isinstance(raw, dict):
47
+ raise ConfigError(f"Invalid hooks config format (expected JSON object): {path}")
48
+ return raw
49
+
50
+
51
+ def _format_validation_error(path: Path, exc: ValidationError) -> ConfigError:
52
+ details: list[str] = []
53
+ for err in exc.errors(include_url=False):
54
+ loc = ".".join(str(part) for part in err.get("loc", ()))
55
+ msg = str(err.get("msg") or "invalid value")
56
+ detail = f"{path}: {msg}" if not loc else f"{path}: {loc}: {msg}"
57
+ details.append(detail)
58
+ joined = "\n".join(details) if details else f"{path}: invalid hooks config"
59
+ return ConfigError(f"Invalid hooks config:\n{joined}")
60
+
61
+
62
+ def _load_hook_config_file(path: Path) -> HookConfigFile:
63
+ raw = _read_json_object(path)
64
+ try:
65
+ return HookConfigFile.model_validate(raw)
66
+ except ValidationError as exc:
67
+ raise _format_validation_error(path, exc) from exc
68
+
69
+
70
+ def load_hook_config_file(path: Path) -> HookConfigFile:
71
+ return _load_hook_config_file(path)
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class ResolvedHookMatcherGroup:
76
+ event_name: CanonicalHookEventName
77
+ matcher: str
78
+ hooks: tuple[CommandHookSpec, ...]
79
+ source_path: Path
80
+ source_scope: str
81
+ trusted: bool = True
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ResolvedHookConfig:
86
+ groups_by_event: dict[str, tuple[ResolvedHookMatcherGroup, ...]] = field(default_factory=dict)
87
+ loaded_paths: tuple[Path, ...] = ()
88
+ untrusted_project_paths: tuple[Path, ...] = ()
89
+
90
+ @property
91
+ def has_any_hooks(self) -> bool:
92
+ return any(groups for groups in self.groups_by_event.values())
93
+
94
+ def groups_for_event(self, event_name: HookEventName) -> tuple[ResolvedHookMatcherGroup, ...]:
95
+ return self.groups_by_event.get(canonicalize_hook_event_name(event_name), ())
96
+
97
+
98
+ def _resolve_groups_for_file(
99
+ *,
100
+ config_file: HookConfigFile,
101
+ source_path: Path,
102
+ source_scope: str,
103
+ trusted: bool,
104
+ ) -> dict[str, list[ResolvedHookMatcherGroup]]:
105
+ resolved: dict[str, list[ResolvedHookMatcherGroup]] = {}
106
+ for event_name, groups in config_file.hooks.items():
107
+ event_groups: list[ResolvedHookMatcherGroup] = []
108
+ for group in groups:
109
+ if not group.enabled:
110
+ continue
111
+ enabled_hooks = tuple(
112
+ sorted(
113
+ (hook for hook in group.hooks if hook.enabled),
114
+ key=lambda hook: (-hook.priority, hook.id or "", hook.command),
115
+ )
116
+ )
117
+ if not enabled_hooks:
118
+ continue
119
+ event_groups.append(
120
+ ResolvedHookMatcherGroup(
121
+ event_name=event_name,
122
+ matcher=str(group.matcher or ""),
123
+ hooks=enabled_hooks,
124
+ source_path=source_path,
125
+ source_scope=source_scope,
126
+ trusted=trusted,
127
+ )
128
+ )
129
+ if event_groups:
130
+ resolved[event_name] = event_groups
131
+ return resolved
132
+
133
+
134
+ def _drop_overridden_hooks(
135
+ groups: list[ResolvedHookMatcherGroup],
136
+ *,
137
+ overridden_ids: set[str],
138
+ ) -> list[ResolvedHookMatcherGroup]:
139
+ if not overridden_ids:
140
+ return groups
141
+ retained: list[ResolvedHookMatcherGroup] = []
142
+ for group in groups:
143
+ remaining_hooks = tuple(hook for hook in group.hooks if hook.id not in overridden_ids)
144
+ if not remaining_hooks:
145
+ continue
146
+ if remaining_hooks == group.hooks:
147
+ retained.append(group)
148
+ continue
149
+ retained.append(
150
+ ResolvedHookMatcherGroup(
151
+ event_name=group.event_name,
152
+ matcher=group.matcher,
153
+ hooks=remaining_hooks,
154
+ source_path=group.source_path,
155
+ source_scope=group.source_scope,
156
+ trusted=group.trusted,
157
+ )
158
+ )
159
+ return retained
160
+
161
+
162
+ def _merge_event_groups(
163
+ *,
164
+ existing: list[ResolvedHookMatcherGroup],
165
+ incoming: list[ResolvedHookMatcherGroup],
166
+ ) -> list[ResolvedHookMatcherGroup]:
167
+ override_ids = {hook.id for group in incoming for hook in group.hooks if hook.id is not None}
168
+ merged = _drop_overridden_hooks(existing, overridden_ids=override_ids)
169
+ merged.extend(incoming)
170
+ return merged
171
+
172
+
173
+ def load_resolved_hooks_config(workspace_root: Path) -> ResolvedHookConfig:
174
+ workspace_root = workspace_root.resolve()
175
+ candidate_paths = (
176
+ (user_hooks_config_path(), "user"),
177
+ (project_hooks_config_path(workspace_root), "project"),
178
+ (project_local_hooks_config_path(workspace_root), "project_local"),
179
+ )
180
+ trust_state = load_trust_state()
181
+ merged: dict[str, list[ResolvedHookMatcherGroup]] = {}
182
+ loaded_paths: list[Path] = []
183
+ untrusted_project_paths: list[Path] = []
184
+ for path, source_scope in candidate_paths:
185
+ if not path.exists():
186
+ continue
187
+ trusted = source_scope != "project" or is_project_hooks_config_trusted(
188
+ workspace_root=workspace_root,
189
+ config_path=path,
190
+ state=trust_state,
191
+ )
192
+ loaded_paths.append(path)
193
+ if not trusted:
194
+ untrusted_project_paths.append(path)
195
+ continue
196
+ config_file = _load_hook_config_file(path)
197
+ resolved = _resolve_groups_for_file(
198
+ config_file=config_file,
199
+ source_path=path,
200
+ source_scope=source_scope,
201
+ trusted=trusted,
202
+ )
203
+ if not resolved:
204
+ continue
205
+ for event_name, groups in resolved.items():
206
+ merged[event_name] = _merge_event_groups(
207
+ existing=merged.get(event_name, []),
208
+ incoming=groups,
209
+ )
210
+ return ResolvedHookConfig(
211
+ groups_by_event={event_name: tuple(groups) for event_name, groups in merged.items()},
212
+ loaded_paths=tuple(loaded_paths),
213
+ untrusted_project_paths=tuple(untrusted_project_paths),
214
+ )
215
+
216
+
217
+ __all__ = [
218
+ "ResolvedHookConfig",
219
+ "ResolvedHookMatcherGroup",
220
+ "load_hook_config_file",
221
+ "load_resolved_hooks_config",
222
+ "project_hooks_config_path",
223
+ "project_local_hooks_config_path",
224
+ "user_hooks_config_path",
225
+ ]