@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,753 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Setup / Installation Functions for gaia-scan
|
|
3
|
+
|
|
4
|
+
Ported from the original gaia-init — provides all the installation and setup
|
|
5
|
+
functionality that gaia-scan needs when operating on a fresh project
|
|
6
|
+
(Mode 1) or refreshing an existing project (Mode 2).
|
|
7
|
+
|
|
8
|
+
Functions:
|
|
9
|
+
- create_claude_directory: mkdir .claude/ with symlinks and subdirs
|
|
10
|
+
- copy_claude_md: deprecated no-op (identity now via submit hook)
|
|
11
|
+
- copy_settings_json: create minimal settings.json only if missing (non-invasive)
|
|
12
|
+
- install_git_hooks: copy commit-msg hook to all git repos
|
|
13
|
+
- generate_governance: interpolate governance.template.md
|
|
14
|
+
- ensure_gaia_ops_package: npm install @jaguilar87/gaia-ops
|
|
15
|
+
- ensure_claude_code: check/install claude CLI
|
|
16
|
+
- generate_project_context: create/merge project-context.json
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _find_package_root() -> Path:
|
|
32
|
+
"""Find the gaia-ops plugin root directory.
|
|
33
|
+
|
|
34
|
+
Returns the directory containing this file's grandparent (tools/scan/setup.py
|
|
35
|
+
-> tools/ -> plugin root). This works both when running from the plugin
|
|
36
|
+
directory directly and when installed as a package.
|
|
37
|
+
"""
|
|
38
|
+
return Path(__file__).resolve().parent.parent.parent
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _find_installed_package_root(project_root: Path) -> Optional[Path]:
|
|
42
|
+
"""Find the installed @jaguilar87/gaia-ops package in node_modules.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
project_root: Project root directory.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Path to the package root, or None if not found.
|
|
49
|
+
"""
|
|
50
|
+
pkg_path = project_root / "node_modules" / "@jaguilar87" / "gaia-ops"
|
|
51
|
+
if pkg_path.is_dir():
|
|
52
|
+
return pkg_path
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_template_path(name: str) -> Path:
|
|
57
|
+
"""Get the path to a template file.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
name: Template filename (e.g., 'governance.template.md').
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Absolute path to the template file.
|
|
64
|
+
"""
|
|
65
|
+
return _find_package_root() / "templates" / name
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ensure_gaia_ops_package(project_root: Path) -> bool:
|
|
69
|
+
"""Ensure @jaguilar87/gaia-ops is installed as npm dependency.
|
|
70
|
+
|
|
71
|
+
Checks node_modules for the package. If not found, creates package.json
|
|
72
|
+
if needed and runs npm install.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
project_root: Project root directory.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if package is available (already installed or newly installed).
|
|
79
|
+
"""
|
|
80
|
+
pkg_path = project_root / "node_modules" / "@jaguilar87" / "gaia-ops" / "package.json"
|
|
81
|
+
if pkg_path.is_file():
|
|
82
|
+
logger.info("@jaguilar87/gaia-ops already installed")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# Create package.json if missing
|
|
86
|
+
package_json_path = project_root / "package.json"
|
|
87
|
+
if not package_json_path.is_file():
|
|
88
|
+
initial_pkg = {
|
|
89
|
+
"name": "my-project",
|
|
90
|
+
"version": "1.0.0",
|
|
91
|
+
"private": True,
|
|
92
|
+
"dependencies": {},
|
|
93
|
+
}
|
|
94
|
+
package_json_path.write_text(json.dumps(initial_pkg, indent=2))
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
subprocess.run(
|
|
98
|
+
["npm", "install", "@jaguilar87/gaia-ops"],
|
|
99
|
+
cwd=str(project_root),
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
timeout=120,
|
|
103
|
+
check=True,
|
|
104
|
+
)
|
|
105
|
+
logger.info("@jaguilar87/gaia-ops installed")
|
|
106
|
+
return True
|
|
107
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as exc:
|
|
108
|
+
logger.error("Failed to install @jaguilar87/gaia-ops: %s", exc)
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def ensure_claude_code(skip_install: bool = False) -> Dict[str, Any]:
|
|
113
|
+
"""Check if Claude Code CLI is installed, optionally install it.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
skip_install: If True, skip installation attempt.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Dict with 'installed' (bool) and 'version' (str or None).
|
|
120
|
+
"""
|
|
121
|
+
# Try to get version
|
|
122
|
+
for cmd in ["claude --version", "claude-code --version"]:
|
|
123
|
+
try:
|
|
124
|
+
result = subprocess.run(
|
|
125
|
+
cmd.split(),
|
|
126
|
+
capture_output=True,
|
|
127
|
+
text=True,
|
|
128
|
+
timeout=10,
|
|
129
|
+
)
|
|
130
|
+
if result.returncode == 0:
|
|
131
|
+
version = result.stdout.strip().split("\n")[0]
|
|
132
|
+
return {"installed": True, "version": version}
|
|
133
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if skip_install:
|
|
137
|
+
logger.warning("Claude Code not installed (--skip-claude-install used)")
|
|
138
|
+
return {"installed": False, "version": None}
|
|
139
|
+
|
|
140
|
+
# Attempt installation
|
|
141
|
+
try:
|
|
142
|
+
subprocess.run(
|
|
143
|
+
["npm", "install", "-g", "@anthropic-ai/claude-code"],
|
|
144
|
+
capture_output=True,
|
|
145
|
+
text=True,
|
|
146
|
+
timeout=120,
|
|
147
|
+
check=True,
|
|
148
|
+
)
|
|
149
|
+
logger.info("Claude Code installed")
|
|
150
|
+
return {"installed": True, "version": "newly installed"}
|
|
151
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as exc:
|
|
152
|
+
logger.warning("Failed to install Claude Code: %s", exc)
|
|
153
|
+
return {"installed": False, "version": None}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_claude_directory(project_root: Path) -> List[str]:
|
|
157
|
+
"""Create .claude/ directory with symlinks to the gaia-ops package.
|
|
158
|
+
|
|
159
|
+
Creates:
|
|
160
|
+
- Symlinks: agents, tools, hooks, commands, templates, config, speckit, skills, CHANGELOG.md
|
|
161
|
+
- Directories: logs, tests, project-context, project-context/workflow-episodic-memory, approvals
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
project_root: Project root directory.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
List of created symlink names (for reporting).
|
|
168
|
+
"""
|
|
169
|
+
claude_dir = project_root / ".claude"
|
|
170
|
+
claude_dir.mkdir(exist_ok=True)
|
|
171
|
+
|
|
172
|
+
# Find the installed package for symlinks
|
|
173
|
+
package_path = _find_installed_package_root(project_root)
|
|
174
|
+
if package_path is None:
|
|
175
|
+
# Fallback: use the plugin root directly (running from source)
|
|
176
|
+
package_path = _find_package_root()
|
|
177
|
+
|
|
178
|
+
# Compute relative path from .claude/ to the package
|
|
179
|
+
try:
|
|
180
|
+
rel_path = os.path.relpath(str(package_path), str(claude_dir))
|
|
181
|
+
except ValueError:
|
|
182
|
+
# On Windows, relpath can fail across drives
|
|
183
|
+
rel_path = str(package_path)
|
|
184
|
+
|
|
185
|
+
# Create symlinks
|
|
186
|
+
symlink_names = [
|
|
187
|
+
"agents", "tools", "hooks", "commands",
|
|
188
|
+
"templates", "config", "speckit", "skills",
|
|
189
|
+
]
|
|
190
|
+
created = []
|
|
191
|
+
|
|
192
|
+
for name in symlink_names:
|
|
193
|
+
link_path = claude_dir / name
|
|
194
|
+
target = os.path.join(rel_path, name)
|
|
195
|
+
|
|
196
|
+
if link_path.exists() or link_path.is_symlink():
|
|
197
|
+
link_path.unlink()
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
os.symlink(target, str(link_path))
|
|
201
|
+
created.append(name)
|
|
202
|
+
except OSError as exc:
|
|
203
|
+
logger.warning("Failed to create symlink %s: %s", name, exc)
|
|
204
|
+
|
|
205
|
+
# CHANGELOG.md symlink
|
|
206
|
+
changelog_link = claude_dir / "CHANGELOG.md"
|
|
207
|
+
if changelog_link.exists() or changelog_link.is_symlink():
|
|
208
|
+
changelog_link.unlink()
|
|
209
|
+
try:
|
|
210
|
+
os.symlink(os.path.join(rel_path, "CHANGELOG.md"), str(changelog_link))
|
|
211
|
+
created.append("CHANGELOG.md")
|
|
212
|
+
except OSError as exc:
|
|
213
|
+
logger.warning("Failed to create CHANGELOG.md symlink: %s", exc)
|
|
214
|
+
|
|
215
|
+
# Create project-specific directories (NOT symlinked)
|
|
216
|
+
for subdir in [
|
|
217
|
+
"logs",
|
|
218
|
+
"tests",
|
|
219
|
+
"project-context",
|
|
220
|
+
os.path.join("project-context", "workflow-episodic-memory"),
|
|
221
|
+
"approvals",
|
|
222
|
+
]:
|
|
223
|
+
(claude_dir / subdir).mkdir(parents=True, exist_ok=True)
|
|
224
|
+
|
|
225
|
+
return created
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def copy_claude_md(project_root: Path) -> bool:
|
|
229
|
+
"""Deprecated — CLAUDE.md is no longer generated from template.
|
|
230
|
+
|
|
231
|
+
Orchestrator identity is now injected by the UserPromptSubmit hook
|
|
232
|
+
via ops_identity.py + deterministic surface routing + on-demand skills (agent-response).
|
|
233
|
+
This avoids two sources of truth.
|
|
234
|
+
|
|
235
|
+
Kept as no-op for backward compatibility with callers.
|
|
236
|
+
"""
|
|
237
|
+
logger.info("copy_claude_md skipped — identity now injected via submit hook")
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def copy_settings_json(project_root: Path) -> bool:
|
|
242
|
+
"""Create a minimal .claude/settings.json only if it does not exist.
|
|
243
|
+
|
|
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.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
project_root: Project root directory.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if file exists (created or already present).
|
|
253
|
+
"""
|
|
254
|
+
dest_path = project_root / ".claude" / "settings.json"
|
|
255
|
+
|
|
256
|
+
if dest_path.is_file():
|
|
257
|
+
logger.info("settings.json already exists — not overwriting")
|
|
258
|
+
return True
|
|
259
|
+
|
|
260
|
+
try:
|
|
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)")
|
|
264
|
+
return True
|
|
265
|
+
except OSError as exc:
|
|
266
|
+
logger.error("Failed to write settings.json: %s", exc)
|
|
267
|
+
return False
|
|
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
|
+
|
|
376
|
+
def install_git_hooks(project_root: Path) -> int:
|
|
377
|
+
"""Install commit-msg git hook to all detected git repositories.
|
|
378
|
+
|
|
379
|
+
Copies git-hooks/commit-msg from the package to .git/hooks/ in all
|
|
380
|
+
repos found in the project root and its immediate subdirectories.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
project_root: Project root directory.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Number of repos where hooks were installed.
|
|
387
|
+
"""
|
|
388
|
+
hook_source = _find_package_root() / "git-hooks" / "commit-msg"
|
|
389
|
+
if not hook_source.is_file():
|
|
390
|
+
logger.warning("git-hooks/commit-msg not found in package, skipping")
|
|
391
|
+
return 0
|
|
392
|
+
|
|
393
|
+
# Find git repos: project root and immediate subdirectories
|
|
394
|
+
candidates = [project_root]
|
|
395
|
+
try:
|
|
396
|
+
for entry in project_root.iterdir():
|
|
397
|
+
if entry.is_dir() and not entry.name.startswith(".") and entry.name != "node_modules":
|
|
398
|
+
candidates.append(entry)
|
|
399
|
+
except OSError:
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
installed = 0
|
|
403
|
+
for dir_path in candidates:
|
|
404
|
+
git_hooks_dir = dir_path / ".git" / "hooks"
|
|
405
|
+
if not git_hooks_dir.is_dir():
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
dest = git_hooks_dir / "commit-msg"
|
|
409
|
+
try:
|
|
410
|
+
shutil.copy2(str(hook_source), str(dest))
|
|
411
|
+
os.chmod(str(dest), 0o755)
|
|
412
|
+
installed += 1
|
|
413
|
+
except OSError as exc:
|
|
414
|
+
logger.warning("Failed to install hook in %s: %s", dir_path, exc)
|
|
415
|
+
|
|
416
|
+
return installed
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def generate_governance(project_root: Path, config: Dict[str, Any]) -> bool:
|
|
420
|
+
"""Generate governance.md from template with config interpolation.
|
|
421
|
+
|
|
422
|
+
Only creates governance.md if it does not already exist (it is managed
|
|
423
|
+
by speckit.init after initial creation).
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
project_root: Project root directory.
|
|
427
|
+
config: Configuration dict with keys: cloud_provider, region,
|
|
428
|
+
project_id, cluster_name, gitops, terraform.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
True if governance.md was created or already exists.
|
|
432
|
+
"""
|
|
433
|
+
speckit_root = config.get("speckit_root", ".claude/project-context/speckit-project-specs")
|
|
434
|
+
|
|
435
|
+
if os.path.isabs(speckit_root):
|
|
436
|
+
resolved_root = Path(speckit_root)
|
|
437
|
+
else:
|
|
438
|
+
resolved_root = project_root / speckit_root
|
|
439
|
+
|
|
440
|
+
resolved_root.mkdir(parents=True, exist_ok=True)
|
|
441
|
+
dest_path = resolved_root / "governance.md"
|
|
442
|
+
|
|
443
|
+
if dest_path.is_file():
|
|
444
|
+
logger.info("governance.md already exists -- skipping (managed by speckit.init)")
|
|
445
|
+
return True
|
|
446
|
+
|
|
447
|
+
template_path = _get_template_path("governance.template.md")
|
|
448
|
+
if not template_path.is_file():
|
|
449
|
+
logger.warning("governance.template.md not found -- skipping")
|
|
450
|
+
return False
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
template = template_path.read_text()
|
|
454
|
+
|
|
455
|
+
cloud_provider = config.get("cloud_provider", "gcp")
|
|
456
|
+
k8s_platform = {
|
|
457
|
+
"aws": "EKS",
|
|
458
|
+
"gcp": "GKE",
|
|
459
|
+
}.get(cloud_provider, "Kubernetes")
|
|
460
|
+
|
|
461
|
+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
462
|
+
|
|
463
|
+
interpolated = (
|
|
464
|
+
template
|
|
465
|
+
.replace("[CLOUD_PROVIDER]", (cloud_provider or "gcp").upper())
|
|
466
|
+
.replace("[PRIMARY_REGION]", config.get("region", "") or "N/A")
|
|
467
|
+
.replace("[PROJECT_ID]", config.get("project_id", "") or "N/A")
|
|
468
|
+
.replace("[CLUSTER_NAME]", config.get("cluster_name", "") or "N/A")
|
|
469
|
+
.replace("[GITOPS_PATH]", config.get("gitops", "") or "N/A")
|
|
470
|
+
.replace("[TERRAFORM_PATH]", config.get("terraform", "") or "N/A")
|
|
471
|
+
.replace("[POSTGRES_INSTANCE]", "N/A")
|
|
472
|
+
.replace("[CONTAINER_REGISTRY]", "N/A")
|
|
473
|
+
.replace("[K8S_PLATFORM]", k8s_platform)
|
|
474
|
+
.replace("[DATE]", today)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
dest_path.write_text(interpolated)
|
|
478
|
+
logger.info("governance.md created at %s", dest_path)
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
except OSError as exc:
|
|
482
|
+
logger.error("Failed to create governance.md: %s", exc)
|
|
483
|
+
return False
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def generate_project_context(
|
|
487
|
+
project_root: Path,
|
|
488
|
+
config: Dict[str, Any],
|
|
489
|
+
scan_context: Optional[Dict[str, Any]] = None,
|
|
490
|
+
) -> bool:
|
|
491
|
+
"""Generate or merge project-context.json from config and scan results.
|
|
492
|
+
|
|
493
|
+
For fresh projects (no existing file): writes a full generated context
|
|
494
|
+
that includes scan results if available.
|
|
495
|
+
|
|
496
|
+
For existing projects: merges metadata and paths from scan, preserves
|
|
497
|
+
agent-enriched sections.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
project_root: Project root directory.
|
|
501
|
+
config: Configuration dict with detected/user-provided values.
|
|
502
|
+
scan_context: Full context from scan orchestrator (if available).
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
True if file was written successfully.
|
|
506
|
+
"""
|
|
507
|
+
dest_path = (
|
|
508
|
+
project_root / ".claude" / "project-context" / "project-context.json"
|
|
509
|
+
)
|
|
510
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
511
|
+
|
|
512
|
+
now_iso = datetime.now(timezone.utc).isoformat()
|
|
513
|
+
|
|
514
|
+
# If we have scan_context from the orchestrator, it already has the
|
|
515
|
+
# correct v2 schema structure. Use it as the base.
|
|
516
|
+
if scan_context:
|
|
517
|
+
# Enrich scan context with user-provided config values
|
|
518
|
+
context = _enrich_scan_context(scan_context, config, now_iso, project_root)
|
|
519
|
+
else:
|
|
520
|
+
# Build a minimal context from config alone
|
|
521
|
+
context = _build_minimal_context(config, now_iso, project_root)
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
if not dest_path.is_file():
|
|
525
|
+
# First-time install: write the full context
|
|
526
|
+
dest_path.write_text(json.dumps(context, indent=2) + "\n")
|
|
527
|
+
logger.info("project-context.json generated")
|
|
528
|
+
return True
|
|
529
|
+
|
|
530
|
+
# File exists -- merge
|
|
531
|
+
try:
|
|
532
|
+
existing = json.loads(dest_path.read_text())
|
|
533
|
+
except (json.JSONDecodeError, OSError):
|
|
534
|
+
dest_path.write_text(json.dumps(context, indent=2) + "\n")
|
|
535
|
+
logger.info("project-context.json regenerated (previous was invalid)")
|
|
536
|
+
return True
|
|
537
|
+
|
|
538
|
+
merged = _merge_project_context(existing, context)
|
|
539
|
+
dest_path.write_text(json.dumps(merged, indent=2) + "\n")
|
|
540
|
+
logger.info("project-context.json updated (metadata+paths synced, sections preserved)")
|
|
541
|
+
return True
|
|
542
|
+
|
|
543
|
+
except OSError as exc:
|
|
544
|
+
logger.error("Failed to write project-context.json: %s", exc)
|
|
545
|
+
return False
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _enrich_scan_context(
|
|
549
|
+
scan_context: Dict[str, Any],
|
|
550
|
+
config: Dict[str, Any],
|
|
551
|
+
now_iso: str,
|
|
552
|
+
project_root: Path,
|
|
553
|
+
) -> Dict[str, Any]:
|
|
554
|
+
"""Enrich scan context with user-provided config values."""
|
|
555
|
+
import copy
|
|
556
|
+
context = copy.deepcopy(scan_context)
|
|
557
|
+
|
|
558
|
+
# Ensure metadata exists
|
|
559
|
+
meta = context.setdefault("metadata", {})
|
|
560
|
+
meta["version"] = meta.get("version", "2.0")
|
|
561
|
+
meta["last_updated"] = now_iso
|
|
562
|
+
meta["created_by"] = "gaia-scan"
|
|
563
|
+
|
|
564
|
+
# Update infrastructure.paths from config (user overrides trump scan)
|
|
565
|
+
sections = context.setdefault("sections", {})
|
|
566
|
+
infra = sections.setdefault("infrastructure", {})
|
|
567
|
+
infra_paths = infra.setdefault("paths", {})
|
|
568
|
+
if config.get("gitops"):
|
|
569
|
+
infra_paths["gitops"] = config["gitops"]
|
|
570
|
+
if config.get("terraform"):
|
|
571
|
+
infra_paths["terraform"] = config["terraform"]
|
|
572
|
+
if config.get("app_services"):
|
|
573
|
+
infra_paths["app_services"] = config["app_services"]
|
|
574
|
+
# Remove top-level paths if present (single source: infrastructure.paths)
|
|
575
|
+
context.pop("paths", None)
|
|
576
|
+
|
|
577
|
+
# Ensure operational_guidelines has speckit_root
|
|
578
|
+
sections = context.setdefault("sections", {})
|
|
579
|
+
op_guide = sections.setdefault("operational_guidelines", {})
|
|
580
|
+
if "speckit_root" not in op_guide:
|
|
581
|
+
op_guide["speckit_root"] = config.get(
|
|
582
|
+
"speckit_root",
|
|
583
|
+
".claude/project-context/speckit-project-specs",
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Enrich sections from contract file
|
|
587
|
+
_enrich_from_contracts(context, config, project_root)
|
|
588
|
+
|
|
589
|
+
return context
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _build_minimal_context(
|
|
593
|
+
config: Dict[str, Any],
|
|
594
|
+
now_iso: str,
|
|
595
|
+
project_root: Path,
|
|
596
|
+
) -> Dict[str, Any]:
|
|
597
|
+
"""Build a minimal project-context.json from config when no scan data available."""
|
|
598
|
+
cloud_provider = config.get("cloud_provider", "gcp")
|
|
599
|
+
project_name = config.get("project_name", project_root.name)
|
|
600
|
+
|
|
601
|
+
metadata = {
|
|
602
|
+
"version": "2.0",
|
|
603
|
+
"last_updated": now_iso,
|
|
604
|
+
"project_name": project_name,
|
|
605
|
+
"project_root": ".",
|
|
606
|
+
"created_by": "gaia-scan",
|
|
607
|
+
"cloud_provider": cloud_provider,
|
|
608
|
+
"environment": "non-prod",
|
|
609
|
+
"primary_region": config.get("region", ""),
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if cloud_provider in ("gcp", "multi-cloud") and config.get("project_id"):
|
|
613
|
+
metadata["project_id"] = config["project_id"]
|
|
614
|
+
if cloud_provider in ("aws", "multi-cloud") and config.get("project_id"):
|
|
615
|
+
metadata["aws_account"] = config["project_id"]
|
|
616
|
+
|
|
617
|
+
cloud_entry: Dict[str, Any] = {
|
|
618
|
+
"name": cloud_provider,
|
|
619
|
+
"region": config.get("region", ""),
|
|
620
|
+
}
|
|
621
|
+
if cloud_provider in ("gcp", "multi-cloud") and config.get("project_id"):
|
|
622
|
+
cloud_entry["project_id"] = config["project_id"]
|
|
623
|
+
if cloud_provider in ("aws", "multi-cloud") and config.get("project_id"):
|
|
624
|
+
cloud_entry["account_id"] = config["project_id"]
|
|
625
|
+
|
|
626
|
+
speckit_root = config.get("speckit_root", ".claude/project-context/speckit-project-specs")
|
|
627
|
+
|
|
628
|
+
# Build paths dict, filtering out empty strings
|
|
629
|
+
infra_paths: Dict[str, str] = {}
|
|
630
|
+
for key in ("gitops", "terraform", "app_services"):
|
|
631
|
+
val = config.get(key, "")
|
|
632
|
+
if val:
|
|
633
|
+
infra_paths[key] = val
|
|
634
|
+
|
|
635
|
+
context = {
|
|
636
|
+
"metadata": metadata,
|
|
637
|
+
"sections": {
|
|
638
|
+
"project_identity": {
|
|
639
|
+
"name": project_name,
|
|
640
|
+
"type": "application",
|
|
641
|
+
},
|
|
642
|
+
"stack": {"languages": [], "frameworks": [], "build_tools": []},
|
|
643
|
+
"git": {
|
|
644
|
+
"platform": config.get("git_platform"),
|
|
645
|
+
"remotes": [],
|
|
646
|
+
"default_branch": "main",
|
|
647
|
+
},
|
|
648
|
+
"environment": {"runtimes": [], "os": {}},
|
|
649
|
+
"infrastructure": {
|
|
650
|
+
"cloud_providers": [cloud_entry],
|
|
651
|
+
"ci_cd": (
|
|
652
|
+
[{"platform": config["ci_platform"]}]
|
|
653
|
+
if config.get("ci_platform")
|
|
654
|
+
else []
|
|
655
|
+
),
|
|
656
|
+
"paths": infra_paths,
|
|
657
|
+
},
|
|
658
|
+
"operational_guidelines": {
|
|
659
|
+
"speckit_root": speckit_root,
|
|
660
|
+
"commit_standards": {
|
|
661
|
+
"format": "conventional_commits",
|
|
662
|
+
"validation_required": True,
|
|
663
|
+
"config_path": ".claude/config/git_standards.json",
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
_enrich_from_contracts(context, config, project_root)
|
|
670
|
+
return context
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _enrich_from_contracts(
|
|
674
|
+
context: Dict[str, Any],
|
|
675
|
+
config: Dict[str, Any],
|
|
676
|
+
project_root: Path,
|
|
677
|
+
) -> None:
|
|
678
|
+
"""Enrich context sections from contract file (progressive context enrichment).
|
|
679
|
+
|
|
680
|
+
Only creates empty {} placeholders for scanner-owned sections that agents
|
|
681
|
+
need to read. Agent-enriched and mixed sections are NOT pre-created --
|
|
682
|
+
they should only exist when populated with actual data. The exception is
|
|
683
|
+
architecture_overview, which always exists (even empty) because all agent
|
|
684
|
+
contracts reference it.
|
|
685
|
+
"""
|
|
686
|
+
try:
|
|
687
|
+
cloud_provider = config.get("cloud_provider", "gcp")
|
|
688
|
+
provider = "gcp" if cloud_provider == "multi-cloud" else cloud_provider
|
|
689
|
+
contract_path = _find_package_root() / "config" / f"context-contracts.{provider}.json"
|
|
690
|
+
|
|
691
|
+
if not contract_path.is_file():
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
contracts = json.loads(contract_path.read_text())
|
|
695
|
+
contract_sections: set = set()
|
|
696
|
+
for agent in (contracts.get("agents") or {}).values():
|
|
697
|
+
for s in agent.get("read", []):
|
|
698
|
+
contract_sections.add(s)
|
|
699
|
+
for s in agent.get("write", []):
|
|
700
|
+
contract_sections.add(s)
|
|
701
|
+
|
|
702
|
+
# Sections that should NOT be pre-created as empty {}.
|
|
703
|
+
# They only exist when an agent or scanner populates them with data.
|
|
704
|
+
# architecture_overview is the exception -- always present.
|
|
705
|
+
from tools.scan.merge import AGENT_ENRICHED_SECTIONS, MIXED_SECTION_SCANNER_FIELDS
|
|
706
|
+
skip_empty = (
|
|
707
|
+
AGENT_ENRICHED_SECTIONS
|
|
708
|
+
| frozenset(MIXED_SECTION_SCANNER_FIELDS.keys())
|
|
709
|
+
) - {"architecture_overview"}
|
|
710
|
+
|
|
711
|
+
sections = context.setdefault("sections", {})
|
|
712
|
+
for section in contract_sections:
|
|
713
|
+
if section not in sections:
|
|
714
|
+
if section in skip_empty:
|
|
715
|
+
continue
|
|
716
|
+
sections[section] = {}
|
|
717
|
+
|
|
718
|
+
except (json.JSONDecodeError, OSError):
|
|
719
|
+
pass
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _merge_project_context(
|
|
723
|
+
existing: Dict[str, Any],
|
|
724
|
+
new_context: Dict[str, Any],
|
|
725
|
+
) -> Dict[str, Any]:
|
|
726
|
+
"""Merge new context into existing, preserving agent-enriched sections.
|
|
727
|
+
|
|
728
|
+
Strategy:
|
|
729
|
+
- metadata: field-by-field replace from new
|
|
730
|
+
- paths: field-by-field replace from new
|
|
731
|
+
- sections: preserve existing content; add new sections if absent
|
|
732
|
+
"""
|
|
733
|
+
import copy
|
|
734
|
+
|
|
735
|
+
merged = {
|
|
736
|
+
"metadata": {
|
|
737
|
+
**(existing.get("metadata") or {}),
|
|
738
|
+
**(new_context.get("metadata") or {}),
|
|
739
|
+
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
740
|
+
},
|
|
741
|
+
"sections": {
|
|
742
|
+
# Start from new context sections as schema base,
|
|
743
|
+
# then override with existing sections that have content
|
|
744
|
+
**(new_context.get("sections") or {}),
|
|
745
|
+
**{
|
|
746
|
+
k: v
|
|
747
|
+
for k, v in (existing.get("sections") or {}).items()
|
|
748
|
+
if v is not None and isinstance(v, dict) and len(v) > 0
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return merged
|