@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,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task tool validator.
|
|
3
|
+
|
|
4
|
+
Validates Task tool invocations:
|
|
5
|
+
- Agent existence verification
|
|
6
|
+
- Context provisioning enforcement
|
|
7
|
+
- T3 operation detection for user approval workflow
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from ..security.tiers import SecurityTier
|
|
16
|
+
from ..security.mutative_verbs import (
|
|
17
|
+
detect_mutative_command,
|
|
18
|
+
MutativeResult,
|
|
19
|
+
CLI_FAMILY_LOOKUP,
|
|
20
|
+
COMMAND_ALIASES,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Available agents for Task invocation — both bare and plugin-namespaced forms
|
|
26
|
+
_BASE_AGENTS = [
|
|
27
|
+
"terraform-architect",
|
|
28
|
+
"gitops-operator",
|
|
29
|
+
"cloud-troubleshooter",
|
|
30
|
+
"devops-developer",
|
|
31
|
+
"gaia-system",
|
|
32
|
+
"Explore",
|
|
33
|
+
"Plan",
|
|
34
|
+
"speckit-planner",
|
|
35
|
+
"claude-code-guide",
|
|
36
|
+
]
|
|
37
|
+
# Support both "cloud-troubleshooter" and "gaia-ops:cloud-troubleshooter"
|
|
38
|
+
AVAILABLE_AGENTS = _BASE_AGENTS + [f"gaia-ops:{a}" for a in _BASE_AGENTS]
|
|
39
|
+
|
|
40
|
+
# Meta-agents that don't require context_provider.
|
|
41
|
+
# speckit-planner is a project agent that DOES receive context, so it is NOT a meta-agent.
|
|
42
|
+
META_AGENTS = ["gaia-system", "Explore", "Plan", "claude-code-guide"]
|
|
43
|
+
|
|
44
|
+
# T3_KEYWORDS is test-only: used by tests and cross-layer consistency checks
|
|
45
|
+
# to verify that these commands are classified as T3 by the verb detector.
|
|
46
|
+
# NOT used at runtime -- detection is handled entirely by detect_mutative_command().
|
|
47
|
+
T3_KEYWORDS = [
|
|
48
|
+
"git commit",
|
|
49
|
+
"git push",
|
|
50
|
+
"terraform apply",
|
|
51
|
+
"terragrunt apply",
|
|
52
|
+
"terragrunt run-all apply",
|
|
53
|
+
"kubectl apply",
|
|
54
|
+
"kubectl delete",
|
|
55
|
+
"kubectl create",
|
|
56
|
+
"kubectl rollout restart",
|
|
57
|
+
"kubectl scale",
|
|
58
|
+
"kubectl set image",
|
|
59
|
+
"git push origin main",
|
|
60
|
+
"git push origin master",
|
|
61
|
+
"helm install",
|
|
62
|
+
"helm upgrade",
|
|
63
|
+
"flux reconcile",
|
|
64
|
+
"npm publish",
|
|
65
|
+
"docker push",
|
|
66
|
+
"gcloud sql import",
|
|
67
|
+
"gcloud storage cp",
|
|
68
|
+
"gcloud storage rsync",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_EMBEDDED_COMMAND_QUOTE_CHARS = "\"'`"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _sanitize_candidate_fragment(fragment: str) -> str:
|
|
76
|
+
"""Normalize a prose-embedded command fragment for verb detection.
|
|
77
|
+
|
|
78
|
+
Task prompts often mention commands inside backticks or quotes:
|
|
79
|
+
- Please run `terraform apply` in prod
|
|
80
|
+
- Need to execute "terraform apply" in prod
|
|
81
|
+
|
|
82
|
+
The detector only needs the command skeleton, so strip quote delimiters and
|
|
83
|
+
collapse whitespace before handing the fragment to the dangerous verb
|
|
84
|
+
classifier.
|
|
85
|
+
"""
|
|
86
|
+
if not fragment:
|
|
87
|
+
return ""
|
|
88
|
+
cleaned = fragment.translate(str.maketrans({char: " " for char in _EMBEDDED_COMMAND_QUOTE_CHARS}))
|
|
89
|
+
cleaned = re.sub(r"\s+", " ", cleaned).strip()
|
|
90
|
+
return cleaned.rstrip(".,;:!?")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _extract_command_candidates(text: str) -> List[str]:
|
|
94
|
+
"""Extract command-like lines from free-form text for verb detection.
|
|
95
|
+
|
|
96
|
+
Looks for lines that start with known CLI prefixes or contain command-like
|
|
97
|
+
patterns (e.g., "git push", "terraform apply").
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
text: Free-form text (prompt or description).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of candidate command strings to scan.
|
|
104
|
+
"""
|
|
105
|
+
if not text:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
candidates: List[str] = []
|
|
109
|
+
# Derive CLI prefixes from the canonical CLI_FAMILY_LOOKUP and COMMAND_ALIASES
|
|
110
|
+
cli_prefixes = tuple(
|
|
111
|
+
f"{cli} " for cli in sorted(
|
|
112
|
+
set(CLI_FAMILY_LOOKUP.keys()) | set(COMMAND_ALIASES.keys()),
|
|
113
|
+
key=len,
|
|
114
|
+
reverse=True,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
text_lower = text.lower()
|
|
119
|
+
|
|
120
|
+
# Strategy 1: Scan the full text for known CLI command patterns
|
|
121
|
+
for prefix in cli_prefixes:
|
|
122
|
+
idx = 0
|
|
123
|
+
while True:
|
|
124
|
+
pos = text_lower.find(prefix, idx)
|
|
125
|
+
if pos == -1:
|
|
126
|
+
break
|
|
127
|
+
# Only match at word boundaries (start of string or preceded by whitespace/punctuation)
|
|
128
|
+
if pos > 0 and text_lower[pos - 1].isalnum():
|
|
129
|
+
idx = pos + len(prefix)
|
|
130
|
+
continue
|
|
131
|
+
# Extract from the prefix to end of line (or next sentence boundary)
|
|
132
|
+
end = text.find("\n", pos)
|
|
133
|
+
if end == -1:
|
|
134
|
+
end = len(text)
|
|
135
|
+
fragment = text[pos:end].strip()
|
|
136
|
+
# Trim trailing punctuation/quotes that are part of prose
|
|
137
|
+
fragment = fragment.rstrip(".,;:!?\"')")
|
|
138
|
+
fragment = _sanitize_candidate_fragment(fragment)
|
|
139
|
+
if fragment:
|
|
140
|
+
candidates.append(fragment)
|
|
141
|
+
idx = pos + len(prefix)
|
|
142
|
+
|
|
143
|
+
return candidates
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _scan_text_for_t3(text: str) -> Tuple[bool, str, Optional[MutativeResult]]:
|
|
147
|
+
"""Scan free-form text for T3 (dangerous) command intent using the verb detector.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
text: Combined prompt/description text.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
(is_t3, matched_command, danger_result) tuple.
|
|
154
|
+
"""
|
|
155
|
+
candidates = _extract_command_candidates(text)
|
|
156
|
+
|
|
157
|
+
for candidate in candidates:
|
|
158
|
+
result = detect_mutative_command(candidate)
|
|
159
|
+
if result.is_mutative:
|
|
160
|
+
return True, candidate, result
|
|
161
|
+
|
|
162
|
+
return False, "", None
|
|
163
|
+
|
|
164
|
+
__all__ = [
|
|
165
|
+
"TaskValidator",
|
|
166
|
+
"TaskValidationResult",
|
|
167
|
+
"validate_task_invocation",
|
|
168
|
+
"AVAILABLE_AGENTS",
|
|
169
|
+
"META_AGENTS",
|
|
170
|
+
"T3_KEYWORDS",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class TaskValidationResult:
|
|
176
|
+
"""Result of Task tool validation."""
|
|
177
|
+
allowed: bool
|
|
178
|
+
tier: SecurityTier
|
|
179
|
+
reason: str
|
|
180
|
+
agent_name: str = ""
|
|
181
|
+
has_context: bool = False
|
|
182
|
+
is_t3_operation: bool = False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TaskValidator:
|
|
186
|
+
"""Validator for Task tool invocations."""
|
|
187
|
+
|
|
188
|
+
def __init__(self, available_agents: Optional[List[str]] = None):
|
|
189
|
+
"""
|
|
190
|
+
Initialize validator.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
available_agents: Override available agents list
|
|
194
|
+
"""
|
|
195
|
+
self.available_agents = available_agents or AVAILABLE_AGENTS
|
|
196
|
+
|
|
197
|
+
def validate(self, parameters: Dict[str, Any]) -> TaskValidationResult:
|
|
198
|
+
"""
|
|
199
|
+
Validate Task tool invocation.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
parameters: Task tool parameters
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
TaskValidationResult with validation details
|
|
206
|
+
"""
|
|
207
|
+
agent_name = parameters.get("subagent_type", "unknown")
|
|
208
|
+
prompt = parameters.get("prompt", "")
|
|
209
|
+
description = parameters.get("description", "")
|
|
210
|
+
|
|
211
|
+
# additionalContext means prompt is never mutated, so T3 detection
|
|
212
|
+
# runs directly against the original user prompt.
|
|
213
|
+
user_task_for_t3_check = prompt
|
|
214
|
+
|
|
215
|
+
logger.info(f"Task tool validation for agent: {agent_name}")
|
|
216
|
+
|
|
217
|
+
# Check agent exists
|
|
218
|
+
if agent_name not in self.available_agents:
|
|
219
|
+
error_msg = f"Unknown agent: '{agent_name}'\n\n"
|
|
220
|
+
error_msg += f"Available agents:\n"
|
|
221
|
+
for agent in sorted(self.available_agents):
|
|
222
|
+
error_msg += f" - {agent}\n"
|
|
223
|
+
error_msg += "\nRefer to the Surface Routing Recommendation for agent selection.\n"
|
|
224
|
+
error_msg += f"\nCorrect usage: Task(subagent_type=\"<agent-name>\", ...)"
|
|
225
|
+
|
|
226
|
+
return TaskValidationResult(
|
|
227
|
+
allowed=False,
|
|
228
|
+
tier=SecurityTier.T3_BLOCKED,
|
|
229
|
+
reason=error_msg,
|
|
230
|
+
agent_name=agent_name,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Context is injected via additionalContext by the adapter, not by
|
|
234
|
+
# mutating the prompt. The validator cannot check additionalContext
|
|
235
|
+
# (it only sees parameters), so we determine context status by agent type.
|
|
236
|
+
# Meta-agents never receive context by design.
|
|
237
|
+
has_context = agent_name not in META_AGENTS
|
|
238
|
+
|
|
239
|
+
# Check for T3 operations (use original user task to avoid false positives from context)
|
|
240
|
+
is_t3 = self._is_t3_operation(user_task_for_t3_check, description)
|
|
241
|
+
|
|
242
|
+
logger.info(
|
|
243
|
+
f"Task invocation validated: {agent_name} "
|
|
244
|
+
f"(T3={is_t3}, context={has_context})"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
tier = SecurityTier.T3_BLOCKED if is_t3 else SecurityTier.T0_READ_ONLY
|
|
248
|
+
reason = (
|
|
249
|
+
f"Task invocation allowed for {agent_name}; T3 execution still requires "
|
|
250
|
+
f"nonce-based approval at Bash time"
|
|
251
|
+
if is_t3
|
|
252
|
+
else f"Task invocation allowed for {agent_name}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return TaskValidationResult(
|
|
256
|
+
allowed=True,
|
|
257
|
+
tier=tier,
|
|
258
|
+
reason=reason,
|
|
259
|
+
agent_name=agent_name,
|
|
260
|
+
has_context=has_context,
|
|
261
|
+
is_t3_operation=is_t3,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def _is_t3_operation(self, prompt: str, description: str) -> bool:
|
|
265
|
+
"""Check if this is a T3 (destructive) operation using the verb detector."""
|
|
266
|
+
combined = f"{description} {prompt}"
|
|
267
|
+
is_t3, _, _ = _scan_text_for_t3(combined)
|
|
268
|
+
return is_t3
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def validate_task_invocation(parameters: Dict[str, Any]) -> TaskValidationResult:
|
|
273
|
+
"""
|
|
274
|
+
Validate Task tool invocation (convenience function).
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
parameters: Task tool parameters
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
TaskValidationResult
|
|
281
|
+
"""
|
|
282
|
+
validator = TaskValidator()
|
|
283
|
+
return validator.validate(parameters)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation Module: Commit message validation for bash_validator
|
|
3
|
+
|
|
4
|
+
This module provides commit message validation that is exclusively used
|
|
5
|
+
by hooks/modules/tools/bash_validator.py to enforce git commit standards.
|
|
6
|
+
|
|
7
|
+
Note: This is an internal module. Do not import directly in agent code.
|
|
8
|
+
Commit validation is automatically enforced via bash_validator.py.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .commit_validator import (
|
|
12
|
+
CommitMessageValidator,
|
|
13
|
+
ValidationResult,
|
|
14
|
+
validate_commit_message,
|
|
15
|
+
safe_validate_before_commit,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CommitMessageValidator",
|
|
20
|
+
"ValidationResult",
|
|
21
|
+
"validate_commit_message",
|
|
22
|
+
"safe_validate_before_commit",
|
|
23
|
+
]
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Commit Message Validator
|
|
3
|
+
|
|
4
|
+
Validates commit messages against project standards before execution.
|
|
5
|
+
This prevents commits with forbidden footers or incorrect format from
|
|
6
|
+
being pushed to the repository.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from commit_validator import CommitMessageValidator
|
|
10
|
+
|
|
11
|
+
validator = CommitMessageValidator()
|
|
12
|
+
validation = validator.validate(commit_message)
|
|
13
|
+
|
|
14
|
+
if not validation.valid:
|
|
15
|
+
for error in validation.errors:
|
|
16
|
+
print(f"Error: {error['message']}")
|
|
17
|
+
# Do not proceed with commit
|
|
18
|
+
else:
|
|
19
|
+
# Safe to commit
|
|
20
|
+
git commit -m "$commit_message"
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
from typing import Dict, List, Any, Optional
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ValidationResult:
|
|
33
|
+
"""Result of commit message validation."""
|
|
34
|
+
valid: bool
|
|
35
|
+
errors: List[Dict[str, str]]
|
|
36
|
+
warnings: List[Dict[str, str]] = None
|
|
37
|
+
|
|
38
|
+
def __post_init__(self):
|
|
39
|
+
if self.warnings is None:
|
|
40
|
+
self.warnings = []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CommitMessageValidator:
|
|
44
|
+
"""
|
|
45
|
+
Validates git commit messages against project standards.
|
|
46
|
+
|
|
47
|
+
Standards are defined in .claude/config/git_standards.json
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
51
|
+
"""
|
|
52
|
+
Initialize validator with configuration.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config_path: Optional path to git_standards.json
|
|
56
|
+
If None, uses default location
|
|
57
|
+
"""
|
|
58
|
+
if config_path is None:
|
|
59
|
+
# Default path relative to this file
|
|
60
|
+
# From hooks/modules/validation/ go up to gaia-ops root
|
|
61
|
+
# __file__ -> hooks/modules/validation/commit_validator.py
|
|
62
|
+
# dirname(dirname(dirname(dirname(__file__)))) -> gaia-ops root
|
|
63
|
+
base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
64
|
+
config_path = os.path.join(base_path, 'config', 'git_standards.json')
|
|
65
|
+
else:
|
|
66
|
+
# If config_path provided, derive base_path from it
|
|
67
|
+
base_path = os.path.dirname(os.path.dirname(config_path))
|
|
68
|
+
|
|
69
|
+
self.base_path = base_path
|
|
70
|
+
self.config_path = config_path
|
|
71
|
+
self.config = self._load_config()
|
|
72
|
+
self.standards = self.config.get('commit_message', {})
|
|
73
|
+
self.enforcement = self.config.get('enforcement', {})
|
|
74
|
+
|
|
75
|
+
def _load_config(self) -> Dict[str, Any]:
|
|
76
|
+
"""Load git standards configuration from JSON file."""
|
|
77
|
+
if not os.path.exists(self.config_path):
|
|
78
|
+
raise FileNotFoundError(
|
|
79
|
+
f"Git standards configuration not found at: {self.config_path}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
with open(self.config_path, 'r') as f:
|
|
83
|
+
return json.load(f)
|
|
84
|
+
|
|
85
|
+
def validate(self, message: str) -> ValidationResult:
|
|
86
|
+
"""
|
|
87
|
+
Validate a commit message against all standards.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
message: The commit message to validate
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
ValidationResult with valid status and any errors/warnings
|
|
94
|
+
"""
|
|
95
|
+
errors = []
|
|
96
|
+
warnings = []
|
|
97
|
+
|
|
98
|
+
# 1. Check for forbidden footers (CRITICAL)
|
|
99
|
+
footer_errors = self._check_forbidden_footers(message)
|
|
100
|
+
errors.extend(footer_errors)
|
|
101
|
+
|
|
102
|
+
# 2. Check conventional commits format
|
|
103
|
+
format_errors = self._check_conventional_format(message)
|
|
104
|
+
errors.extend(format_errors)
|
|
105
|
+
|
|
106
|
+
# 3. Check subject line rules
|
|
107
|
+
subject_errors = self._check_subject_rules(message)
|
|
108
|
+
errors.extend(subject_errors)
|
|
109
|
+
|
|
110
|
+
# 4. Check body rules (warnings only)
|
|
111
|
+
body_warnings = self._check_body_rules(message)
|
|
112
|
+
warnings.extend(body_warnings)
|
|
113
|
+
|
|
114
|
+
# Log violations if configured
|
|
115
|
+
if errors and self.enforcement.get('log_violations', False):
|
|
116
|
+
self._log_violation(message, errors)
|
|
117
|
+
|
|
118
|
+
return ValidationResult(
|
|
119
|
+
valid=len(errors) == 0,
|
|
120
|
+
errors=errors,
|
|
121
|
+
warnings=warnings
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _check_forbidden_footers(self, message: str) -> List[Dict[str, str]]:
|
|
125
|
+
"""Check for forbidden footers in commit message."""
|
|
126
|
+
errors = []
|
|
127
|
+
forbidden = self.standards.get('footer_forbidden', [])
|
|
128
|
+
|
|
129
|
+
for forbidden_text in forbidden:
|
|
130
|
+
if forbidden_text.lower() in message.lower():
|
|
131
|
+
errors.append({
|
|
132
|
+
'type': 'FORBIDDEN_FOOTER',
|
|
133
|
+
'message': f"Commit message contains forbidden footer: '{forbidden_text}'",
|
|
134
|
+
'fix': f"Remove all occurrences of '{forbidden_text}'",
|
|
135
|
+
'severity': 'error'
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return errors
|
|
139
|
+
|
|
140
|
+
def _check_conventional_format(self, message: str) -> List[Dict[str, str]]:
|
|
141
|
+
"""Check if message follows Conventional Commits format."""
|
|
142
|
+
errors = []
|
|
143
|
+
|
|
144
|
+
# Get first line (subject)
|
|
145
|
+
lines = message.split('\n')
|
|
146
|
+
subject = lines[0].strip()
|
|
147
|
+
|
|
148
|
+
# Pattern: type(scope)?: description
|
|
149
|
+
# Examples: feat: add feature, fix(api): correct bug
|
|
150
|
+
allowed_types = '|'.join(self.standards.get('type_allowed', []))
|
|
151
|
+
pattern = rf'^({allowed_types})(\(.+?\))?: .+$'
|
|
152
|
+
|
|
153
|
+
if not re.match(pattern, subject):
|
|
154
|
+
errors.append({
|
|
155
|
+
'type': 'INVALID_FORMAT',
|
|
156
|
+
'message': 'Commit message does not follow Conventional Commits format',
|
|
157
|
+
'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(self.standards.get('type_allowed', []))}",
|
|
158
|
+
'severity': 'error',
|
|
159
|
+
'examples': self.standards.get('examples_valid', [])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return errors
|
|
163
|
+
|
|
164
|
+
def _check_subject_rules(self, message: str) -> List[Dict[str, str]]:
|
|
165
|
+
"""Check subject line specific rules."""
|
|
166
|
+
errors = []
|
|
167
|
+
|
|
168
|
+
lines = message.split('\n')
|
|
169
|
+
subject = lines[0].strip()
|
|
170
|
+
|
|
171
|
+
# Extract description part (after type and scope)
|
|
172
|
+
# Example: "feat(scope): description" -> "description"
|
|
173
|
+
match = re.match(r'^[a-z]+(\(.+?\))?: (.+)$', subject)
|
|
174
|
+
if match:
|
|
175
|
+
description = match.group(2)
|
|
176
|
+
|
|
177
|
+
# Check max length
|
|
178
|
+
max_length = self.standards.get('subject_max_length', 72)
|
|
179
|
+
if len(subject) > max_length:
|
|
180
|
+
errors.append({
|
|
181
|
+
'type': 'SUBJECT_TOO_LONG',
|
|
182
|
+
'message': f'Subject line exceeds {max_length} characters (current: {len(subject)})',
|
|
183
|
+
'fix': f'Shorten subject to {max_length} characters or less',
|
|
184
|
+
'severity': 'error'
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
# Check for period at end
|
|
188
|
+
rules = self.standards.get('subject_rules', {})
|
|
189
|
+
if rules.get('no_period_at_end', True) and description.endswith('.'):
|
|
190
|
+
errors.append({
|
|
191
|
+
'type': 'SUBJECT_ENDS_WITH_PERIOD',
|
|
192
|
+
'message': 'Subject line should not end with a period',
|
|
193
|
+
'fix': 'Remove the period at the end of the subject',
|
|
194
|
+
'severity': 'error'
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
# Check for emojis in subject line
|
|
198
|
+
if rules.get('no_emoji', False):
|
|
199
|
+
emoji_pattern = re.compile(
|
|
200
|
+
"["
|
|
201
|
+
"\U0001F600-\U0001F64F" # emoticons
|
|
202
|
+
"\U0001F300-\U0001F5FF" # symbols & pictographs
|
|
203
|
+
"\U0001F680-\U0001F6FF" # transport & map symbols
|
|
204
|
+
"\U0001F700-\U0001F77F" # alchemical symbols
|
|
205
|
+
"\U0001F780-\U0001F7FF" # Geometric Shapes Extended
|
|
206
|
+
"\U0001F800-\U0001F8FF" # Supplemental Arrows-C
|
|
207
|
+
"\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
|
|
208
|
+
"\U0001FA00-\U0001FA6F" # Chess Symbols
|
|
209
|
+
"\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A
|
|
210
|
+
"\U00002702-\U000027B0" # Dingbats
|
|
211
|
+
"\U000024C2-\U0001F251" # Enclosed characters
|
|
212
|
+
"]+", flags=re.UNICODE
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if emoji_pattern.search(subject):
|
|
216
|
+
errors.append({
|
|
217
|
+
'type': 'SUBJECT_CONTAINS_EMOJI',
|
|
218
|
+
'message': 'Subject line contains emojis which are not allowed',
|
|
219
|
+
'fix': 'Remove all emojis from the subject line',
|
|
220
|
+
'severity': 'error'
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
return errors
|
|
224
|
+
|
|
225
|
+
def _check_body_rules(self, message: str) -> List[Dict[str, str]]:
|
|
226
|
+
"""Check body rules (returns warnings, not errors)."""
|
|
227
|
+
warnings = []
|
|
228
|
+
|
|
229
|
+
lines = message.split('\n')
|
|
230
|
+
|
|
231
|
+
# Check if there's a body (more than just subject)
|
|
232
|
+
if len(lines) <= 1:
|
|
233
|
+
return warnings
|
|
234
|
+
|
|
235
|
+
# Check blank line after subject
|
|
236
|
+
if len(lines) > 1 and lines[1].strip() != '':
|
|
237
|
+
warnings.append({
|
|
238
|
+
'type': 'MISSING_BLANK_LINE',
|
|
239
|
+
'message': 'Missing blank line between subject and body',
|
|
240
|
+
'fix': 'Add a blank line after the subject line',
|
|
241
|
+
'severity': 'warning'
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
# Check body line length
|
|
245
|
+
max_length = self.standards.get('body_max_line_length', 72)
|
|
246
|
+
for i, line in enumerate(lines[2:], start=3): # Skip subject and blank line
|
|
247
|
+
if len(line) > max_length and not line.startswith('http'):
|
|
248
|
+
warnings.append({
|
|
249
|
+
'type': 'BODY_LINE_TOO_LONG',
|
|
250
|
+
'message': f'Body line {i} exceeds {max_length} characters',
|
|
251
|
+
'fix': f'Wrap line to {max_length} characters',
|
|
252
|
+
'severity': 'warning'
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
return warnings
|
|
256
|
+
|
|
257
|
+
def _log_violation(self, message: str, errors: List[Dict[str, str]]):
|
|
258
|
+
"""Log commit message violation for audit trail."""
|
|
259
|
+
log_path = self.enforcement.get('log_path', '.claude/logs/commit-violations.jsonl')
|
|
260
|
+
|
|
261
|
+
# If log_path is relative, resolve from base_path (not cwd)
|
|
262
|
+
if not os.path.isabs(log_path):
|
|
263
|
+
# Remove leading ./ if present
|
|
264
|
+
log_path = log_path.lstrip('./')
|
|
265
|
+
# If starts with 'claude/', remove it since base_path already points to .claude/
|
|
266
|
+
if log_path.startswith('claude/'):
|
|
267
|
+
log_path = log_path[7:] # Remove 'claude/' prefix
|
|
268
|
+
log_path = os.path.join(self.base_path, log_path)
|
|
269
|
+
|
|
270
|
+
# Ensure log directory exists
|
|
271
|
+
log_dir = os.path.dirname(log_path)
|
|
272
|
+
if log_dir and not os.path.exists(log_dir):
|
|
273
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
274
|
+
|
|
275
|
+
log_entry = {
|
|
276
|
+
'timestamp': datetime.now().isoformat(),
|
|
277
|
+
'message': message[:100] + ('...' if len(message) > 100 else ''),
|
|
278
|
+
'errors': errors,
|
|
279
|
+
'error_count': len(errors)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
with open(log_path, 'a') as f:
|
|
283
|
+
f.write(json.dumps(log_entry) + '\n')
|
|
284
|
+
|
|
285
|
+
def get_examples(self) -> Dict[str, List[str]]:
|
|
286
|
+
"""Get example commit messages (valid and invalid)."""
|
|
287
|
+
return {
|
|
288
|
+
'valid': self.standards.get('examples_valid', []),
|
|
289
|
+
'invalid': self.standards.get('examples_invalid', [])
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
def get_allowed_types(self) -> List[str]:
|
|
293
|
+
"""Get list of allowed commit types."""
|
|
294
|
+
return self.standards.get('type_allowed', [])
|
|
295
|
+
|
|
296
|
+
def format_error_message(self, validation: ValidationResult) -> str:
|
|
297
|
+
"""
|
|
298
|
+
Format validation errors into human-readable message.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
validation: ValidationResult from validate()
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Formatted error message string
|
|
305
|
+
"""
|
|
306
|
+
if validation.valid:
|
|
307
|
+
return "[OK] Commit message is valid"
|
|
308
|
+
|
|
309
|
+
lines = ["[ERROR] Commit message validation failed:\n"]
|
|
310
|
+
|
|
311
|
+
for error in validation.errors:
|
|
312
|
+
lines.append(f" [{error['type']}]")
|
|
313
|
+
lines.append(f" {error['message']}")
|
|
314
|
+
lines.append(f" Fix: {error['fix']}")
|
|
315
|
+
|
|
316
|
+
if 'examples' in error:
|
|
317
|
+
lines.append(f" Examples:")
|
|
318
|
+
for example in error['examples'][:3]:
|
|
319
|
+
lines.append(f" - {example}")
|
|
320
|
+
|
|
321
|
+
lines.append("")
|
|
322
|
+
|
|
323
|
+
if validation.warnings:
|
|
324
|
+
lines.append("[WARNING] Warnings:")
|
|
325
|
+
for warning in validation.warnings:
|
|
326
|
+
lines.append(f" - {warning['message']}")
|
|
327
|
+
lines.append("")
|
|
328
|
+
|
|
329
|
+
return "\n".join(lines)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# Convenience function for quick validation
|
|
333
|
+
def validate_commit_message(message: str) -> ValidationResult:
|
|
334
|
+
"""
|
|
335
|
+
Quick validation function.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
message: Commit message to validate
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
ValidationResult
|
|
342
|
+
|
|
343
|
+
Example:
|
|
344
|
+
validation = validate_commit_message("feat: add new feature")
|
|
345
|
+
if not validation.valid:
|
|
346
|
+
print("Invalid commit message")
|
|
347
|
+
"""
|
|
348
|
+
validator = CommitMessageValidator()
|
|
349
|
+
return validator.validate(message)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# Function for use in git commit workflow
|
|
353
|
+
def safe_validate_before_commit(message: str) -> bool:
|
|
354
|
+
"""
|
|
355
|
+
Validate commit message and print errors if invalid.
|
|
356
|
+
|
|
357
|
+
This is the primary function that agents should call before git commit.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
message: Commit message to validate
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
True if valid, False if invalid (with errors printed)
|
|
364
|
+
|
|
365
|
+
Example:
|
|
366
|
+
if not safe_validate_before_commit(commit_message):
|
|
367
|
+
return {"status": "failed", "reason": "commit_validation_failed"}
|
|
368
|
+
|
|
369
|
+
# Safe to commit
|
|
370
|
+
Bash(f'git commit -m "{commit_message}"')
|
|
371
|
+
"""
|
|
372
|
+
validator = CommitMessageValidator()
|
|
373
|
+
validation = validator.validate(message)
|
|
374
|
+
|
|
375
|
+
if not validation.valid:
|
|
376
|
+
error_message = validator.format_error_message(validation)
|
|
377
|
+
print(error_message)
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
return True
|