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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- 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()}")
|