@jaguilar87/gaia-ops 4.4.0 → 4.7.2
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +12 -3
- package/ARCHITECTURE.md +9 -8
- package/CHANGELOG.md +34 -0
- package/README.md +43 -11
- package/agents/terraform-architect.md +1 -1
- package/bin/README.md +2 -2
- package/bin/gaia-doctor.js +18 -5
- package/bin/gaia-history.js +0 -1
- package/bin/gaia-metrics.js +2 -2
- package/bin/gaia-scan.py +23 -1
- package/bin/gaia-update.js +346 -54
- package/bin/pre-publish-validate.js +33 -10
- package/commands/gaia.md +37 -0
- package/config/README.md +3 -9
- package/config/context-contracts.json +47 -15
- package/config/surface-routing.json +9 -1
- package/dist/gaia-ops/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
- package/dist/gaia-ops/agents/devops-developer.md +57 -0
- package/dist/gaia-ops/agents/gaia-system.md +58 -0
- package/dist/gaia-ops/agents/gitops-operator.md +60 -0
- package/dist/gaia-ops/agents/speckit-planner.md +71 -0
- package/dist/gaia-ops/agents/terraform-architect.md +60 -0
- package/dist/gaia-ops/commands/gaia.md +37 -0
- package/dist/gaia-ops/config/README.md +58 -0
- package/dist/gaia-ops/config/cloud/aws.json +140 -0
- package/dist/gaia-ops/config/cloud/gcp.json +145 -0
- package/dist/gaia-ops/config/context-contracts.json +131 -0
- package/dist/gaia-ops/config/git_standards.json +72 -0
- package/dist/gaia-ops/config/surface-routing.json +197 -0
- package/dist/gaia-ops/config/universal-rules.json +10 -0
- package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-ops/hooks/adapters/base.py +219 -0
- package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
- package/dist/gaia-ops/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-ops/hooks/adapters/types.py +194 -0
- package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
- package/dist/gaia-ops/hooks/hooks.json +126 -0
- package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
- package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-ops/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-ops/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
- package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-ops/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-ops/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-ops/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-ops/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-ops/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-ops/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-ops/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-ops/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-ops/hooks/post_compact.py +43 -0
- package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
- package/dist/gaia-ops/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-ops/hooks/session_start.py +69 -0
- package/dist/gaia-ops/hooks/stop_hook.py +69 -0
- package/dist/gaia-ops/hooks/subagent_start.py +71 -0
- package/dist/gaia-ops/hooks/subagent_stop.py +288 -0
- package/dist/gaia-ops/hooks/task_completed.py +70 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-ops/settings.json +72 -0
- package/dist/gaia-ops/skills/README.md +109 -0
- package/dist/gaia-ops/skills/agent-protocol/SKILL.md +105 -0
- package/dist/gaia-ops/skills/agent-protocol/examples.md +170 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +53 -0
- package/dist/gaia-ops/skills/approval/SKILL.md +85 -0
- package/dist/gaia-ops/skills/approval/examples.md +140 -0
- package/dist/gaia-ops/skills/approval/reference.md +57 -0
- package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
- package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
- package/dist/gaia-ops/skills/context-updater/SKILL.md +76 -0
- package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
- package/dist/gaia-ops/skills/developer-patterns/SKILL.md +93 -0
- package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
- package/dist/gaia-ops/skills/execution/SKILL.md +66 -0
- package/dist/gaia-ops/skills/fast-queries/SKILL.md +47 -0
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +92 -0
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +22 -0
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +48 -0
- package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +73 -0
- package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
- package/dist/gaia-ops/skills/investigation/SKILL.md +77 -0
- package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +64 -0
- package/dist/gaia-ops/skills/reference.md +134 -0
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +61 -0
- package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +119 -0
- package/dist/gaia-ops/skills/specification/SKILL.md +186 -0
- package/dist/gaia-ops/skills/speckit-workflow/SKILL.md +165 -0
- package/dist/gaia-ops/skills/speckit-workflow/reference.md +117 -0
- package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +63 -0
- package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
- package/dist/gaia-ops/speckit/README.md +516 -0
- package/dist/gaia-ops/speckit/scripts/.gitkeep +0 -0
- package/dist/gaia-ops/speckit/templates/adr-template.md +118 -0
- package/dist/gaia-ops/speckit/templates/agent-file-template.md +23 -0
- package/dist/gaia-ops/speckit/templates/plan-template.md +227 -0
- package/dist/gaia-ops/speckit/templates/spec-template.md +140 -0
- package/dist/gaia-ops/speckit/templates/tasks-template.md +257 -0
- package/dist/gaia-ops/tools/context/README.md +132 -0
- package/dist/gaia-ops/tools/context/__init__.py +42 -0
- package/dist/gaia-ops/tools/context/_paths.py +20 -0
- package/dist/gaia-ops/tools/context/context_provider.py +476 -0
- package/dist/gaia-ops/tools/context/context_section_reader.py +330 -0
- package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
- package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
- package/dist/gaia-ops/tools/context/surface_router.py +278 -0
- package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
- package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
- package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
- package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
- package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
- package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
- package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
- package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
- package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
- package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +262 -0
- package/dist/gaia-ops/tools/memory/README.md +0 -0
- package/dist/gaia-ops/tools/memory/__init__.py +20 -0
- package/dist/gaia-ops/tools/memory/episodic.py +1196 -0
- package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
- package/dist/gaia-ops/tools/review/__init__.py +1 -0
- package/dist/gaia-ops/tools/review/review_engine.py +157 -0
- package/dist/gaia-ops/tools/scan/__init__.py +35 -0
- package/dist/gaia-ops/tools/scan/config.py +247 -0
- package/dist/gaia-ops/tools/scan/merge.py +212 -0
- package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
- package/dist/gaia-ops/tools/scan/registry.py +127 -0
- package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
- package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
- package/dist/gaia-ops/tools/scan/scanners/environment.py +324 -0
- package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
- package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
- package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
- package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
- package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
- package/dist/gaia-ops/tools/scan/setup.py +753 -0
- package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
- package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
- package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
- package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
- package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
- package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
- package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
- package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
- package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
- package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
- package/dist/gaia-ops/tools/scan/ui.py +624 -0
- package/dist/gaia-ops/tools/scan/verify.py +266 -0
- package/dist/gaia-ops/tools/scan/walk.py +118 -0
- package/dist/gaia-ops/tools/scan/workspace.py +85 -0
- package/dist/gaia-ops/tools/validation/README.md +244 -0
- package/dist/gaia-ops/tools/validation/__init__.py +17 -0
- package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
- package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
- package/dist/gaia-security/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-security/config/universal-rules.json +10 -0
- package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-security/hooks/adapters/base.py +219 -0
- package/dist/gaia-security/hooks/adapters/channel.py +17 -0
- package/dist/gaia-security/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-security/hooks/adapters/types.py +194 -0
- package/dist/gaia-security/hooks/adapters/utils.py +25 -0
- package/dist/gaia-security/hooks/hooks.json +57 -0
- package/dist/gaia-security/hooks/modules/__init__.py +15 -0
- package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-security/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-security/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-security/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-security/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-security/hooks/modules/core/state.py +179 -0
- package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-security/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-security/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-security/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-security/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-security/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-security/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-security/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-security/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-security/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-security/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-security/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-security/hooks/post_tool_use.py +54 -0
- package/dist/gaia-security/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-security/hooks/session_start.py +69 -0
- package/dist/gaia-security/hooks/stop_hook.py +69 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-security/settings.json +58 -0
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +8 -6
- package/hooks/adapters/channel.py +0 -25
- package/hooks/adapters/claude_code.py +364 -125
- package/hooks/elicitation_result.py +132 -0
- package/hooks/hooks.json +10 -1
- package/hooks/modules/README.md +3 -2
- package/hooks/modules/agents/contract_validator.py +3 -51
- package/hooks/modules/agents/response_contract.py +4 -8
- package/hooks/modules/agents/transcript_reader.py +4 -5
- package/hooks/modules/audit/__init__.py +4 -6
- package/hooks/modules/audit/event_detector.py +0 -2
- package/hooks/modules/audit/metrics.py +108 -187
- package/hooks/modules/audit/workflow_auditor.py +0 -4
- package/hooks/modules/audit/workflow_recorder.py +0 -5
- package/hooks/modules/context/compact_context_builder.py +1 -0
- package/hooks/modules/context/context_cache.py +129 -0
- package/hooks/modules/context/context_injector.py +18 -40
- package/hooks/modules/context/context_writer.py +1 -25
- package/hooks/modules/context/contracts_loader.py +7 -10
- package/hooks/modules/core/hook_entry.py +1 -0
- package/hooks/modules/core/paths.py +12 -13
- package/hooks/modules/core/plugin_mode.py +74 -4
- package/hooks/modules/core/plugin_setup.py +395 -23
- package/hooks/modules/events/__init__.py +1 -0
- package/hooks/modules/events/event_writer.py +210 -0
- package/hooks/modules/identity/ops_identity.py +18 -27
- package/hooks/modules/memory/episode_writer.py +1 -6
- package/hooks/modules/orchestrator/__init__.py +1 -0
- package/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/hooks/modules/security/__init__.py +2 -4
- package/hooks/modules/security/approval_constants.py +5 -1
- package/hooks/modules/security/approval_grants.py +189 -6
- package/hooks/modules/security/approval_messages.py +9 -21
- package/hooks/modules/security/blocked_commands.py +98 -34
- package/hooks/modules/security/command_semantics.py +0 -4
- package/hooks/modules/security/gitops_validator.py +1 -11
- package/hooks/modules/security/mutative_verbs.py +179 -38
- package/hooks/modules/security/tiers.py +1 -19
- package/hooks/modules/session/session_event_injector.py +1 -25
- package/hooks/modules/tools/bash_validator.py +310 -94
- package/hooks/modules/tools/shell_parser.py +0 -1
- package/hooks/modules/tools/task_validator.py +9 -29
- package/hooks/post_tool_use.py +0 -72
- package/hooks/pre_tool_use.py +42 -102
- package/hooks/session_start.py +4 -2
- package/hooks/subagent_start.py +6 -2
- package/hooks/subagent_stop.py +1 -13
- package/hooks/user_prompt_submit.py +119 -37
- package/index.js +1 -1
- package/package.json +5 -3
- package/skills/README.md +3 -5
- package/skills/agent-protocol/SKILL.md +17 -16
- package/skills/agent-protocol/examples.md +6 -6
- package/skills/agent-response/SKILL.md +11 -14
- package/skills/approval/SKILL.md +28 -13
- package/skills/approval/reference.md +2 -2
- package/skills/execution/SKILL.md +1 -1
- package/skills/gaia-patterns/SKILL.md +2 -3
- package/skills/orchestrator-approval/SKILL.md +22 -50
- package/skills/security-tiers/SKILL.md +1 -1
- package/templates/README.md +9 -9
- package/templates/managed-settings.template.json +43 -0
- package/tools/gaia_simulator/runner.py +34 -1
- package/tools/scan/orchestrator.py +13 -0
- package/tools/scan/scanners/base.py +8 -0
- package/tools/scan/scanners/git.py +78 -0
- package/tools/scan/scanners/infrastructure.py +65 -0
- package/tools/scan/scanners/stack.py +110 -0
- package/tools/scan/setup.py +120 -13
- package/tools/scan/workspace.py +85 -0
- package/config/context-contracts.aws.json +0 -42
- package/config/context-contracts.gcp.json +0 -39
- package/skills/project-dispatch/SKILL.md +0 -34
- package/templates/settings.template.json +0 -226
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Post-tool use hook - Thin gate.
|
|
4
|
+
|
|
5
|
+
Architecture:
|
|
6
|
+
- Uses adapter layer to parse and process the full PostToolUse lifecycle
|
|
7
|
+
- All business logic lives in ClaudeCodeAdapter.adapt_post_tool_use()
|
|
8
|
+
- This file is stdin/stdout glue only
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
18
|
+
|
|
19
|
+
from modules.core.paths import get_logs_dir
|
|
20
|
+
from adapters.claude_code import ClaudeCodeAdapter
|
|
21
|
+
from modules.core.hook_entry import run_hook
|
|
22
|
+
|
|
23
|
+
# Configure logging
|
|
24
|
+
log_file = get_logs_dir() / f"hooks-{datetime.now().strftime('%Y-%m-%d')}.log"
|
|
25
|
+
logging.basicConfig(
|
|
26
|
+
level=logging.INFO,
|
|
27
|
+
format='%(asctime)s [post_tool_use] %(name)s - %(levelname)s - %(message)s',
|
|
28
|
+
handlers=[logging.FileHandler(log_file)],
|
|
29
|
+
)
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _handle_post_tool_use(event) -> None:
|
|
34
|
+
"""Process a PostToolUse event.
|
|
35
|
+
|
|
36
|
+
Delegates all business logic to the adapter.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
event: Parsed HookEvent from the adapter layer.
|
|
40
|
+
"""
|
|
41
|
+
adapter = ClaudeCodeAdapter()
|
|
42
|
+
response = adapter.adapt_post_tool_use(event)
|
|
43
|
+
|
|
44
|
+
if response.output:
|
|
45
|
+
print(json.dumps(response.output))
|
|
46
|
+
sys.exit(response.exit_code)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ============================================================================
|
|
50
|
+
# STDIN HANDLER (Claude Code integration)
|
|
51
|
+
# ============================================================================
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
run_hook(_handle_post_tool_use, hook_name="post_tool_use")
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Pre-tool use hook - Thin Gate Architecture.
|
|
4
|
+
|
|
5
|
+
Entry point for Bash and Task/Agent tool validation. The hook is the primary
|
|
6
|
+
security gate: with Bash(*) in the settings.json allow list, all commands
|
|
7
|
+
reach this hook regardless of settings.json permissions.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
- Uses adapter layer to parse and process the full PreToolUse lifecycle
|
|
11
|
+
- All business logic lives in ClaudeCodeAdapter.adapt_pre_tool_use()
|
|
12
|
+
- This file is stdin/stdout glue only
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
23
|
+
from modules.core.paths import get_logs_dir
|
|
24
|
+
|
|
25
|
+
# Adapter layer
|
|
26
|
+
from adapters.claude_code import ClaudeCodeAdapter
|
|
27
|
+
from modules.core.stdin import has_stdin_data
|
|
28
|
+
from adapters.utils import warn_if_dual_channel
|
|
29
|
+
|
|
30
|
+
# Configure logging -- all hooks share hooks-YYYY-MM-DD.log for easy tailing
|
|
31
|
+
log_file = get_logs_dir() / f"hooks-{datetime.now().strftime('%Y-%m-%d')}.log"
|
|
32
|
+
logging.basicConfig(
|
|
33
|
+
level=logging.INFO,
|
|
34
|
+
format='%(asctime)s [pre_tool_use] %(name)s - %(levelname)s - %(message)s',
|
|
35
|
+
handlers=[
|
|
36
|
+
logging.FileHandler(log_file),
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ============================================================================
|
|
43
|
+
# BACKWARD-COMPATIBLE API
|
|
44
|
+
# ============================================================================
|
|
45
|
+
# Tests and e2e scripts import these names directly. They delegate to the
|
|
46
|
+
# adapter internally but preserve the original call signatures.
|
|
47
|
+
|
|
48
|
+
from modules.tools.bash_validator import BashValidator
|
|
49
|
+
from modules.tools.task_validator import TaskValidator, AVAILABLE_AGENTS, META_AGENTS
|
|
50
|
+
from modules.security.prompt_validator import classify_resume_prompt
|
|
51
|
+
from modules.context.context_injector import build_project_context
|
|
52
|
+
from modules.session.session_event_injector import build_session_events
|
|
53
|
+
from modules.core.state import create_pre_hook_state, save_hook_state
|
|
54
|
+
from modules.security.approval_grants import (
|
|
55
|
+
cleanup_expired_grants,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Derived constants used by backward-compat wrappers
|
|
59
|
+
PROJECT_AGENTS = [a for a in AVAILABLE_AGENTS if a not in META_AGENTS]
|
|
60
|
+
_HOOKS_DIR = Path(__file__).parent
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _classify_resume_prompt(prompt: str) -> str:
|
|
64
|
+
"""Classify a resume prompt. Delegates to modules.security.prompt_validator."""
|
|
65
|
+
return classify_resume_prompt(prompt)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pre_tool_use_hook(tool_name: str, parameters: dict) -> str | dict | None:
|
|
69
|
+
"""
|
|
70
|
+
Pre-tool use hook implementation (backward-compatible API).
|
|
71
|
+
|
|
72
|
+
Delegates to adapter but preserves the original return signature:
|
|
73
|
+
None: allowed (no modification)
|
|
74
|
+
str: blocked (error message)
|
|
75
|
+
dict: allowed with modification (JSON with updatedInput)
|
|
76
|
+
"""
|
|
77
|
+
adapter = ClaudeCodeAdapter()
|
|
78
|
+
|
|
79
|
+
# Build a minimal HookEvent-like payload for the adapter's internal methods
|
|
80
|
+
logger.info(f"Hook invoked: tool={tool_name}, params={json.dumps(parameters)[:200]}")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
cleanup_expired_grants()
|
|
84
|
+
|
|
85
|
+
if not isinstance(tool_name, str):
|
|
86
|
+
return "Error: Invalid tool name"
|
|
87
|
+
if not isinstance(parameters, dict):
|
|
88
|
+
return "Error: Invalid parameters"
|
|
89
|
+
|
|
90
|
+
if tool_name.lower() == "bash":
|
|
91
|
+
return _handle_bash(tool_name, parameters)
|
|
92
|
+
elif tool_name.lower() in ("task", "agent"):
|
|
93
|
+
return _handle_task(tool_name, parameters)
|
|
94
|
+
elif tool_name.lower() == "sendmessage":
|
|
95
|
+
return _handle_send_message(tool_name, parameters)
|
|
96
|
+
else:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Unexpected error in pre_tool_use_hook: {e}", exc_info=True)
|
|
101
|
+
return f"Error during security validation: {str(e)}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _handle_bash(tool_name: str, parameters: dict) -> str | dict | None:
|
|
105
|
+
"""
|
|
106
|
+
Handle Bash tool validation.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
None: allowed (no modification)
|
|
110
|
+
str: blocked (error message)
|
|
111
|
+
dict: allowed with modification (hookSpecificOutput JSON)
|
|
112
|
+
"""
|
|
113
|
+
command = parameters.get("command", "")
|
|
114
|
+
if not command:
|
|
115
|
+
return "Error: Bash tool requires a command"
|
|
116
|
+
|
|
117
|
+
validator = BashValidator()
|
|
118
|
+
result = validator.validate(command)
|
|
119
|
+
|
|
120
|
+
if not result.allowed:
|
|
121
|
+
logger.warning(f"BLOCKED: {command[:100]} - {result.reason}")
|
|
122
|
+
|
|
123
|
+
# Structured response from bash_validator (ask or deny)
|
|
124
|
+
if result.block_response is not None:
|
|
125
|
+
return result.block_response
|
|
126
|
+
|
|
127
|
+
# Permanently blocked (no structured response) — hard block (exit 2)
|
|
128
|
+
return _format_blocked_message(result)
|
|
129
|
+
|
|
130
|
+
effective_command = result.modified_input.get("command", command) if result.modified_input else command
|
|
131
|
+
state = create_pre_hook_state(
|
|
132
|
+
tool_name=tool_name,
|
|
133
|
+
command=effective_command,
|
|
134
|
+
tier=str(result.tier),
|
|
135
|
+
allowed=True,
|
|
136
|
+
)
|
|
137
|
+
save_hook_state(state)
|
|
138
|
+
|
|
139
|
+
if result.modified_input:
|
|
140
|
+
logger.info(f"MODIFIED: {command[:80]} -> stripped footer - tier={result.tier}")
|
|
141
|
+
return {
|
|
142
|
+
"hookSpecificOutput": {
|
|
143
|
+
"hookEventName": "PreToolUse",
|
|
144
|
+
"permissionDecision": "allow",
|
|
145
|
+
"permissionDecisionReason": result.reason,
|
|
146
|
+
"updatedInput": result.modified_input
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
logger.info(f"ALLOWED: {command[:100]} - tier={result.tier}")
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _handle_task(tool_name: str, parameters: dict) -> str | dict | None:
|
|
155
|
+
"""
|
|
156
|
+
Handle Task/Agent tool validation for new task dispatches.
|
|
157
|
+
|
|
158
|
+
Context is built here and cached for SubagentStart to forward to the
|
|
159
|
+
subagent. PreToolUse no longer returns additionalContext (that would
|
|
160
|
+
inject it into the orchestrator, not the subagent).
|
|
161
|
+
"""
|
|
162
|
+
context_text, _telemetry = build_project_context(parameters, PROJECT_AGENTS, _HOOKS_DIR)
|
|
163
|
+
events_text = build_session_events(parameters, PROJECT_AGENTS)
|
|
164
|
+
|
|
165
|
+
# Standard task validation (runs against ORIGINAL prompt -- no workaround needed)
|
|
166
|
+
validator = TaskValidator()
|
|
167
|
+
result = validator.validate(parameters)
|
|
168
|
+
|
|
169
|
+
if not result.allowed:
|
|
170
|
+
logger.warning(f"BLOCKED Task: {result.agent_name} - {result.reason}")
|
|
171
|
+
return result.reason
|
|
172
|
+
|
|
173
|
+
state = create_pre_hook_state(
|
|
174
|
+
tool_name=tool_name,
|
|
175
|
+
command=f"Task:{result.agent_name}",
|
|
176
|
+
tier=str(result.tier),
|
|
177
|
+
allowed=True,
|
|
178
|
+
is_t3=result.is_t3_operation,
|
|
179
|
+
)
|
|
180
|
+
save_hook_state(state)
|
|
181
|
+
|
|
182
|
+
logger.info(f"ALLOWED Task: {result.agent_name}")
|
|
183
|
+
|
|
184
|
+
# Cache context for SubagentStart to pick up and forward to the subagent.
|
|
185
|
+
additional = "\n".join(filter(None, [context_text, events_text]))
|
|
186
|
+
|
|
187
|
+
# Fallback: if build_project_context returned None because the
|
|
188
|
+
# orchestrator already embedded context in the prompt (dedup guard),
|
|
189
|
+
# extract the embedded context so SubagentStart can still inject it.
|
|
190
|
+
if not additional:
|
|
191
|
+
prompt = parameters.get("prompt", "")
|
|
192
|
+
marker = "# Project Context"
|
|
193
|
+
if marker in prompt:
|
|
194
|
+
idx = prompt.index(marker)
|
|
195
|
+
additional = prompt[idx:]
|
|
196
|
+
logger.info(
|
|
197
|
+
"Extracted embedded context from prompt for caching "
|
|
198
|
+
"(len=%d, agent=%s)",
|
|
199
|
+
len(additional), result.agent_name,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if additional:
|
|
203
|
+
from adapters.claude_code import ClaudeCodeAdapter
|
|
204
|
+
adapter = ClaudeCodeAdapter()
|
|
205
|
+
session_id = parameters.get("session_id", "") or "unknown"
|
|
206
|
+
agent_type = result.agent_name or "unknown"
|
|
207
|
+
adapter._cache_context_for_subagent(session_id, agent_type, additional)
|
|
208
|
+
logger.info(f"Cached context for SubagentStart: agent={agent_type}")
|
|
209
|
+
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _handle_send_message(tool_name: str, parameters: dict) -> str | None:
|
|
214
|
+
"""
|
|
215
|
+
Handle SendMessage tool validation for agent resumption.
|
|
216
|
+
|
|
217
|
+
Validates agent ID format and message content. Does NOT inject
|
|
218
|
+
project context (it's a resume). Nonce relay is no longer processed
|
|
219
|
+
here -- approval grants are activated by the UserPromptSubmit hook.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
None: allowed (no modification)
|
|
223
|
+
str: blocked (error message)
|
|
224
|
+
"""
|
|
225
|
+
import re
|
|
226
|
+
|
|
227
|
+
agent_id = parameters.get("to", "")
|
|
228
|
+
message = parameters.get("message", "")
|
|
229
|
+
|
|
230
|
+
if not agent_id or not re.match(r'^a[0-9a-f]{5,}$', agent_id):
|
|
231
|
+
logger.warning(f"BLOCKED SendMessage: Invalid agentId format '{agent_id}'")
|
|
232
|
+
return (
|
|
233
|
+
f"[ERROR] Invalid agent ID format: '{agent_id}'\n\n"
|
|
234
|
+
"Agent ID should be 'a' followed by hex characters.\n"
|
|
235
|
+
"Example: a12345f or a51a0cbbf6afb831d\n\n"
|
|
236
|
+
"The agent ID is returned at the end of agent responses.\n"
|
|
237
|
+
"Look for: 'agentId: a...' in the previous agent output."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if not message or not message.strip():
|
|
241
|
+
logger.warning(f"BLOCKED SendMessage: Missing message for agent {agent_id}")
|
|
242
|
+
return (
|
|
243
|
+
"[ERROR] SendMessage requires a message\n\n"
|
|
244
|
+
"When resuming an agent, you must provide a message:\n\n"
|
|
245
|
+
"SendMessage(\n"
|
|
246
|
+
" to=\"a12345\",\n"
|
|
247
|
+
" message=\"Continue with the latest user instruction.\"\n"
|
|
248
|
+
")\n\n"
|
|
249
|
+
"The message tells the agent what to do next."
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
logger.info(f"SENDMESSAGE: Resuming agent {agent_id}")
|
|
253
|
+
|
|
254
|
+
state = create_pre_hook_state(
|
|
255
|
+
tool_name=tool_name,
|
|
256
|
+
command=f"SendMessage:{agent_id}",
|
|
257
|
+
tier="T0",
|
|
258
|
+
allowed=True,
|
|
259
|
+
is_t3=False,
|
|
260
|
+
has_approval=False,
|
|
261
|
+
)
|
|
262
|
+
save_hook_state(state)
|
|
263
|
+
|
|
264
|
+
logger.info(f"ALLOWED SendMessage: agent {agent_id} - message length: {len(message)}")
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _format_blocked_message(result) -> str:
|
|
269
|
+
"""Format blocked command message. Delegates to blocked_message_formatter."""
|
|
270
|
+
from modules.security.blocked_message_formatter import format_blocked_message
|
|
271
|
+
return format_blocked_message(result)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ============================================================================
|
|
275
|
+
# CLI INTERFACE
|
|
276
|
+
# ============================================================================
|
|
277
|
+
|
|
278
|
+
def main():
|
|
279
|
+
"""CLI interface for testing."""
|
|
280
|
+
if len(sys.argv) < 2:
|
|
281
|
+
print("Usage: python pre_tool_use.py <command>")
|
|
282
|
+
print(" python pre_tool_use.py --test")
|
|
283
|
+
sys.exit(1)
|
|
284
|
+
|
|
285
|
+
if sys.argv[1] == "--test":
|
|
286
|
+
_run_tests()
|
|
287
|
+
else:
|
|
288
|
+
command = " ".join(sys.argv[1:])
|
|
289
|
+
result = pre_tool_use_hook("bash", {"command": command})
|
|
290
|
+
if result:
|
|
291
|
+
print(f"BLOCKED: {result}")
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
else:
|
|
294
|
+
print(f"ALLOWED: {command}")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _run_tests():
|
|
298
|
+
"""Run validation tests."""
|
|
299
|
+
print("Testing Pre-Tool Use Hook...\n")
|
|
300
|
+
|
|
301
|
+
test_cases = [
|
|
302
|
+
("terraform validate", True, "T1"),
|
|
303
|
+
("terraform apply", False, "T3"),
|
|
304
|
+
("kubectl get pods", True, "T0"),
|
|
305
|
+
("kubectl apply -f manifest.yaml", False, "T3"),
|
|
306
|
+
("kubectl apply -f manifest.yaml --dry-run=client", True, "T2"),
|
|
307
|
+
("ls -la", True, "T0"),
|
|
308
|
+
("rm -rf /", False, "T3"),
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
for command, expected_allowed, expected_tier in test_cases:
|
|
312
|
+
result = pre_tool_use_hook("bash", {"command": command})
|
|
313
|
+
actual_allowed = result is None
|
|
314
|
+
status = "PASS" if actual_allowed == expected_allowed else "FAIL"
|
|
315
|
+
print(f"{status}: {command}")
|
|
316
|
+
if status == "FAIL":
|
|
317
|
+
print(f" Expected: allowed={expected_allowed}, Got: allowed={actual_allowed}")
|
|
318
|
+
|
|
319
|
+
print("\nTest completed")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ============================================================================
|
|
323
|
+
# STDIN HANDLER (Claude Code integration)
|
|
324
|
+
# ============================================================================
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
# Check if running from CLI with arguments
|
|
328
|
+
if len(sys.argv) > 1:
|
|
329
|
+
main()
|
|
330
|
+
elif has_stdin_data():
|
|
331
|
+
try:
|
|
332
|
+
adapter = ClaudeCodeAdapter()
|
|
333
|
+
warn_if_dual_channel()
|
|
334
|
+
|
|
335
|
+
stdin_data = sys.stdin.read()
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
event = adapter.parse_event(stdin_data)
|
|
339
|
+
except ValueError as e:
|
|
340
|
+
error_msg = str(e)
|
|
341
|
+
logger.error(f"Adapter parse failed: {error_msg}")
|
|
342
|
+
print(f"HOOK ERROR: {error_msg}", file=sys.stderr)
|
|
343
|
+
if "Empty stdin" in error_msg:
|
|
344
|
+
print(f"Error: {error_msg}")
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
|
|
347
|
+
response = adapter.adapt_pre_tool_use(event)
|
|
348
|
+
|
|
349
|
+
if isinstance(response.output, dict) and response.output:
|
|
350
|
+
hook_output = response.output.get("hookSpecificOutput", {})
|
|
351
|
+
decision = hook_output.get("permissionDecision")
|
|
352
|
+
if decision in ("block", "deny"):
|
|
353
|
+
reason = hook_output.get("permissionDecisionReason", "Command blocked by hook policy")
|
|
354
|
+
summary = reason.split('\n')[0]
|
|
355
|
+
print(f"BLOCKED: {summary}", file=sys.stderr)
|
|
356
|
+
elif decision == "ask":
|
|
357
|
+
reason = hook_output.get("permissionDecisionReason", "")
|
|
358
|
+
summary = reason.split('\n')[0]
|
|
359
|
+
print(f"T3: {summary}", file=sys.stderr)
|
|
360
|
+
print(json.dumps(response.output))
|
|
361
|
+
sys.exit(response.exit_code)
|
|
362
|
+
elif isinstance(response.output, str) and response.output:
|
|
363
|
+
summary = response.output.split('\n')[0]
|
|
364
|
+
print(f"BLOCKED: {summary}", file=sys.stderr)
|
|
365
|
+
print(response.output)
|
|
366
|
+
sys.exit(response.exit_code)
|
|
367
|
+
else:
|
|
368
|
+
sys.exit(0)
|
|
369
|
+
|
|
370
|
+
except json.JSONDecodeError as e:
|
|
371
|
+
logger.error(f"Invalid JSON from stdin: {e}")
|
|
372
|
+
print(f"HOOK ERROR: Invalid JSON from stdin: {e}", file=sys.stderr)
|
|
373
|
+
sys.exit(1)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error(f"Error processing hook: {e}", exc_info=True)
|
|
376
|
+
print(f"HOOK ERROR: {str(e)}", file=sys.stderr)
|
|
377
|
+
print(f"Hook error: {str(e)}")
|
|
378
|
+
sys.exit(1)
|
|
379
|
+
else:
|
|
380
|
+
print("Usage: python pre_tool_use.py <command>")
|
|
381
|
+
print(" python pre_tool_use.py --test")
|
|
382
|
+
print(" echo '{\"tool_name\":\"bash\",\"tool_input\":{\"command\":\"ls\"}}' | python pre_tool_use.py")
|
|
383
|
+
sys.exit(1)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SessionStart hook — first-time setup + project scan (ops only)."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
11
|
+
|
|
12
|
+
from modules.core.stdin import has_stdin_data
|
|
13
|
+
from modules.core.paths import get_logs_dir
|
|
14
|
+
from modules.core.plugin_mode import is_ops_mode
|
|
15
|
+
from modules.core.plugin_setup import run_first_time_setup
|
|
16
|
+
|
|
17
|
+
# Configure logging — file only
|
|
18
|
+
_log_file = get_logs_dir() / f"hooks-{datetime.now().strftime('%Y-%m-%d')}.log"
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=logging.INFO,
|
|
21
|
+
format='%(asctime)s [session_start] %(name)s - %(levelname)s - %(message)s',
|
|
22
|
+
handlers=[logging.FileHandler(_log_file)],
|
|
23
|
+
)
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
if not has_stdin_data():
|
|
29
|
+
sys.exit(0)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
sys.stdin.read()
|
|
33
|
+
|
|
34
|
+
# First-time setup: create project permissions if needed.
|
|
35
|
+
# mark_done=False so UserPromptSubmit can detect first-run
|
|
36
|
+
# and show the welcome message before marking initialized.
|
|
37
|
+
setup_message = run_first_time_setup(mark_done=False)
|
|
38
|
+
if setup_message:
|
|
39
|
+
logger.info("First-time setup: %s", setup_message)
|
|
40
|
+
|
|
41
|
+
# Project scan: only in ops mode
|
|
42
|
+
project_scanned = False
|
|
43
|
+
if is_ops_mode():
|
|
44
|
+
from modules.context.context_freshness import check_freshness
|
|
45
|
+
from modules.scanning.scan_trigger import trigger_lightweight_scan
|
|
46
|
+
|
|
47
|
+
freshness = check_freshness()
|
|
48
|
+
if freshness.is_fresh:
|
|
49
|
+
logger.info("SessionStart: skipped scan (fresh)")
|
|
50
|
+
else:
|
|
51
|
+
logger.info("SessionStart: %s — running lightweight scan", freshness.reason)
|
|
52
|
+
scan_ok = trigger_lightweight_scan(Path.cwd())
|
|
53
|
+
if scan_ok:
|
|
54
|
+
project_scanned = True
|
|
55
|
+
logger.info("Auto-refresh completed successfully")
|
|
56
|
+
else:
|
|
57
|
+
logger.warning("Auto-refresh failed")
|
|
58
|
+
|
|
59
|
+
response = {"session_type": "startup", "project_scanned": project_scanned}
|
|
60
|
+
if setup_message:
|
|
61
|
+
response["setup_message"] = setup_message
|
|
62
|
+
|
|
63
|
+
print(json.dumps(response))
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error("SessionStart error (non-fatal): %s", e)
|
|
68
|
+
print(json.dumps({}))
|
|
69
|
+
sys.exit(0)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Stop hook for Claude Code Agent System.
|
|
4
|
+
|
|
5
|
+
Fires when Claude finishes responding. Evaluates whether the response has
|
|
6
|
+
adequate evidence quality. For MVP: logs the event and allows stop (exit 0).
|
|
7
|
+
Quality check logic will be wired in a future iteration.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
- Uses adapter layer to parse Stop event
|
|
11
|
+
- Calls adapter.adapt_stop() for quality assessment
|
|
12
|
+
- Returns quality result via adapter format_quality_response()
|
|
13
|
+
- Exit code 0 = allow stop, exit code 2 = continue instead of stop
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
23
|
+
|
|
24
|
+
from adapters.claude_code import ClaudeCodeAdapter
|
|
25
|
+
from modules.core.hook_entry import run_hook
|
|
26
|
+
from modules.core.paths import get_logs_dir
|
|
27
|
+
|
|
28
|
+
# Configure logging
|
|
29
|
+
_log_file = get_logs_dir() / f"hooks-{datetime.now().strftime('%Y-%m-%d')}.log"
|
|
30
|
+
logging.basicConfig(
|
|
31
|
+
level=logging.INFO,
|
|
32
|
+
format='%(asctime)s [stop_hook] %(name)s - %(levelname)s - %(message)s',
|
|
33
|
+
handlers=[logging.FileHandler(_log_file)],
|
|
34
|
+
)
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _handle_stop(event) -> None:
|
|
39
|
+
"""Process a Stop event.
|
|
40
|
+
|
|
41
|
+
Evaluates response quality and decides whether to allow the stop.
|
|
42
|
+
For MVP, always allows stop (exit 0).
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
event: Parsed HookEvent from the adapter layer.
|
|
46
|
+
"""
|
|
47
|
+
adapter = ClaudeCodeAdapter()
|
|
48
|
+
|
|
49
|
+
quality_result = adapter.adapt_stop(event.payload)
|
|
50
|
+
stop_reason = event.payload.get("stop_reason", "unknown")
|
|
51
|
+
|
|
52
|
+
logger.info(
|
|
53
|
+
"Stop: reason=%s, quality_sufficient=%s, score=%.2f",
|
|
54
|
+
stop_reason,
|
|
55
|
+
quality_result.quality_sufficient,
|
|
56
|
+
quality_result.score,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
response = adapter.format_quality_response(quality_result)
|
|
60
|
+
print(json.dumps(response.output))
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ============================================================================
|
|
65
|
+
# STDIN HANDLER (Claude Code integration)
|
|
66
|
+
# ============================================================================
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
run_hook(_handle_stop, hook_name="stop_hook")
|