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,737 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import typer
10
+
11
+ from ...config import ConfigError, load_config
12
+ from ...hooks import (
13
+ canonicalize_hook_event_name,
14
+ hook_audit_artifact_path,
15
+ is_project_hooks_config_trusted,
16
+ load_hook_config_file,
17
+ load_resolved_hooks_config,
18
+ load_trust_state,
19
+ project_hooks_config_path,
20
+ project_local_hooks_config_path,
21
+ read_hook_audit_events,
22
+ trust_project_hooks_config,
23
+ untrust_project_hooks_config,
24
+ user_hooks_config_path,
25
+ )
26
+ from ...runtime_kind import RuntimeKind, normalize_runtime_kind
27
+ from ...session_store import filter_sessions_to_local_owner, list_sessions, resolve_sessions_dir
28
+ from ...surface.styles import STYLE_CONTENT
29
+ from . import _patchable
30
+ from ._shared import _console, _resolve_tool_workspace_root, _Table
31
+
32
+ hooks_app = typer.Typer(
33
+ add_completion=False,
34
+ help="Lifecycle hook inspection, trust, toggle, and audit commands.",
35
+ )
36
+
37
+
38
+ def _project_hooks_config_or_exit(
39
+ *,
40
+ path: Path,
41
+ console: Any,
42
+ validate: bool = True,
43
+ ) -> tuple[Path, Path]:
44
+ workspace_root = _resolve_tool_workspace_root(path=path)
45
+ config_path = project_hooks_config_path(workspace_root)
46
+ if not config_path.exists():
47
+ console.print(f"[red]Project hooks config not found:[/red] {config_path}")
48
+ raise typer.Exit(code=1)
49
+ if validate:
50
+ try:
51
+ load_hook_config_file(config_path)
52
+ except ConfigError as exc:
53
+ console.print(f"[red]{exc}[/red]")
54
+ raise typer.Exit(code=1) from exc
55
+ return workspace_root, config_path
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class _HookSourceStatus:
60
+ source_scope: str
61
+ path: Path
62
+ exists: bool
63
+ trusted: bool
64
+ status: str
65
+ event_count: int = 0
66
+ hook_count: int = 0
67
+ issue: str = ""
68
+
69
+
70
+ def _collect_hook_source_statuses(*, workspace_root: Path) -> list[_HookSourceStatus]:
71
+ trust_state = load_trust_state()
72
+ candidate_paths = (
73
+ ("user", user_hooks_config_path()),
74
+ ("project", project_hooks_config_path(workspace_root)),
75
+ ("project_local", project_local_hooks_config_path(workspace_root)),
76
+ )
77
+ statuses: list[_HookSourceStatus] = []
78
+ for source_scope, hook_config_path in candidate_paths:
79
+ if not hook_config_path.exists():
80
+ statuses.append(
81
+ _HookSourceStatus(
82
+ source_scope=source_scope,
83
+ path=hook_config_path,
84
+ exists=False,
85
+ trusted=(source_scope != "project"),
86
+ status="missing",
87
+ )
88
+ )
89
+ continue
90
+ trusted = source_scope != "project" or is_project_hooks_config_trusted(
91
+ workspace_root=workspace_root,
92
+ config_path=hook_config_path,
93
+ state=trust_state,
94
+ )
95
+ try:
96
+ config_file = load_hook_config_file(hook_config_path)
97
+ except ConfigError as exc:
98
+ statuses.append(
99
+ _HookSourceStatus(
100
+ source_scope=source_scope,
101
+ path=hook_config_path,
102
+ exists=True,
103
+ trusted=trusted,
104
+ status="invalid",
105
+ issue=str(exc),
106
+ )
107
+ )
108
+ continue
109
+ event_count = len(config_file.hooks)
110
+ hook_count = sum(
111
+ len(group.hooks) for groups in config_file.hooks.values() for group in groups
112
+ )
113
+ statuses.append(
114
+ _HookSourceStatus(
115
+ source_scope=source_scope,
116
+ path=hook_config_path,
117
+ exists=True,
118
+ trusted=trusted,
119
+ status="loaded" if trusted or source_scope != "project" else "untrusted",
120
+ event_count=event_count,
121
+ hook_count=hook_count,
122
+ )
123
+ )
124
+ return statuses
125
+
126
+
127
+ _HOOK_TOOL_EVENTS = {"PreToolUse", "PostToolUse"}
128
+ _HOOK_SESSION_SOURCES = {"startup", "resume", "fork"}
129
+
130
+
131
+ def _hook_test_match_result(
132
+ *,
133
+ event_name: str,
134
+ matcher_target: str,
135
+ runtime_kind: str,
136
+ session_source: str,
137
+ matcher: str,
138
+ hook: Any,
139
+ ) -> tuple[bool, str]:
140
+ if hook.runtime_kinds and runtime_kind not in set(hook.runtime_kinds):
141
+ return False, "runtime_kind mismatch"
142
+ if hook.session_source and session_source not in set(hook.session_source):
143
+ return False, "session_source mismatch"
144
+ if event_name in _HOOK_TOOL_EVENTS:
145
+ if not matcher_target:
146
+ return False, "missing tool target"
147
+ if matcher:
148
+ try:
149
+ if re.search(matcher, matcher_target) is None:
150
+ return False, "matcher miss"
151
+ except re.error as exc:
152
+ return False, f"invalid matcher: {exc}"
153
+ return True, "matched"
154
+
155
+
156
+ @hooks_app.command("list")
157
+ def hooks_list(
158
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
159
+ ) -> None:
160
+ console = _console()
161
+ workspace_root = _resolve_tool_workspace_root(path=path)
162
+ entries: list[tuple[str, str, str, str, str, str, str, str, str, str, str]] = []
163
+ for status in _collect_hook_source_statuses(workspace_root=workspace_root):
164
+ if not status.exists or status.status == "invalid":
165
+ continue
166
+ try:
167
+ config_file = load_hook_config_file(status.path)
168
+ except ConfigError:
169
+ continue
170
+ for event_name, groups in config_file.hooks.items():
171
+ for group in groups:
172
+ if not group.enabled:
173
+ continue
174
+ for hook in sorted(
175
+ (item for item in group.hooks if item.enabled),
176
+ key=lambda item: (-item.priority, item.id or "", item.command),
177
+ ):
178
+ entries.append(
179
+ (
180
+ status.source_scope,
181
+ status.status,
182
+ "trusted" if status.trusted else "untrusted",
183
+ event_name,
184
+ group.matcher or "*",
185
+ hook.id or "-",
186
+ str(hook.priority),
187
+ hook.failure_policy,
188
+ ",".join(hook.runtime_kinds or ()) or "-",
189
+ ",".join(hook.session_source or ()) or "-",
190
+ hook.command,
191
+ )
192
+ )
193
+ table = _Table(title=f"Hooks ({len(entries)})")
194
+ table.add_column("scope")
195
+ table.add_column("status")
196
+ table.add_column("trust")
197
+ table.add_column("event")
198
+ table.add_column("matcher")
199
+ table.add_column("id")
200
+ table.add_column("priority", justify="right")
201
+ table.add_column("failure")
202
+ table.add_column("runtime_kinds")
203
+ table.add_column("session_source")
204
+ table.add_column("command")
205
+ for row in entries:
206
+ table.add_row(*row)
207
+ console.print(table)
208
+ for row in entries:
209
+ if row[5] != "-":
210
+ console.print(
211
+ f"[dim]hook:[/dim] event={row[3]} id={row[5]} "
212
+ f"runtime_kinds={row[8]} session_source={row[9]}"
213
+ )
214
+ console.print(f"[dim]Workspace root:[/dim] {workspace_root.as_posix()}")
215
+
216
+
217
+ @hooks_app.command("doctor")
218
+ def hooks_doctor(
219
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
220
+ ) -> None:
221
+ console = _console()
222
+ workspace_root = _resolve_tool_workspace_root(path=path)
223
+ statuses = _collect_hook_source_statuses(workspace_root=workspace_root)
224
+ table = _Table(title="hooks doctor")
225
+ table.add_column("source")
226
+ table.add_column("status")
227
+ table.add_column("trust")
228
+ table.add_column("events", justify="right")
229
+ table.add_column("hooks", justify="right")
230
+ table.add_column("path")
231
+ for status in statuses:
232
+ table.add_row(
233
+ status.source_scope,
234
+ status.status,
235
+ "trusted" if status.trusted else "untrusted",
236
+ str(status.event_count),
237
+ str(status.hook_count),
238
+ status.path.as_posix(),
239
+ )
240
+ console.print(table)
241
+ for status in statuses:
242
+ console.print(f"[dim]{status.source_scope} path:[/dim] {status.path.as_posix()}")
243
+ for status in statuses:
244
+ if status.issue:
245
+ console.print(
246
+ f"[yellow]Hook config issue:[/yellow] {status.path.as_posix()} ({status.issue})"
247
+ )
248
+ try:
249
+ resolved = load_resolved_hooks_config(workspace_root)
250
+ console.print(
251
+ "Effective loaded hooks: "
252
+ + ", ".join(
253
+ f"{event}={len(groups)}" for event, groups in resolved.groups_by_event.items()
254
+ )
255
+ if resolved.groups_by_event
256
+ else "Effective loaded hooks: none"
257
+ )
258
+ for event_name, groups in resolved.groups_by_event.items():
259
+ for group in groups:
260
+ if not group.matcher:
261
+ continue
262
+ try:
263
+ re.compile(group.matcher)
264
+ except re.error as exc:
265
+ console.print(
266
+ f"[red]Matcher error:[/red] {event_name} in {group.source_path.as_posix()}: "
267
+ f"invalid regex {group.matcher!r} ({exc})"
268
+ )
269
+ for untrusted in resolved.untrusted_project_paths:
270
+ console.print(
271
+ f"[yellow]Untrusted project hooks config (ignored):[/yellow] "
272
+ f"{untrusted.as_posix()}. Run "
273
+ f"`alysis hooks trust --path {workspace_root.as_posix()}` to allow it."
274
+ )
275
+ except ConfigError as exc:
276
+ console.print(f"[yellow]Effective load failed:[/yellow] {exc}")
277
+
278
+
279
+ @hooks_app.command("trace")
280
+ def hooks_trace(
281
+ session_id: str | None = typer.Argument(
282
+ None,
283
+ help="Session id (filename stem). If omitted, reads the latest retained session.",
284
+ ),
285
+ limit: int = typer.Option(200, "--limit", min=1, help="Maximum events to display."),
286
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
287
+ ) -> None:
288
+ console = _console()
289
+ cfg = _patchable("load_config", load_config)()
290
+ sessions_dir = resolve_sessions_dir(cfg)
291
+ resolved_session_id = session_id
292
+ if not resolved_session_id:
293
+ all_infos = _patchable("list_sessions", list_sessions)(sessions_dir)
294
+ infos = filter_sessions_to_local_owner(all_infos)
295
+ if not infos:
296
+ if all_infos:
297
+ console.print(
298
+ f"No sessions owned by this account in {sessions_dir} "
299
+ f"({len(all_infos)} recorded by a different account; "
300
+ "pass an explicit session id)."
301
+ )
302
+ else:
303
+ console.print(f"No retained sessions found in {sessions_dir}")
304
+ return
305
+ resolved_session_id = infos[0].session_id
306
+ artifact_path = hook_audit_artifact_path(
307
+ sessions_dir=sessions_dir,
308
+ session_id=resolved_session_id,
309
+ )
310
+ events = list(read_hook_audit_events(artifact_path))
311
+ if not events:
312
+ console.print(f"No hook audit log found: {artifact_path}")
313
+ return
314
+ if json_output:
315
+ console.print_json(json.dumps(events))
316
+ return
317
+ table = _Table(title=f"Hook Trace ({resolved_session_id})")
318
+ table.add_column("ts")
319
+ table.add_column("event")
320
+ table.add_column("hook_id")
321
+ table.add_column("status")
322
+ table.add_column("trust")
323
+ table.add_column("scope")
324
+ table.add_column("duration_ms", justify="right")
325
+ table.add_column("warnings")
326
+ for event in events[-limit:]:
327
+ payload = event.get("payload")
328
+ event_payload = payload if isinstance(payload, dict) else event
329
+ warning_previews = event_payload.get("warning_previews") or []
330
+ warning_text = (
331
+ "; ".join(str(item) for item in warning_previews[:2]) if warning_previews else "-"
332
+ )
333
+ table.add_row(
334
+ str(event.get("ts") or "-"),
335
+ str(event_payload.get("event_name") or "-"),
336
+ str(event_payload.get("hook_id") or "-"),
337
+ str(event_payload.get("status") or "-"),
338
+ "trusted" if bool(event_payload.get("trusted")) else "untrusted",
339
+ str(event_payload.get("source_scope") or "-"),
340
+ str(int(event_payload.get("duration_ms") or 0)),
341
+ warning_text,
342
+ )
343
+ console.print(table)
344
+ console.print(f"[dim]Artifact:[/dim] {artifact_path.as_posix()}")
345
+
346
+
347
+ @hooks_app.command("test")
348
+ def hooks_test(
349
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
350
+ event: str = typer.Option("SessionStart", "--event", help="Hook event name."),
351
+ tool: str = typer.Option("", "--tool", help="Tool name for Pre/PostToolUse dry-runs."),
352
+ runtime_kind: str = typer.Option(
353
+ RuntimeKind.INTERACTIVE_CHAT.value,
354
+ "--runtime-kind",
355
+ help="Runtime kind for hook matching.",
356
+ ),
357
+ session_source: str = typer.Option(
358
+ "startup",
359
+ "--session-source",
360
+ help="Session source for SessionStart hook matching.",
361
+ ),
362
+ ) -> None:
363
+ console = _console()
364
+ workspace_root = _resolve_tool_workspace_root(path=path)
365
+ try:
366
+ canonical_event = canonicalize_hook_event_name(event)
367
+ except ValueError as exc:
368
+ console.print(f"[red]{exc}[/red]")
369
+ raise typer.Exit(code=2) from exc
370
+ try:
371
+ resolved_runtime_kind = normalize_runtime_kind(runtime_kind).value
372
+ except ConfigError as exc:
373
+ console.print(f"[red]{exc}[/red]")
374
+ raise typer.Exit(code=2) from exc
375
+ normalized_session_source = str(session_source or "").strip().lower()
376
+ if normalized_session_source not in _HOOK_SESSION_SOURCES:
377
+ console.print("[red]session_source must be one of: startup, resume, fork[/red]")
378
+ raise typer.Exit(code=2)
379
+
380
+ resolved = load_resolved_hooks_config(workspace_root)
381
+ groups = resolved.groups_for_event(canonical_event)
382
+ if not groups:
383
+ console.print(f"No active hooks for event {canonical_event}.")
384
+ return
385
+
386
+ table = _Table(title=f"Hooks Test ({canonical_event})")
387
+ detail_rows: list[tuple[str, str, str, str]] = []
388
+ table.add_column("scope")
389
+ table.add_column("trust")
390
+ table.add_column("matcher")
391
+ table.add_column("id")
392
+ table.add_column("runtime_kinds")
393
+ table.add_column("session_source")
394
+ table.add_column("match")
395
+ table.add_column("reason")
396
+ table.add_column("command")
397
+ for group in groups:
398
+ matcher = str(group.matcher or "")
399
+ for hook in group.hooks:
400
+ matched, reason = _hook_test_match_result(
401
+ event_name=canonical_event,
402
+ matcher_target=str(tool or "").strip(),
403
+ runtime_kind=resolved_runtime_kind,
404
+ session_source=normalized_session_source,
405
+ matcher=matcher,
406
+ hook=hook,
407
+ )
408
+ table.add_row(
409
+ group.source_scope,
410
+ "trusted" if group.trusted else "untrusted",
411
+ matcher or "*",
412
+ hook.id or "-",
413
+ ",".join(hook.runtime_kinds or ()) or "-",
414
+ ",".join(hook.session_source or ()) or "-",
415
+ "yes" if matched else "no",
416
+ reason,
417
+ hook.command,
418
+ )
419
+ detail_rows.append((hook.id or "-", "yes" if matched else "no", reason, hook.command))
420
+ console.print(table)
421
+ for hook_id, matched, reason, command in detail_rows:
422
+ if hook_id != "-":
423
+ console.print(
424
+ f"[dim]hook_test:[/dim] id={hook_id} match={matched} "
425
+ f"reason={reason} command={command}"
426
+ )
427
+ if resolved.untrusted_project_paths:
428
+ console.print(
429
+ "[yellow]Ignored untrusted project hook configs:[/yellow] "
430
+ + ", ".join(path.as_posix() for path in resolved.untrusted_project_paths)
431
+ )
432
+
433
+
434
+ @hooks_app.command("trust")
435
+ def hooks_trust(
436
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
437
+ ) -> None:
438
+ console = _console()
439
+ workspace_root, config_path = _project_hooks_config_or_exit(path=path, console=console)
440
+ trust_project_hooks_config(workspace_root=workspace_root, config_path=config_path)
441
+ console.print(f"Trusted project hooks config: {config_path}")
442
+
443
+
444
+ @hooks_app.command("untrust")
445
+ def hooks_untrust(
446
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
447
+ ) -> None:
448
+ console = _console()
449
+ workspace_root, config_path = _project_hooks_config_or_exit(
450
+ path=path,
451
+ console=console,
452
+ validate=False,
453
+ )
454
+ untrust_project_hooks_config(workspace_root=workspace_root, config_path=config_path)
455
+ console.print(f"Untrusted project hooks config: {config_path}")
456
+
457
+
458
+ _HOOKS_INIT_TEMPLATE: dict[str, Any] = {
459
+ "schema_version": 1,
460
+ "hooks": {
461
+ "SessionStart": [
462
+ {
463
+ "hooks": [
464
+ {
465
+ "type": "command",
466
+ "id": "starter.session-start-log",
467
+ "description": "Log a readable line when the session begins.",
468
+ "command": 'echo "[hooks] session started" >> .alysis/demo-hooks.log',
469
+ "enabled": False,
470
+ }
471
+ ]
472
+ }
473
+ ],
474
+ "PreToolUse": [
475
+ {
476
+ "matcher": "shell_run",
477
+ "hooks": [
478
+ {
479
+ "type": "command",
480
+ "id": "starter.block-dangerous-shell",
481
+ "description": "Block obviously dangerous shell commands.",
482
+ "command": "python3 docs/examples/hooks/block_dangerous.py",
483
+ "enabled": False,
484
+ }
485
+ ],
486
+ }
487
+ ],
488
+ },
489
+ }
490
+
491
+
492
+ @hooks_app.command("init")
493
+ def hooks_init(
494
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
495
+ force: bool = typer.Option(
496
+ False, "--force", help="Overwrite an existing .alysis/hooks.local.json file."
497
+ ),
498
+ ) -> None:
499
+ console = _console()
500
+ workspace_root = _resolve_tool_workspace_root(path=path)
501
+ local_config_path = project_local_hooks_config_path(workspace_root)
502
+ if local_config_path.exists() and not force:
503
+ console.print(
504
+ f"[yellow]Hooks config already exists:[/yellow] {local_config_path.as_posix()}"
505
+ )
506
+ console.print("Use --force to overwrite.")
507
+ raise typer.Exit(code=1)
508
+ local_config_path.parent.mkdir(parents=True, exist_ok=True)
509
+ local_config_path.write_text(
510
+ json.dumps(_HOOKS_INIT_TEMPLATE, indent=2) + "\n",
511
+ encoding="utf-8",
512
+ )
513
+ console.print(f"Wrote starter hooks config: {local_config_path.as_posix()}")
514
+ gitignore_path = workspace_root / ".gitignore"
515
+ gitignore_entry = ".alysis/hooks.local.json"
516
+ if gitignore_path.exists():
517
+ existing = gitignore_path.read_text(encoding="utf-8")
518
+ already_ignored = any(
519
+ line.strip() == gitignore_entry or line.strip() == ".alysis/"
520
+ for line in existing.splitlines()
521
+ )
522
+ if not already_ignored:
523
+ with gitignore_path.open("a", encoding="utf-8") as handle:
524
+ if not existing.endswith("\n"):
525
+ handle.write("\n")
526
+ handle.write(f"{gitignore_entry}\n")
527
+ console.print(f"Appended .gitignore entry: {gitignore_entry}")
528
+ console.print("[dim]Local config is trusted by location; no `alysis hooks trust` needed.[/dim]")
529
+
530
+
531
+ @hooks_app.command("effective")
532
+ def hooks_effective(
533
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
534
+ event: str = typer.Option(..., "--event", help="Hook event name to resolve."),
535
+ tool: str | None = typer.Option(
536
+ None,
537
+ "--tool",
538
+ help="Tool name for matcher resolution (PreToolUse/PostToolUse/SubagentStop).",
539
+ ),
540
+ runtime: str = typer.Option("interactive_chat", "--runtime", help="Runtime kind to evaluate."),
541
+ session_source: str | None = typer.Option(
542
+ None, "--session-source", help="Session source (startup / resume / fork)."
543
+ ),
544
+ ) -> None:
545
+ console = _console()
546
+ workspace_root = _resolve_tool_workspace_root(path=path)
547
+ try:
548
+ canonical_event = canonicalize_hook_event_name(event)
549
+ except ValueError as exc:
550
+ console.print(f"[red]Invalid event:[/red] {exc}")
551
+ raise typer.Exit(code=1) from exc
552
+ resolved = load_resolved_hooks_config(workspace_root)
553
+ groups = resolved.groups_for_event(canonical_event)
554
+ table = _Table(title=f"Effective hooks for {canonical_event}")
555
+ table.add_column("order", justify="right")
556
+ table.add_column("hook_id")
557
+ table.add_column("fires")
558
+ table.add_column("reason")
559
+ table.add_column("priority", justify="right")
560
+ table.add_column("source")
561
+ table.add_column("command")
562
+ order = 0
563
+ for group in groups:
564
+ matcher_pass = True
565
+ matcher_reason = ""
566
+ if tool is not None and canonical_event in {"PreToolUse", "PostToolUse", "SubagentStop"}:
567
+ if group.matcher:
568
+ try:
569
+ matcher_pass = re.compile(group.matcher).search(tool) is not None
570
+ if not matcher_pass:
571
+ matcher_reason = f"matcher {group.matcher!r} does not match {tool!r}"
572
+ except re.error as exc:
573
+ matcher_pass = False
574
+ matcher_reason = f"matcher regex error: {exc}"
575
+ for hook in group.hooks:
576
+ order += 1
577
+ fires = matcher_pass
578
+ reason = matcher_reason
579
+ if fires and hook.runtime_kinds and runtime not in set(hook.runtime_kinds):
580
+ fires = False
581
+ reason = f"runtime_kind {runtime!r} not in {list(hook.runtime_kinds)}"
582
+ if fires and hook.session_source:
583
+ if session_source is None:
584
+ fires = False
585
+ reason = "sessionSource filter set but --session-source not provided"
586
+ elif session_source not in set(hook.session_source):
587
+ fires = False
588
+ reason = f"session_source {session_source!r} not in {list(hook.session_source)}"
589
+ table.add_row(
590
+ str(order),
591
+ hook.id or "-",
592
+ "yes" if fires else "no",
593
+ reason or "-",
594
+ str(hook.priority),
595
+ f"{group.source_scope}:{group.source_path.name}",
596
+ hook.command,
597
+ )
598
+ if order == 0:
599
+ console.print(f"No effective hooks for {canonical_event}.")
600
+ return
601
+ console.print(table)
602
+
603
+
604
+ def _find_hook_in_layer(
605
+ *,
606
+ path: Path,
607
+ hook_id: str,
608
+ ) -> tuple[dict[str, Any], dict[str, Any]] | None:
609
+ raw = json.loads(path.read_text(encoding="utf-8"))
610
+ hooks_root = raw.get("hooks")
611
+ if not isinstance(hooks_root, dict):
612
+ return None
613
+ for groups in hooks_root.values():
614
+ if not isinstance(groups, list):
615
+ continue
616
+ for group in groups:
617
+ if not isinstance(group, dict):
618
+ continue
619
+ hook_list = group.get("hooks")
620
+ if not isinstance(hook_list, list):
621
+ continue
622
+ for entry in hook_list:
623
+ if isinstance(entry, dict) and str(entry.get("id") or "") == hook_id:
624
+ return raw, entry
625
+ return None
626
+
627
+
628
+ def _hooks_config_path_for_layer(
629
+ *,
630
+ workspace_root: Path,
631
+ layer: str,
632
+ ) -> Path:
633
+ if layer == "user":
634
+ return user_hooks_config_path()
635
+ if layer == "project":
636
+ return project_hooks_config_path(workspace_root)
637
+ if layer == "local":
638
+ return project_local_hooks_config_path(workspace_root)
639
+ raise typer.BadParameter("layer must be one of: user, project, local.")
640
+
641
+
642
+ def _set_hook_enabled(
643
+ *,
644
+ path: Path,
645
+ hook_id: str,
646
+ enabled: bool,
647
+ console: Any,
648
+ ) -> None:
649
+ if not path.exists():
650
+ console.print(f"[red]Config not found:[/red] {path.as_posix()}")
651
+ raise typer.Exit(code=1)
652
+ result = _find_hook_in_layer(path=path, hook_id=hook_id)
653
+ if result is None:
654
+ console.print(f"[red]Hook id not found in layer:[/red] {hook_id!r} at {path.as_posix()}")
655
+ raise typer.Exit(code=1)
656
+ raw, entry = result
657
+ entry["enabled"] = enabled
658
+ path.write_text(json.dumps(raw, indent=2) + "\n", encoding="utf-8")
659
+ verb = "Enabled" if enabled else "Disabled"
660
+ console.print(f"{verb} hook {hook_id!r} in {path.as_posix()}")
661
+
662
+
663
+ @hooks_app.command("enable")
664
+ def hooks_enable(
665
+ hook_id: str = typer.Argument(..., help="Hook id to enable."),
666
+ layer: str = typer.Option("local", "--layer", help="Config layer: user, project, or local."),
667
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
668
+ ) -> None:
669
+ console = _console()
670
+ workspace_root = _resolve_tool_workspace_root(path=path)
671
+ config_path = _hooks_config_path_for_layer(workspace_root=workspace_root, layer=layer)
672
+ _set_hook_enabled(path=config_path, hook_id=hook_id, enabled=True, console=console)
673
+
674
+
675
+ @hooks_app.command("disable")
676
+ def hooks_disable(
677
+ hook_id: str = typer.Argument(..., help="Hook id to disable."),
678
+ layer: str = typer.Option("local", "--layer", help="Config layer: user, project, or local."),
679
+ path: Path = typer.Option(Path("."), "--path", help="Workspace or repository path."),
680
+ ) -> None:
681
+ console = _console()
682
+ workspace_root = _resolve_tool_workspace_root(path=path)
683
+ config_path = _hooks_config_path_for_layer(workspace_root=workspace_root, layer=layer)
684
+ _set_hook_enabled(path=config_path, hook_id=hook_id, enabled=False, console=console)
685
+
686
+
687
+ @hooks_app.command("watch")
688
+ def hooks_watch(
689
+ session_id: str | None = typer.Argument(
690
+ None,
691
+ help="Session id. Defaults to the latest retained session.",
692
+ ),
693
+ limit: int = typer.Option(50, "--limit", min=1, help="Maximum events to show."),
694
+ ) -> None:
695
+ console = _console()
696
+ cfg = _patchable("load_config", load_config)()
697
+ sessions_dir = resolve_sessions_dir(cfg)
698
+ resolved_session_id = session_id
699
+ if not resolved_session_id:
700
+ all_infos = _patchable("list_sessions", list_sessions)(sessions_dir)
701
+ infos = filter_sessions_to_local_owner(all_infos)
702
+ if not infos:
703
+ if all_infos:
704
+ console.print(
705
+ f"No sessions owned by this account in {sessions_dir} "
706
+ f"({len(all_infos)} recorded by a different account; "
707
+ "pass an explicit session id)."
708
+ )
709
+ else:
710
+ console.print(f"No retained sessions found in {sessions_dir}")
711
+ return
712
+ resolved_session_id = infos[0].session_id
713
+ artifact_path = hook_audit_artifact_path(
714
+ sessions_dir=sessions_dir,
715
+ session_id=resolved_session_id,
716
+ )
717
+ events = list(read_hook_audit_events(artifact_path))
718
+ if not events:
719
+ console.print(f"No hook audit events yet at {artifact_path}")
720
+ return
721
+ status_color = {
722
+ "ok": "green",
723
+ "warning": "yellow",
724
+ "blocked": "red",
725
+ }
726
+ for event in events[-limit:]:
727
+ status = str(event.get("status") or "-")
728
+ color = status_color.get(status, STYLE_CONTENT or "default")
729
+ event_name = str(event.get("event_name") or "-")
730
+ hook_id = str(event.get("hook_id") or "-")
731
+ duration_ms = int(event.get("duration_ms") or 0)
732
+ ts = str(event.get("ts") or "-")
733
+ console.print(
734
+ f"[dim]{ts}[/dim] [{color}]{status:<8}[/] {event_name:<20} {hook_id:<30} "
735
+ f"[dim]{duration_ms}ms[/dim]"
736
+ )
737
+ console.print(f"[dim]artifact:[/dim] {artifact_path.as_posix()}")