@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
|
@@ -370,6 +370,10 @@ class GitScanner(BaseScanner):
|
|
|
370
370
|
def scan(self, root: Path) -> ScanResult:
|
|
371
371
|
"""Scan the project for git configuration.
|
|
372
372
|
|
|
373
|
+
In multi-repo mode (workspace_info.is_multi_repo), scans ALL
|
|
374
|
+
subdirectories with .git and produces a 'repos' array. In
|
|
375
|
+
single-repo mode, behaves as before.
|
|
376
|
+
|
|
373
377
|
Args:
|
|
374
378
|
root: Absolute path to the project root directory.
|
|
375
379
|
|
|
@@ -379,6 +383,17 @@ class GitScanner(BaseScanner):
|
|
|
379
383
|
start_ms = time.monotonic() * 1000
|
|
380
384
|
warnings: List[str] = []
|
|
381
385
|
|
|
386
|
+
# Multi-repo mode: scan all repo subdirectories
|
|
387
|
+
if self.workspace_info and self.workspace_info.is_multi_repo:
|
|
388
|
+
section = self._scan_multi_repo(root, warnings)
|
|
389
|
+
elapsed = (time.monotonic() * 1000) - start_ms
|
|
390
|
+
return self.make_result(
|
|
391
|
+
sections={"git": section},
|
|
392
|
+
warnings=warnings,
|
|
393
|
+
duration_ms=elapsed,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Single-repo mode (original behavior)
|
|
382
397
|
git_dir = root / ".git"
|
|
383
398
|
git_root = root
|
|
384
399
|
|
|
@@ -446,6 +461,69 @@ class GitScanner(BaseScanner):
|
|
|
446
461
|
duration_ms=elapsed,
|
|
447
462
|
)
|
|
448
463
|
|
|
464
|
+
def _scan_multi_repo(
|
|
465
|
+
self, root: Path, warnings: List[str]
|
|
466
|
+
) -> Dict[str, Any]:
|
|
467
|
+
"""Scan all repos in a multi-repo workspace.
|
|
468
|
+
|
|
469
|
+
Produces a section with 'repos' array where each entry has:
|
|
470
|
+
name, path, remote_url, platform, default_branch.
|
|
471
|
+
|
|
472
|
+
Also determines the primary platform from the first repo's origin.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
root: Workspace root path.
|
|
476
|
+
warnings: Warning accumulator.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Git section dict with 'repos' array and aggregate fields.
|
|
480
|
+
"""
|
|
481
|
+
repos: List[Dict[str, Any]] = []
|
|
482
|
+
primary_platform: Optional[str] = None
|
|
483
|
+
|
|
484
|
+
for repo_dir in self.workspace_info.repo_dirs:
|
|
485
|
+
git_dir = repo_dir / ".git"
|
|
486
|
+
if not git_dir.is_dir():
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
git_config = _parse_git_config(git_dir)
|
|
490
|
+
remotes = git_config["remotes"]
|
|
491
|
+
platform = _determine_primary_platform(remotes)
|
|
492
|
+
default_branch = _detect_default_branch(git_dir)
|
|
493
|
+
|
|
494
|
+
# Get origin remote URL
|
|
495
|
+
origin_url = None
|
|
496
|
+
for remote in remotes:
|
|
497
|
+
if remote.get("name") == "origin":
|
|
498
|
+
origin_url = remote.get("url")
|
|
499
|
+
break
|
|
500
|
+
if origin_url is None and remotes:
|
|
501
|
+
origin_url = remotes[0].get("url")
|
|
502
|
+
|
|
503
|
+
repo_entry: Dict[str, Any] = {
|
|
504
|
+
"name": repo_dir.name,
|
|
505
|
+
"path": str(repo_dir.relative_to(root)),
|
|
506
|
+
"remote_url": origin_url,
|
|
507
|
+
"platform": platform,
|
|
508
|
+
"default_branch": default_branch,
|
|
509
|
+
}
|
|
510
|
+
repos.append(repo_entry)
|
|
511
|
+
|
|
512
|
+
if primary_platform is None and platform:
|
|
513
|
+
primary_platform = platform
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
"platform": primary_platform,
|
|
517
|
+
"workspace_type": "multi-repo",
|
|
518
|
+
"repos": repos,
|
|
519
|
+
"branch_strategy": {
|
|
520
|
+
"detected": False,
|
|
521
|
+
"pattern": None,
|
|
522
|
+
"indicators": ["multi-repo workspace — per-repo strategies vary"],
|
|
523
|
+
},
|
|
524
|
+
"monorepo": {"workspace_config": None},
|
|
525
|
+
}
|
|
526
|
+
|
|
449
527
|
@staticmethod
|
|
450
528
|
def _find_git_in_subdirs(
|
|
451
529
|
root: Path,
|
|
@@ -77,6 +77,9 @@ _CICD_MARKERS: List[Dict[str, Any]] = [
|
|
|
77
77
|
{"platform": "gitlab-ci", "type": "file", "path": ".gitlab-ci.yml"},
|
|
78
78
|
{"platform": "jenkins", "type": "file", "path": "Jenkinsfile"},
|
|
79
79
|
{"platform": "circleci", "type": "dir", "path": ".circleci"},
|
|
80
|
+
{"platform": "bitbucket-pipelines", "type": "file", "path": "bitbucket-pipelines.yml"},
|
|
81
|
+
{"platform": "cloud-build", "type": "file", "path": "cloudbuild.yaml"},
|
|
82
|
+
{"platform": "cloud-build", "type": "file", "path": "cloudbuild.json"},
|
|
80
83
|
]
|
|
81
84
|
|
|
82
85
|
# ---------------------------------------------------------------------------
|
|
@@ -114,6 +117,10 @@ class InfrastructureScanner(BaseScanner):
|
|
|
114
117
|
def scan(self, root: Path) -> ScanResult:
|
|
115
118
|
"""Scan for infrastructure indicators.
|
|
116
119
|
|
|
120
|
+
In multi-repo mode, scans each repo subdirectory and tags IaC
|
|
121
|
+
entries with their containing repo name. In single-repo mode,
|
|
122
|
+
behaves as before.
|
|
123
|
+
|
|
117
124
|
Args:
|
|
118
125
|
root: Absolute path to the project root directory.
|
|
119
126
|
|
|
@@ -132,6 +139,12 @@ class InfrastructureScanner(BaseScanner):
|
|
|
132
139
|
paths = self._detect_paths(root, warnings)
|
|
133
140
|
app_services = self._detect_application_services(root, warnings)
|
|
134
141
|
|
|
142
|
+
# Multi-repo mode: also scan each repo subdirectory and tag results
|
|
143
|
+
if self.workspace_info and self.workspace_info.is_multi_repo:
|
|
144
|
+
self._enrich_multi_repo(
|
|
145
|
+
root, iac, containers, ci_cd, warnings
|
|
146
|
+
)
|
|
147
|
+
|
|
135
148
|
# Only produce section when at least one indicator is found
|
|
136
149
|
has_indicators = (
|
|
137
150
|
cloud_providers
|
|
@@ -174,6 +187,58 @@ class InfrastructureScanner(BaseScanner):
|
|
|
174
187
|
duration_ms = (time.monotonic() - start) * 1000
|
|
175
188
|
return self.make_result(sections={}, warnings=[str(exc)], duration_ms=duration_ms)
|
|
176
189
|
|
|
190
|
+
def _enrich_multi_repo(
|
|
191
|
+
self,
|
|
192
|
+
root: Path,
|
|
193
|
+
iac: List[Dict[str, Any]],
|
|
194
|
+
containers: List[Dict[str, Any]],
|
|
195
|
+
ci_cd: List[Dict[str, Any]],
|
|
196
|
+
warnings: List[str],
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Tag IaC, container, and CI/CD entries with their containing repo.
|
|
199
|
+
|
|
200
|
+
For each entry whose base_path or config_path starts with a known
|
|
201
|
+
repo directory, adds a 'repo' field with the repo name. This helps
|
|
202
|
+
agents understand which repo owns each infrastructure component.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
root: Workspace root path.
|
|
206
|
+
iac: IaC entries list (mutated in place).
|
|
207
|
+
containers: Container entries list (mutated in place).
|
|
208
|
+
ci_cd: CI/CD entries list (mutated in place).
|
|
209
|
+
warnings: Warning accumulator.
|
|
210
|
+
"""
|
|
211
|
+
repo_names = {
|
|
212
|
+
str(rd.relative_to(root)): rd.name
|
|
213
|
+
for rd in self.workspace_info.repo_dirs
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
def _tag_repo(entry: Dict[str, Any], path_key: str) -> None:
|
|
217
|
+
"""Add 'repo' field if path matches a known repo directory."""
|
|
218
|
+
path_val = entry.get(path_key, "")
|
|
219
|
+
if not path_val:
|
|
220
|
+
return
|
|
221
|
+
for repo_path, repo_name in repo_names.items():
|
|
222
|
+
if path_val == repo_path or path_val.startswith(repo_path + "/"):
|
|
223
|
+
entry["repo"] = repo_name
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
for entry in iac:
|
|
227
|
+
_tag_repo(entry, "base_path")
|
|
228
|
+
|
|
229
|
+
for entry in containers:
|
|
230
|
+
# Containers use 'files' list; tag based on first file's directory
|
|
231
|
+
files = entry.get("files", [])
|
|
232
|
+
if files:
|
|
233
|
+
first_file = files[0]
|
|
234
|
+
for repo_path, repo_name in repo_names.items():
|
|
235
|
+
if first_file.startswith(repo_path + "/") or first_file.startswith(repo_path):
|
|
236
|
+
entry["repo"] = repo_name
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
for entry in ci_cd:
|
|
240
|
+
_tag_repo(entry, "config_path")
|
|
241
|
+
|
|
177
242
|
# ------------------------------------------------------------------
|
|
178
243
|
# Cloud provider detection
|
|
179
244
|
# ------------------------------------------------------------------
|
|
@@ -151,6 +151,14 @@ class StackScanner(BaseScanner):
|
|
|
151
151
|
build_tools = self._detect_build_tools(root, warnings)
|
|
152
152
|
project_identity = self._detect_project_identity(root, languages, warnings)
|
|
153
153
|
|
|
154
|
+
# Multi-repo workspace override: if orchestrator detected multi-repo,
|
|
155
|
+
# set project type and add workspace_repos listing
|
|
156
|
+
if self.workspace_info and self.workspace_info.is_multi_repo:
|
|
157
|
+
project_identity["type"] = "multi-repo-workspace"
|
|
158
|
+
project_identity["workspace_repos"] = self._build_workspace_repos(
|
|
159
|
+
root, self.workspace_info.repo_dirs, warnings
|
|
160
|
+
)
|
|
161
|
+
|
|
154
162
|
sections: Dict[str, Any] = {
|
|
155
163
|
"project_identity": project_identity,
|
|
156
164
|
"stack": {
|
|
@@ -821,6 +829,108 @@ class StackScanner(BaseScanner):
|
|
|
821
829
|
|
|
822
830
|
return result
|
|
823
831
|
|
|
832
|
+
# ------------------------------------------------------------------
|
|
833
|
+
# Multi-repo workspace helpers
|
|
834
|
+
# ------------------------------------------------------------------
|
|
835
|
+
|
|
836
|
+
def _build_workspace_repos(
|
|
837
|
+
self,
|
|
838
|
+
root: Path,
|
|
839
|
+
repo_dirs: List[Path],
|
|
840
|
+
warnings: List[str],
|
|
841
|
+
) -> List[Dict[str, Any]]:
|
|
842
|
+
"""Build workspace_repos list for multi-repo workspaces.
|
|
843
|
+
|
|
844
|
+
For each subdirectory with .git, extracts name, relative path,
|
|
845
|
+
and primary language from the most prominent manifest file.
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
root: Workspace root path.
|
|
849
|
+
repo_dirs: List of subdirectory Paths that contain .git.
|
|
850
|
+
warnings: Warning accumulator.
|
|
851
|
+
|
|
852
|
+
Returns:
|
|
853
|
+
List of repo descriptor dicts.
|
|
854
|
+
"""
|
|
855
|
+
repos: List[Dict[str, Any]] = []
|
|
856
|
+
|
|
857
|
+
for repo_dir in repo_dirs:
|
|
858
|
+
repo_entry: Dict[str, Any] = {
|
|
859
|
+
"name": repo_dir.name,
|
|
860
|
+
"path": str(repo_dir.relative_to(root)),
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
# Detect primary language from manifest files
|
|
864
|
+
primary_language = self._detect_primary_language(repo_dir)
|
|
865
|
+
if primary_language:
|
|
866
|
+
repo_entry["primary_language"] = primary_language
|
|
867
|
+
|
|
868
|
+
# Detect role from directory naming conventions
|
|
869
|
+
repo_entry["role"] = self._infer_repo_role(repo_dir, primary_language)
|
|
870
|
+
|
|
871
|
+
repos.append(repo_entry)
|
|
872
|
+
|
|
873
|
+
return repos
|
|
874
|
+
|
|
875
|
+
@staticmethod
|
|
876
|
+
def _detect_primary_language(repo_dir: Path) -> Optional[str]:
|
|
877
|
+
"""Detect the primary language of a repo from its manifest files."""
|
|
878
|
+
manifest_checks = [
|
|
879
|
+
("package.json", "javascript"),
|
|
880
|
+
("pyproject.toml", "python"),
|
|
881
|
+
("setup.py", "python"),
|
|
882
|
+
("requirements.txt", "python"),
|
|
883
|
+
("go.mod", "go"),
|
|
884
|
+
("Cargo.toml", "rust"),
|
|
885
|
+
("pom.xml", "java"),
|
|
886
|
+
("build.gradle", "java"),
|
|
887
|
+
("composer.json", "php"),
|
|
888
|
+
("Gemfile", "ruby"),
|
|
889
|
+
]
|
|
890
|
+
|
|
891
|
+
for filename, language in manifest_checks:
|
|
892
|
+
if (repo_dir / filename).is_file():
|
|
893
|
+
# Check for TypeScript indicator
|
|
894
|
+
if language == "javascript":
|
|
895
|
+
for f in repo_dir.iterdir():
|
|
896
|
+
if f.is_file() and f.name.startswith("tsconfig") and f.name.endswith(".json"):
|
|
897
|
+
return "typescript"
|
|
898
|
+
return language
|
|
899
|
+
|
|
900
|
+
return None
|
|
901
|
+
|
|
902
|
+
@staticmethod
|
|
903
|
+
def _infer_repo_role(repo_dir: Path, primary_language: Optional[str]) -> str:
|
|
904
|
+
"""Infer the role of a repo from its name and contents.
|
|
905
|
+
|
|
906
|
+
Returns one of: gitops, iac, platform, agent, library, application.
|
|
907
|
+
"""
|
|
908
|
+
name_lower = repo_dir.name.lower()
|
|
909
|
+
|
|
910
|
+
# GitOps indicators
|
|
911
|
+
if any(kw in name_lower for kw in ("gitops", "flux", "argocd", "deploy")):
|
|
912
|
+
return "gitops"
|
|
913
|
+
|
|
914
|
+
# IaC indicators
|
|
915
|
+
if any(kw in name_lower for kw in ("iac", "infra", "terraform")):
|
|
916
|
+
return "iac"
|
|
917
|
+
# Also check for .tf files at root
|
|
918
|
+
try:
|
|
919
|
+
if any(f.suffix == ".tf" for f in repo_dir.iterdir() if f.is_file()):
|
|
920
|
+
return "iac"
|
|
921
|
+
except OSError:
|
|
922
|
+
pass
|
|
923
|
+
|
|
924
|
+
# Platform indicators
|
|
925
|
+
if any(kw in name_lower for kw in ("platform", "core", "shared", "common")):
|
|
926
|
+
return "platform"
|
|
927
|
+
|
|
928
|
+
# Agent indicators
|
|
929
|
+
if any(kw in name_lower for kw in ("agent", "bot", "assistant")):
|
|
930
|
+
return "agent"
|
|
931
|
+
|
|
932
|
+
return "application"
|
|
933
|
+
|
|
824
934
|
# ------------------------------------------------------------------
|
|
825
935
|
# File search helpers
|
|
826
936
|
# ------------------------------------------------------------------
|
package/tools/scan/setup.py
CHANGED
|
@@ -8,7 +8,7 @@ functionality that gaia-scan needs when operating on a fresh project
|
|
|
8
8
|
Functions:
|
|
9
9
|
- create_claude_directory: mkdir .claude/ with symlinks and subdirs
|
|
10
10
|
- copy_claude_md: deprecated no-op (identity now via submit hook)
|
|
11
|
-
- copy_settings_json:
|
|
11
|
+
- copy_settings_json: create minimal settings.json only if missing (non-invasive)
|
|
12
12
|
- install_git_hooks: copy commit-msg hook to all git repos
|
|
13
13
|
- generate_governance: interpolate governance.template.md
|
|
14
14
|
- ensure_gaia_ops_package: npm install @jaguilar87/gaia-ops
|
|
@@ -57,7 +57,7 @@ def _get_template_path(name: str) -> Path:
|
|
|
57
57
|
"""Get the path to a template file.
|
|
58
58
|
|
|
59
59
|
Args:
|
|
60
|
-
name: Template filename (e.g., '
|
|
60
|
+
name: Template filename (e.g., 'governance.template.md').
|
|
61
61
|
|
|
62
62
|
Returns:
|
|
63
63
|
Absolute path to the template file.
|
|
@@ -229,7 +229,7 @@ def copy_claude_md(project_root: Path) -> bool:
|
|
|
229
229
|
"""Deprecated — CLAUDE.md is no longer generated from template.
|
|
230
230
|
|
|
231
231
|
Orchestrator identity is now injected by the UserPromptSubmit hook
|
|
232
|
-
via ops_identity.py + on-demand skills (
|
|
232
|
+
via ops_identity.py + deterministic surface routing + on-demand skills (agent-response).
|
|
233
233
|
This avoids two sources of truth.
|
|
234
234
|
|
|
235
235
|
Kept as no-op for backward compatibility with callers.
|
|
@@ -239,33 +239,140 @@ def copy_claude_md(project_root: Path) -> bool:
|
|
|
239
239
|
|
|
240
240
|
|
|
241
241
|
def copy_settings_json(project_root: Path) -> bool:
|
|
242
|
-
"""
|
|
242
|
+
"""Create a minimal .claude/settings.json only if it does not exist.
|
|
243
243
|
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
Non-invasive: never overwrites an existing settings.json. Hooks are
|
|
245
|
+
provided by hooks.json (auto-discovered via the .claude/hooks symlink).
|
|
246
|
+
Env vars and permissions live in settings.local.json.
|
|
246
247
|
|
|
247
248
|
Args:
|
|
248
249
|
project_root: Project root directory.
|
|
249
250
|
|
|
250
251
|
Returns:
|
|
251
|
-
True if file
|
|
252
|
+
True if file exists (created or already present).
|
|
252
253
|
"""
|
|
253
|
-
template_path = _get_template_path("settings.template.json")
|
|
254
254
|
dest_path = project_root / ".claude" / "settings.json"
|
|
255
255
|
|
|
256
|
-
if
|
|
257
|
-
logger.
|
|
258
|
-
return
|
|
256
|
+
if dest_path.is_file():
|
|
257
|
+
logger.info("settings.json already exists — not overwriting")
|
|
258
|
+
return True
|
|
259
259
|
|
|
260
260
|
try:
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
262
|
+
dest_path.write_text("{}\n")
|
|
263
|
+
logger.info("settings.json created (minimal — hooks from hooks.json, env from settings.local.json)")
|
|
263
264
|
return True
|
|
264
265
|
except OSError as exc:
|
|
265
266
|
logger.error("Failed to write settings.json: %s", exc)
|
|
266
267
|
return False
|
|
267
268
|
|
|
268
269
|
|
|
270
|
+
def merge_hooks_to_settings_local(project_root: Path) -> bool:
|
|
271
|
+
"""Merge hooks from hooks.json into .claude/settings.local.json.
|
|
272
|
+
|
|
273
|
+
In npm mode, Claude Code reads hooks from settings files, not hooks.json
|
|
274
|
+
directly. This reads hooks.json from the installed package, converts
|
|
275
|
+
${CLAUDE_PLUGIN_ROOT}/hooks/<script> paths to .claude/hooks/<script>,
|
|
276
|
+
and merges into settings.local.json with deduplication by command string.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
project_root: Project root directory.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True if settings.local.json was modified.
|
|
283
|
+
"""
|
|
284
|
+
import re
|
|
285
|
+
|
|
286
|
+
claude_dir = project_root / ".claude"
|
|
287
|
+
settings_path = claude_dir / "settings.local.json"
|
|
288
|
+
|
|
289
|
+
# Find hooks.json from the package
|
|
290
|
+
hooks_json_path = None
|
|
291
|
+
# Strategy 1: installed npm package
|
|
292
|
+
pkg_root = _find_installed_package_root(project_root)
|
|
293
|
+
if pkg_root:
|
|
294
|
+
candidate = pkg_root / "hooks" / "hooks.json"
|
|
295
|
+
if candidate.is_file():
|
|
296
|
+
hooks_json_path = candidate
|
|
297
|
+
# Strategy 2: running from source (gaia-scan direct)
|
|
298
|
+
if not hooks_json_path:
|
|
299
|
+
candidate = _find_package_root() / "hooks" / "hooks.json"
|
|
300
|
+
if candidate.is_file():
|
|
301
|
+
hooks_json_path = candidate
|
|
302
|
+
|
|
303
|
+
if not hooks_json_path:
|
|
304
|
+
logger.info("hooks.json not found, skipping hooks merge")
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
hooks_data = json.loads(hooks_json_path.read_text())
|
|
309
|
+
except (json.JSONDecodeError, OSError):
|
|
310
|
+
logger.warning("hooks.json is invalid, skipping hooks merge")
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
# Unwrap outer "hooks" key if present
|
|
314
|
+
source_hooks = hooks_data.get("hooks", hooks_data)
|
|
315
|
+
|
|
316
|
+
# Convert ${CLAUDE_PLUGIN_ROOT}/hooks/<script> to .claude/hooks/<script>
|
|
317
|
+
def convert_command(cmd: str) -> str:
|
|
318
|
+
return re.sub(r'\$\{CLAUDE_PLUGIN_ROOT\}/hooks/', '.claude/hooks/', cmd)
|
|
319
|
+
|
|
320
|
+
converted_hooks: Dict[str, list] = {}
|
|
321
|
+
for event, entries in source_hooks.items():
|
|
322
|
+
converted_hooks[event] = []
|
|
323
|
+
for entry in entries:
|
|
324
|
+
new_entry = dict(entry)
|
|
325
|
+
if "hooks" in new_entry:
|
|
326
|
+
new_entry["hooks"] = [
|
|
327
|
+
{**h, "command": convert_command(h["command"])} if "command" in h else h
|
|
328
|
+
for h in new_entry["hooks"]
|
|
329
|
+
]
|
|
330
|
+
converted_hooks[event].append(new_entry)
|
|
331
|
+
|
|
332
|
+
# Load existing settings.local.json
|
|
333
|
+
existing: Dict[str, Any] = {}
|
|
334
|
+
if settings_path.exists():
|
|
335
|
+
try:
|
|
336
|
+
existing = json.loads(settings_path.read_text())
|
|
337
|
+
except (json.JSONDecodeError, OSError):
|
|
338
|
+
existing = {}
|
|
339
|
+
|
|
340
|
+
# Smart merge: deduplicate by command string
|
|
341
|
+
existing_hooks = existing.get("hooks", {})
|
|
342
|
+
changed = False
|
|
343
|
+
|
|
344
|
+
for event, new_entries in converted_hooks.items():
|
|
345
|
+
if event not in existing_hooks:
|
|
346
|
+
existing_hooks[event] = new_entries
|
|
347
|
+
changed = True
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
# Collect existing command strings
|
|
351
|
+
existing_commands: set = set()
|
|
352
|
+
for entry in existing_hooks[event]:
|
|
353
|
+
for h in entry.get("hooks", []):
|
|
354
|
+
if "command" in h:
|
|
355
|
+
existing_commands.add(h["command"])
|
|
356
|
+
|
|
357
|
+
# Add entries whose commands are not already present
|
|
358
|
+
for new_entry in new_entries:
|
|
359
|
+
new_commands = [h["command"] for h in new_entry.get("hooks", []) if "command" in h]
|
|
360
|
+
all_present = len(new_commands) > 0 and all(c in existing_commands for c in new_commands)
|
|
361
|
+
if not all_present:
|
|
362
|
+
existing_hooks[event].append(new_entry)
|
|
363
|
+
changed = True
|
|
364
|
+
|
|
365
|
+
if not changed:
|
|
366
|
+
logger.info("settings.local.json hooks already up to date")
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
existing["hooks"] = existing_hooks
|
|
370
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
371
|
+
settings_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
372
|
+
logger.info("Merged hooks into %s", settings_path)
|
|
373
|
+
return True
|
|
374
|
+
|
|
375
|
+
|
|
269
376
|
def install_git_hooks(project_root: Path) -> int:
|
|
270
377
|
"""Install commit-msg git hook to all detected git repositories.
|
|
271
378
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workspace Type Detection
|
|
3
|
+
|
|
4
|
+
Detects whether the scan root is a single-repo, monorepo, or multi-repo workspace.
|
|
5
|
+
Called by the orchestrator before individual scanners run, and importable by scanners
|
|
6
|
+
that need workspace-aware behavior.
|
|
7
|
+
|
|
8
|
+
Detection logic:
|
|
9
|
+
- If root has .git -> single-repo or monorepo (determined by stack scanner)
|
|
10
|
+
- If root has NO .git but 2+ immediate subdirectories have .git -> multi-repo-workspace
|
|
11
|
+
- Otherwise -> single-repo (default)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Directories to always skip during workspace scanning
|
|
22
|
+
_SKIP_DIRS = frozenset({
|
|
23
|
+
"node_modules", "__pycache__", ".tox", ".venv",
|
|
24
|
+
"venv", "dist", "build", ".next", ".nuxt", "target",
|
|
25
|
+
".pytest_cache", ".mypy_cache", ".ruff_cache", "vendor",
|
|
26
|
+
".terraform", ".terragrunt-cache",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class WorkspaceInfo:
|
|
32
|
+
"""Result of workspace type detection.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
workspace_type: One of 'single-repo', 'monorepo', 'multi-repo-workspace'.
|
|
36
|
+
repo_dirs: For multi-repo, list of subdirectory Paths that contain .git.
|
|
37
|
+
Empty for single-repo/monorepo.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
workspace_type: str = "single-repo"
|
|
41
|
+
repo_dirs: List[Path] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_multi_repo(self) -> bool:
|
|
45
|
+
return self.workspace_type == "multi-repo-workspace"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def detect_workspace_type(root: Path) -> WorkspaceInfo:
|
|
49
|
+
"""Detect the workspace type for the given root directory.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
root: Absolute path to the project root directory.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
WorkspaceInfo with the detected workspace type and repo directories.
|
|
56
|
+
"""
|
|
57
|
+
# If root itself has .git, it's a normal repo (single or monorepo)
|
|
58
|
+
if (root / ".git").is_dir():
|
|
59
|
+
return WorkspaceInfo(workspace_type="single-repo")
|
|
60
|
+
|
|
61
|
+
# Check immediate subdirectories for .git
|
|
62
|
+
git_subdirs: List[Path] = []
|
|
63
|
+
try:
|
|
64
|
+
for entry in sorted(root.iterdir()):
|
|
65
|
+
if not entry.is_dir():
|
|
66
|
+
continue
|
|
67
|
+
if entry.name.startswith(".") or entry.name in _SKIP_DIRS:
|
|
68
|
+
continue
|
|
69
|
+
if (entry / ".git").is_dir():
|
|
70
|
+
git_subdirs.append(entry)
|
|
71
|
+
except (PermissionError, OSError) as exc:
|
|
72
|
+
logger.debug("Failed to scan subdirectories of %s: %s", root, exc)
|
|
73
|
+
|
|
74
|
+
if len(git_subdirs) >= 2:
|
|
75
|
+
logger.info(
|
|
76
|
+
"Multi-repo workspace detected: %d repos in %s",
|
|
77
|
+
len(git_subdirs),
|
|
78
|
+
root,
|
|
79
|
+
)
|
|
80
|
+
return WorkspaceInfo(
|
|
81
|
+
workspace_type="multi-repo-workspace",
|
|
82
|
+
repo_dirs=git_subdirs,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return WorkspaceInfo(workspace_type="single-repo")
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_note": "Legacy file. Canonical source: context-contracts.json + cloud/{provider}.json",
|
|
3
|
-
"version": "3.0",
|
|
4
|
-
"provider": "aws",
|
|
5
|
-
"description": "Legacy AWS contracts (self-contained). Kept for backward compatibility with tests. Canonical source: context-contracts.json + cloud/aws.json. v3: removed backward-compat sections.",
|
|
6
|
-
"agents": {
|
|
7
|
-
"cloud-troubleshooter": {
|
|
8
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
9
|
-
"infrastructure", "orchestration", "cluster_details",
|
|
10
|
-
"infrastructure_topology", "terraform_infrastructure",
|
|
11
|
-
"gitops_configuration", "application_services",
|
|
12
|
-
"monitoring_observability",
|
|
13
|
-
"vpc_mapping", "load_balancers"],
|
|
14
|
-
"write": ["cluster_details", "infrastructure_topology", "vpc_mapping"]
|
|
15
|
-
},
|
|
16
|
-
"gitops-operator": {
|
|
17
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
18
|
-
"infrastructure", "orchestration", "gitops_configuration",
|
|
19
|
-
"cluster_details", "operational_guidelines"],
|
|
20
|
-
"write": ["gitops_configuration", "cluster_details"]
|
|
21
|
-
},
|
|
22
|
-
"terraform-architect": {
|
|
23
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
24
|
-
"infrastructure", "orchestration", "terraform_infrastructure",
|
|
25
|
-
"infrastructure_topology", "operational_guidelines",
|
|
26
|
-
"vpc_mapping", "load_balancers", "api_gateway"],
|
|
27
|
-
"write": ["terraform_infrastructure", "infrastructure_topology",
|
|
28
|
-
"vpc_mapping", "load_balancers", "api_gateway"]
|
|
29
|
-
},
|
|
30
|
-
"devops-developer": {
|
|
31
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
32
|
-
"infrastructure", "application_services",
|
|
33
|
-
"operational_guidelines"],
|
|
34
|
-
"write": ["application_services"]
|
|
35
|
-
},
|
|
36
|
-
"speckit-planner": {
|
|
37
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
38
|
-
"infrastructure", "operational_guidelines"],
|
|
39
|
-
"write": ["operational_guidelines"]
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_note": "Legacy file. Canonical source: context-contracts.json + cloud/{provider}.json",
|
|
3
|
-
"version": "3.0",
|
|
4
|
-
"provider": "gcp",
|
|
5
|
-
"description": "Legacy GCP contracts (self-contained). Kept for backward compatibility with tests. Canonical source: context-contracts.json + cloud/gcp.json. v3: removed backward-compat sections.",
|
|
6
|
-
"agents": {
|
|
7
|
-
"cloud-troubleshooter": {
|
|
8
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
9
|
-
"infrastructure", "orchestration", "cluster_details",
|
|
10
|
-
"infrastructure_topology", "terraform_infrastructure",
|
|
11
|
-
"gitops_configuration", "application_services",
|
|
12
|
-
"monitoring_observability"],
|
|
13
|
-
"write": ["cluster_details", "infrastructure_topology"]
|
|
14
|
-
},
|
|
15
|
-
"gitops-operator": {
|
|
16
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
17
|
-
"infrastructure", "orchestration", "gitops_configuration",
|
|
18
|
-
"cluster_details", "operational_guidelines"],
|
|
19
|
-
"write": ["gitops_configuration", "cluster_details"]
|
|
20
|
-
},
|
|
21
|
-
"terraform-architect": {
|
|
22
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
23
|
-
"infrastructure", "orchestration", "terraform_infrastructure",
|
|
24
|
-
"infrastructure_topology", "operational_guidelines"],
|
|
25
|
-
"write": ["terraform_infrastructure", "infrastructure_topology"]
|
|
26
|
-
},
|
|
27
|
-
"devops-developer": {
|
|
28
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
29
|
-
"infrastructure", "application_services",
|
|
30
|
-
"operational_guidelines"],
|
|
31
|
-
"write": ["application_services"]
|
|
32
|
-
},
|
|
33
|
-
"speckit-planner": {
|
|
34
|
-
"read": ["project_identity", "stack", "git", "environment",
|
|
35
|
-
"infrastructure", "operational_guidelines"],
|
|
36
|
-
"write": []
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|