@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,518 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ContextWriter module for gaia-ops progressive context enrichment.
|
|
3
|
+
|
|
4
|
+
Parses CONTEXT_UPDATE blocks from agent output, validates write permissions
|
|
5
|
+
against contracts, and applies deep-merge updates to project-context.json.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
- parse_context_update(agent_output) -> Optional[dict]
|
|
9
|
+
- validate_permissions(update, agent_type, contracts) -> (dict, list)
|
|
10
|
+
- apply_update(context_path, update, agent_type) -> dict
|
|
11
|
+
- load_contracts(provider, config_dir) -> dict
|
|
12
|
+
- process_agent_output(agent_output, task_info) -> dict
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Dynamic import: deep_merge from tools/context/deep_merge.py
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
def _import_deep_merge():
|
|
28
|
+
"""Import deep_merge function from tools/context/deep_merge.py."""
|
|
29
|
+
import importlib.util
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
# Resolve paths relative to this file:
|
|
33
|
+
# this file: hooks/modules/context/context_writer.py
|
|
34
|
+
# deep_merge: tools/context/deep_merge.py
|
|
35
|
+
hooks_dir = Path(__file__).resolve().parents[2] # hooks/
|
|
36
|
+
repo_root = hooks_dir.parent # repo root
|
|
37
|
+
deep_merge_path = repo_root / "tools" / "context" / "deep_merge.py"
|
|
38
|
+
|
|
39
|
+
if not deep_merge_path.exists():
|
|
40
|
+
raise ImportError(f"deep_merge.py not found at {deep_merge_path}")
|
|
41
|
+
|
|
42
|
+
spec = importlib.util.spec_from_file_location("deep_merge", str(deep_merge_path))
|
|
43
|
+
module = importlib.util.module_from_spec(spec)
|
|
44
|
+
sys.modules["deep_merge"] = module
|
|
45
|
+
spec.loader.exec_module(module)
|
|
46
|
+
return module.deep_merge
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_deep_merge = _import_deep_merge()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# LEGACY_AGENT_CONTRACTS (fallback when no contracts file exists)
|
|
54
|
+
# In legacy mode, write permissions = read permissions.
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
LEGACY_AGENT_CONTRACTS: Dict[str, List[str]] = {
|
|
57
|
+
"terraform-architect": [
|
|
58
|
+
"project_identity",
|
|
59
|
+
"stack",
|
|
60
|
+
"git",
|
|
61
|
+
"environment",
|
|
62
|
+
"infrastructure",
|
|
63
|
+
"terraform_infrastructure",
|
|
64
|
+
"infrastructure_topology",
|
|
65
|
+
"operational_guidelines",
|
|
66
|
+
],
|
|
67
|
+
"gitops-operator": [
|
|
68
|
+
"project_identity",
|
|
69
|
+
"stack",
|
|
70
|
+
"git",
|
|
71
|
+
"environment",
|
|
72
|
+
"infrastructure",
|
|
73
|
+
"gitops_configuration",
|
|
74
|
+
"infrastructure_topology",
|
|
75
|
+
"cluster_details",
|
|
76
|
+
"operational_guidelines",
|
|
77
|
+
],
|
|
78
|
+
"cloud-troubleshooter": [
|
|
79
|
+
"project_identity",
|
|
80
|
+
"stack",
|
|
81
|
+
"git",
|
|
82
|
+
"environment",
|
|
83
|
+
"infrastructure",
|
|
84
|
+
"infrastructure_topology",
|
|
85
|
+
"terraform_infrastructure",
|
|
86
|
+
"gitops_configuration",
|
|
87
|
+
"application_services",
|
|
88
|
+
"monitoring_observability",
|
|
89
|
+
"cluster_details",
|
|
90
|
+
],
|
|
91
|
+
"devops-developer": [
|
|
92
|
+
"project_identity",
|
|
93
|
+
"stack",
|
|
94
|
+
"git",
|
|
95
|
+
"environment",
|
|
96
|
+
"infrastructure",
|
|
97
|
+
"application_services",
|
|
98
|
+
"operational_guidelines",
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Module-level cache for load_contracts
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
_contracts_cache: Dict[str, dict] = {}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ============================================================================
|
|
110
|
+
# 1. parse_context_update
|
|
111
|
+
# ============================================================================
|
|
112
|
+
|
|
113
|
+
def parse_context_update(agent_output: str) -> Optional[dict]:
|
|
114
|
+
"""Extract and parse a CONTEXT_UPDATE block from agent output.
|
|
115
|
+
|
|
116
|
+
Searches for the ``CONTEXT_UPDATE:`` marker (case-sensitive, on its own
|
|
117
|
+
line), extracts the JSON that follows until end-of-output or the next
|
|
118
|
+
known marker, and returns the parsed dict.
|
|
119
|
+
|
|
120
|
+
Returns None when:
|
|
121
|
+
- No marker is found
|
|
122
|
+
- The JSON is malformed
|
|
123
|
+
- The parsed value is not a dict
|
|
124
|
+
"""
|
|
125
|
+
# Find the CONTEXT_UPDATE: marker on its own line
|
|
126
|
+
marker = "CONTEXT_UPDATE:"
|
|
127
|
+
lines = agent_output.split("\n")
|
|
128
|
+
|
|
129
|
+
marker_idx = None
|
|
130
|
+
for i, line in enumerate(lines):
|
|
131
|
+
if line.strip() == marker:
|
|
132
|
+
marker_idx = i
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
if marker_idx is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
# Collect all text after the marker
|
|
139
|
+
remaining = "\n".join(lines[marker_idx + 1:]).strip()
|
|
140
|
+
|
|
141
|
+
if not remaining:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# Strip markdown code fences — LLMs reading SKILL.md documentation
|
|
145
|
+
# often wrap the JSON in ```json ... ``` or ``` ... ``` blocks.
|
|
146
|
+
if remaining.startswith("```"):
|
|
147
|
+
fence_lines = remaining.split("\n")
|
|
148
|
+
# Remove opening fence (```json, ```JSON, ```, etc.)
|
|
149
|
+
fence_lines.pop(0)
|
|
150
|
+
# Remove closing fence if present
|
|
151
|
+
for i in range(len(fence_lines) - 1, -1, -1):
|
|
152
|
+
if fence_lines[i].strip() == "```":
|
|
153
|
+
fence_lines.pop(i)
|
|
154
|
+
break
|
|
155
|
+
remaining = "\n".join(fence_lines).strip()
|
|
156
|
+
|
|
157
|
+
if not remaining:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
# Use raw_decode to extract the first complete JSON value, ignoring
|
|
161
|
+
# any trailing text (summaries, AGENT_STATUS blocks, etc.)
|
|
162
|
+
decoder = json.JSONDecoder()
|
|
163
|
+
try:
|
|
164
|
+
parsed, _ = decoder.raw_decode(remaining)
|
|
165
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
166
|
+
logger.warning("Malformed JSON in CONTEXT_UPDATE block: %s", exc)
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
if not isinstance(parsed, dict):
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
return parsed
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ============================================================================
|
|
176
|
+
# 2. validate_permissions
|
|
177
|
+
# ============================================================================
|
|
178
|
+
|
|
179
|
+
def validate_permissions(
|
|
180
|
+
update: dict,
|
|
181
|
+
agent_type: str,
|
|
182
|
+
contracts: dict,
|
|
183
|
+
) -> tuple:
|
|
184
|
+
"""Validate which sections the agent is allowed to write.
|
|
185
|
+
|
|
186
|
+
Returns ``(allowed_updates, rejected_sections)`` where:
|
|
187
|
+
- ``allowed_updates``: dict with only permitted sections
|
|
188
|
+
- ``rejected_sections``: list of section names that were rejected
|
|
189
|
+
"""
|
|
190
|
+
allowed: dict = {}
|
|
191
|
+
rejected: List[str] = []
|
|
192
|
+
|
|
193
|
+
# Determine writable sections for this agent
|
|
194
|
+
agents_map = contracts.get("agents", {})
|
|
195
|
+
|
|
196
|
+
if agent_type in agents_map:
|
|
197
|
+
# Use explicit write list from contracts file
|
|
198
|
+
writable = set(agents_map[agent_type].get("write", []))
|
|
199
|
+
elif agent_type in LEGACY_AGENT_CONTRACTS:
|
|
200
|
+
# Fallback: in legacy mode, write = read
|
|
201
|
+
writable = set(LEGACY_AGENT_CONTRACTS[agent_type])
|
|
202
|
+
else:
|
|
203
|
+
# Unknown agent: no permissions
|
|
204
|
+
writable = set()
|
|
205
|
+
|
|
206
|
+
for section, data in update.items():
|
|
207
|
+
if section in writable:
|
|
208
|
+
allowed[section] = data
|
|
209
|
+
else:
|
|
210
|
+
rejected.append(section)
|
|
211
|
+
|
|
212
|
+
return allowed, rejected
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ============================================================================
|
|
216
|
+
# 3. apply_update
|
|
217
|
+
# ============================================================================
|
|
218
|
+
|
|
219
|
+
def apply_update(
|
|
220
|
+
context_path: Path,
|
|
221
|
+
update: dict,
|
|
222
|
+
agent_type: str,
|
|
223
|
+
) -> dict:
|
|
224
|
+
"""Apply a validated update to project-context.json using deep merge.
|
|
225
|
+
|
|
226
|
+
Performs an atomic write (write to .tmp, then rename) and appends an
|
|
227
|
+
audit entry to ``context-audit.jsonl`` in the same directory.
|
|
228
|
+
|
|
229
|
+
Returns an audit entry dict. Never raises on I/O errors.
|
|
230
|
+
"""
|
|
231
|
+
context_path = Path(context_path)
|
|
232
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
233
|
+
sections_updated = list(update.keys())
|
|
234
|
+
|
|
235
|
+
audit_entry = {
|
|
236
|
+
"timestamp": timestamp,
|
|
237
|
+
"agent": agent_type,
|
|
238
|
+
"sections_updated": sections_updated,
|
|
239
|
+
"changes": {},
|
|
240
|
+
"success": False,
|
|
241
|
+
"error": None,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
# Read current context
|
|
246
|
+
current = json.loads(context_path.read_text())
|
|
247
|
+
|
|
248
|
+
# Deep merge each section
|
|
249
|
+
all_changes = {}
|
|
250
|
+
for section, section_data in update.items():
|
|
251
|
+
current_section = current.get("sections", {}).get(section, {})
|
|
252
|
+
merged_section, diff = _deep_merge(current_section, section_data)
|
|
253
|
+
|
|
254
|
+
# Ensure sections dict exists
|
|
255
|
+
if "sections" not in current:
|
|
256
|
+
current["sections"] = {}
|
|
257
|
+
current["sections"][section] = merged_section
|
|
258
|
+
|
|
259
|
+
if diff:
|
|
260
|
+
all_changes[section] = diff
|
|
261
|
+
|
|
262
|
+
# Update metadata timestamp
|
|
263
|
+
if "metadata" not in current:
|
|
264
|
+
current["metadata"] = {}
|
|
265
|
+
current["metadata"]["last_updated"] = timestamp
|
|
266
|
+
|
|
267
|
+
# Atomic write: write to .tmp, then rename
|
|
268
|
+
tmp_path = context_path.with_suffix(".tmp")
|
|
269
|
+
tmp_path.write_text(json.dumps(current, indent=2))
|
|
270
|
+
tmp_path.rename(context_path)
|
|
271
|
+
|
|
272
|
+
audit_entry["changes"] = all_changes
|
|
273
|
+
audit_entry["success"] = True
|
|
274
|
+
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
logger.error("Failed to apply context update: %s", exc)
|
|
277
|
+
audit_entry["error"] = str(exc)
|
|
278
|
+
audit_entry["success"] = False
|
|
279
|
+
return audit_entry
|
|
280
|
+
|
|
281
|
+
# Append audit entry to context-audit.jsonl
|
|
282
|
+
try:
|
|
283
|
+
audit_file = context_path.parent / "context-audit.jsonl"
|
|
284
|
+
with open(audit_file, "a") as f:
|
|
285
|
+
f.write(json.dumps(audit_entry) + "\n")
|
|
286
|
+
except Exception as exc:
|
|
287
|
+
logger.warning("Failed to write audit entry: %s", exc)
|
|
288
|
+
# Audit write failure doesn't affect the main result
|
|
289
|
+
|
|
290
|
+
return audit_entry
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ============================================================================
|
|
294
|
+
# 4. load_contracts
|
|
295
|
+
# ============================================================================
|
|
296
|
+
|
|
297
|
+
def load_contracts(provider: str, config_dir: Path) -> dict:
|
|
298
|
+
"""Load agent contracts using the base+cloud merge strategy, with caching.
|
|
299
|
+
|
|
300
|
+
Strategy (in priority order):
|
|
301
|
+
1. Load base contracts from context-contracts.json (cloud-agnostic)
|
|
302
|
+
2. Merge cloud overrides from cloud/{provider}.json (extend read/write lists)
|
|
303
|
+
3. Fallback: try legacy context-contracts.{provider}.json
|
|
304
|
+
4. Final fallback: LEGACY_AGENT_CONTRACTS hardcoded dict (write = read)
|
|
305
|
+
|
|
306
|
+
Results are cached by provider string.
|
|
307
|
+
"""
|
|
308
|
+
config_dir = Path(config_dir)
|
|
309
|
+
|
|
310
|
+
# Check cache first
|
|
311
|
+
if provider in _contracts_cache:
|
|
312
|
+
return _contracts_cache[provider]
|
|
313
|
+
|
|
314
|
+
result = _merge_base_and_cloud(provider, config_dir)
|
|
315
|
+
_contracts_cache[provider] = result
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _merge_base_and_cloud(provider: str, config_dir: Path) -> dict:
|
|
320
|
+
"""Load and merge base + cloud/{provider}.json contracts.
|
|
321
|
+
|
|
322
|
+
Returns a merged contracts dict with 'agents' keyed by agent name,
|
|
323
|
+
each containing 'read' and 'write' lists.
|
|
324
|
+
"""
|
|
325
|
+
base_file = config_dir / "context-contracts.json"
|
|
326
|
+
cloud_file = config_dir / "cloud" / f"{provider}.json"
|
|
327
|
+
|
|
328
|
+
# Step 1: Load base contracts
|
|
329
|
+
base_contracts = None
|
|
330
|
+
if base_file.exists():
|
|
331
|
+
try:
|
|
332
|
+
base_contracts = json.loads(base_file.read_text())
|
|
333
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
334
|
+
logger.warning("Failed to load base contracts from %s: %s", base_file, exc)
|
|
335
|
+
|
|
336
|
+
# Step 2: Final fallback to hardcoded LEGACY_AGENT_CONTRACTS
|
|
337
|
+
if base_contracts is None:
|
|
338
|
+
logger.warning("No contract files found in %s, using hardcoded legacy contracts", config_dir)
|
|
339
|
+
return {
|
|
340
|
+
"version": "legacy",
|
|
341
|
+
"provider": provider,
|
|
342
|
+
"agents": {
|
|
343
|
+
agent: {"read": sections, "write": list(sections)}
|
|
344
|
+
for agent, sections in LEGACY_AGENT_CONTRACTS.items()
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Step 4: Merge cloud-specific overrides
|
|
349
|
+
if cloud_file.exists():
|
|
350
|
+
try:
|
|
351
|
+
cloud_overrides = json.loads(cloud_file.read_text())
|
|
352
|
+
for agent_name, agent_overrides in cloud_overrides.get("agents", {}).items():
|
|
353
|
+
if agent_name in base_contracts.get("agents", {}):
|
|
354
|
+
existing_read = base_contracts["agents"][agent_name].get("read", [])
|
|
355
|
+
existing_write = base_contracts["agents"][agent_name].get("write", [])
|
|
356
|
+
extra_read = [s for s in agent_overrides.get("read", []) if s not in existing_read]
|
|
357
|
+
extra_write = [s for s in agent_overrides.get("write", []) if s not in existing_write]
|
|
358
|
+
base_contracts["agents"][agent_name]["read"] = existing_read + extra_read
|
|
359
|
+
base_contracts["agents"][agent_name]["write"] = existing_write + extra_write
|
|
360
|
+
else:
|
|
361
|
+
base_contracts["agents"][agent_name] = agent_overrides
|
|
362
|
+
logger.info("Merged %s cloud overrides from %s", provider.upper(), cloud_file)
|
|
363
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
364
|
+
logger.warning("Failed to load cloud overrides from %s: %s — skipping", cloud_file, exc)
|
|
365
|
+
|
|
366
|
+
return base_contracts
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ============================================================================
|
|
370
|
+
# 5. process_agent_output
|
|
371
|
+
# ============================================================================
|
|
372
|
+
|
|
373
|
+
def process_agent_output(agent_output: str, task_info: dict) -> dict:
|
|
374
|
+
"""Orchestrate the full context-update flow.
|
|
375
|
+
|
|
376
|
+
Steps: parse -> detect provider -> load contracts -> validate -> apply.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
agent_output : str
|
|
381
|
+
Full agent output string.
|
|
382
|
+
task_info : dict
|
|
383
|
+
Must contain: ``agent_type``, ``context_path``, ``config_dir``.
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
-------
|
|
387
|
+
dict
|
|
388
|
+
``{updated, sections/sections_updated, rejected, error}``
|
|
389
|
+
"""
|
|
390
|
+
result = {
|
|
391
|
+
"updated": False,
|
|
392
|
+
"sections_updated": [],
|
|
393
|
+
"rejected": [],
|
|
394
|
+
"error": None,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# 1. Parse CONTEXT_UPDATE
|
|
398
|
+
update = parse_context_update(agent_output)
|
|
399
|
+
if update is None:
|
|
400
|
+
return result
|
|
401
|
+
|
|
402
|
+
agent_type = task_info.get("agent_type", "unknown")
|
|
403
|
+
context_path = Path(task_info.get("context_path", ""))
|
|
404
|
+
config_dir = Path(task_info.get("config_dir", ""))
|
|
405
|
+
|
|
406
|
+
# 2. Detect cloud provider from existing context metadata
|
|
407
|
+
provider = "gcp" # default
|
|
408
|
+
try:
|
|
409
|
+
if context_path.exists():
|
|
410
|
+
ctx = json.loads(context_path.read_text())
|
|
411
|
+
provider = ctx.get("metadata", {}).get("cloud_provider", "gcp").lower()
|
|
412
|
+
except Exception:
|
|
413
|
+
pass
|
|
414
|
+
|
|
415
|
+
# 3. Load contracts
|
|
416
|
+
# Clear cache to avoid cross-test pollution (keyed by provider+config_dir)
|
|
417
|
+
cache_key = f"{provider}:{config_dir}"
|
|
418
|
+
contracts = _load_contracts_with_dir(provider, config_dir)
|
|
419
|
+
|
|
420
|
+
# 4. Validate permissions
|
|
421
|
+
allowed, rejected = validate_permissions(update, agent_type, contracts)
|
|
422
|
+
result["rejected"] = rejected
|
|
423
|
+
|
|
424
|
+
# 5. If nothing allowed, return early
|
|
425
|
+
if not allowed:
|
|
426
|
+
return result
|
|
427
|
+
|
|
428
|
+
# 6. Apply update
|
|
429
|
+
audit = apply_update(context_path, allowed, agent_type)
|
|
430
|
+
|
|
431
|
+
if audit.get("success"):
|
|
432
|
+
result["updated"] = True
|
|
433
|
+
result["sections_updated"] = list(allowed.keys())
|
|
434
|
+
else:
|
|
435
|
+
result["error"] = audit.get("error")
|
|
436
|
+
|
|
437
|
+
return result
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _load_contracts_with_dir(provider: str, config_dir: Path) -> dict:
|
|
441
|
+
"""Load contracts, bypassing the module-level cache for process_agent_output.
|
|
442
|
+
|
|
443
|
+
This ensures each call with a different config_dir gets fresh results
|
|
444
|
+
while still allowing load_contracts to cache for repeated calls with
|
|
445
|
+
the same provider. Uses the same base+cloud merge strategy as load_contracts.
|
|
446
|
+
"""
|
|
447
|
+
return _merge_base_and_cloud(provider, Path(config_dir))
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# ============================================================================
|
|
451
|
+
# 6. process_context_updates (thin wrapper for subagent_stop integration)
|
|
452
|
+
# ============================================================================
|
|
453
|
+
|
|
454
|
+
def process_context_updates(
|
|
455
|
+
agent_output: str,
|
|
456
|
+
task_info: dict,
|
|
457
|
+
find_claude_dir_fn=None,
|
|
458
|
+
) -> Optional[dict]:
|
|
459
|
+
"""
|
|
460
|
+
Process CONTEXT_UPDATE blocks from agent output via context_writer.
|
|
461
|
+
|
|
462
|
+
Loads project-context.json, resolves config_dir from .claude, and calls
|
|
463
|
+
process_agent_output() to apply progressive enrichment.
|
|
464
|
+
|
|
465
|
+
This function MUST NOT break the existing hook flow -- all errors are caught
|
|
466
|
+
and logged, returning None on failure.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
agent_output: Complete output from agent execution
|
|
470
|
+
task_info: Task metadata (agent, description, task_id)
|
|
471
|
+
find_claude_dir_fn: Callable that returns the .claude Path. Defaults to
|
|
472
|
+
modules.core.paths.find_claude_dir if not provided.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Result dict from process_agent_output, or None on error
|
|
476
|
+
"""
|
|
477
|
+
try:
|
|
478
|
+
if find_claude_dir_fn is None:
|
|
479
|
+
from ..core.paths import find_claude_dir
|
|
480
|
+
find_claude_dir_fn = find_claude_dir
|
|
481
|
+
|
|
482
|
+
# Find project-context.json via find_claude_dir
|
|
483
|
+
claude_dir = find_claude_dir_fn()
|
|
484
|
+
context_path = claude_dir / "project-context" / "project-context.json"
|
|
485
|
+
|
|
486
|
+
if not context_path.exists():
|
|
487
|
+
logger.debug("project-context.json not found at %s, skipping context updates", context_path)
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
# Determine config_dir (inside .claude directory)
|
|
491
|
+
config_dir = claude_dir / "config"
|
|
492
|
+
|
|
493
|
+
# Build task_info dict for process_agent_output
|
|
494
|
+
agent_type = task_info.get("agent", "unknown")
|
|
495
|
+
task_info_for_writer = {
|
|
496
|
+
"agent_type": agent_type,
|
|
497
|
+
"context_path": str(context_path),
|
|
498
|
+
"config_dir": str(config_dir),
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
result = process_agent_output(agent_output, task_info_for_writer)
|
|
502
|
+
|
|
503
|
+
if result and result.get("updated"):
|
|
504
|
+
logger.info(
|
|
505
|
+
"Context updated by %s: sections=%s",
|
|
506
|
+
agent_type, result.get("sections_updated", []),
|
|
507
|
+
)
|
|
508
|
+
if result and result.get("rejected"):
|
|
509
|
+
logger.debug(
|
|
510
|
+
"Context sections rejected for %s: %s",
|
|
511
|
+
agent_type, result.get("rejected", []),
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
return result
|
|
515
|
+
|
|
516
|
+
except Exception as e:
|
|
517
|
+
logger.debug("Context update processing failed (non-fatal): %s", e)
|
|
518
|
+
return None
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Load context contracts and merge agent permissions.
|
|
2
|
+
|
|
3
|
+
Subsystem 2 of the pre_tool_use Task/Agent path.
|
|
4
|
+
|
|
5
|
+
Loads context-contracts.json + cloud overlays, merges agent permissions,
|
|
6
|
+
finds empty writable sections, and builds a reminder string.
|
|
7
|
+
|
|
8
|
+
Cloud provider detection (formerly infrastructure_reader) is internal
|
|
9
|
+
to the contract loading process.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _detect_cloud_from_infrastructure(sections: dict) -> str:
|
|
20
|
+
"""Extract cloud provider name from v2 infrastructure.cloud_providers section.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
sections: The sections dict from project-context.json.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Cloud provider name string (e.g. aws, gcp) or empty string.
|
|
27
|
+
"""
|
|
28
|
+
infra = sections.get("infrastructure", {})
|
|
29
|
+
if isinstance(infra, dict):
|
|
30
|
+
providers = infra.get("cloud_providers", [])
|
|
31
|
+
if isinstance(providers, list) and providers:
|
|
32
|
+
primary = providers[0]
|
|
33
|
+
if isinstance(primary, dict):
|
|
34
|
+
return primary.get("name", "")
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_context_update_reminder(
|
|
39
|
+
subagent_type: str,
|
|
40
|
+
project_agents: list,
|
|
41
|
+
hooks_dir: Path = None,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Check which writable sections are empty and build a reminder.
|
|
44
|
+
|
|
45
|
+
Reads the context contracts to find writable sections for this agent,
|
|
46
|
+
then checks project-context.json to see which are empty.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
subagent_type: The agent type string (e.g. devops-developer).
|
|
50
|
+
project_agents: List of valid project agent names.
|
|
51
|
+
hooks_dir: Path to the hooks directory (for fallback config lookup).
|
|
52
|
+
Defaults to Path(__file__).parent.parent.parent if None.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Reminder string or empty string if no empty sections.
|
|
56
|
+
"""
|
|
57
|
+
if subagent_type not in project_agents:
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
if hooks_dir is None:
|
|
61
|
+
hooks_dir = Path(__file__).parent.parent.parent
|
|
62
|
+
|
|
63
|
+
# Load contracts to find writable sections.
|
|
64
|
+
# Strategy: load context-contracts.json (base) then merge cloud/{provider}.json.
|
|
65
|
+
# Fallback to legacy per-provider files for backward compatibility.
|
|
66
|
+
# We detect the cloud provider from project-context.json first.
|
|
67
|
+
cloud_provider = "gcp" # default
|
|
68
|
+
pc_paths_for_provider = [
|
|
69
|
+
Path(".claude/project-context/project-context.json"),
|
|
70
|
+
Path("project-context.json"),
|
|
71
|
+
]
|
|
72
|
+
for pp in pc_paths_for_provider:
|
|
73
|
+
if pp.exists():
|
|
74
|
+
try:
|
|
75
|
+
pc_data = json.loads(pp.read_text())
|
|
76
|
+
detected = (
|
|
77
|
+
pc_data.get("metadata", {}).get("cloud_provider", "")
|
|
78
|
+
or _detect_cloud_from_infrastructure(pc_data.get("sections", {}))
|
|
79
|
+
)
|
|
80
|
+
if detected:
|
|
81
|
+
cloud_provider = detected.lower()
|
|
82
|
+
break
|
|
83
|
+
except Exception:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Candidate config directories (installed project first, package fallback)
|
|
87
|
+
config_dirs = [
|
|
88
|
+
Path(".claude/config"),
|
|
89
|
+
hooks_dir.parent / "config",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
writable = []
|
|
93
|
+
for config_dir in config_dirs:
|
|
94
|
+
if not config_dir.is_dir():
|
|
95
|
+
continue
|
|
96
|
+
# Load base contracts
|
|
97
|
+
base_file = config_dir / "context-contracts.json"
|
|
98
|
+
cloud_file = config_dir / "cloud" / f"{cloud_provider}.json"
|
|
99
|
+
|
|
100
|
+
merged_agents = {}
|
|
101
|
+
if base_file.exists():
|
|
102
|
+
try:
|
|
103
|
+
data = json.loads(base_file.read_text())
|
|
104
|
+
merged_agents = data.get("agents", {})
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
# Merge cloud overrides
|
|
109
|
+
if merged_agents and cloud_file.exists():
|
|
110
|
+
try:
|
|
111
|
+
cloud_data = json.loads(cloud_file.read_text())
|
|
112
|
+
agent_cloud = cloud_data.get("agents", {}).get(subagent_type, {})
|
|
113
|
+
base_write = merged_agents.get(subagent_type, {}).get("write", [])
|
|
114
|
+
extra_write = [s for s in agent_cloud.get("write", []) if s not in base_write]
|
|
115
|
+
if subagent_type in merged_agents:
|
|
116
|
+
merged_agents[subagent_type]["write"] = base_write + extra_write
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
if merged_agents:
|
|
121
|
+
agent_perms = merged_agents.get(subagent_type, {})
|
|
122
|
+
writable = agent_perms.get("write", [])
|
|
123
|
+
if writable:
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
if not writable:
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
# Load project-context.json to find empty sections
|
|
130
|
+
pc_paths = [
|
|
131
|
+
Path(".claude/project-context/project-context.json"),
|
|
132
|
+
Path("project-context.json"),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
sections = {}
|
|
136
|
+
for pp in pc_paths:
|
|
137
|
+
if pp.exists():
|
|
138
|
+
try:
|
|
139
|
+
pc = json.loads(pp.read_text())
|
|
140
|
+
sections = pc.get("sections", {})
|
|
141
|
+
break
|
|
142
|
+
except Exception:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Find empty writable sections
|
|
146
|
+
empty = []
|
|
147
|
+
for section_name in writable:
|
|
148
|
+
section_data = sections.get(section_name, {})
|
|
149
|
+
if not section_data or section_data == {}:
|
|
150
|
+
empty.append(section_name)
|
|
151
|
+
|
|
152
|
+
if not empty:
|
|
153
|
+
return ""
|
|
154
|
+
|
|
155
|
+
empty_list = ", ".join(f"`{s}`" for s in empty)
|
|
156
|
+
return (
|
|
157
|
+
f"\n**CONTEXT_UPDATE REQUIRED:** Your writable sections {empty_list} "
|
|
158
|
+
f"are currently EMPTY. After completing your task, you MUST emit a "
|
|
159
|
+
f"CONTEXT_UPDATE block with any data you discovered. "
|
|
160
|
+
f"See \"Context Updater Protocol\" above for the format.\n\n"
|
|
161
|
+
)
|