@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
|
@@ -1,213 +1,134 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Metrics
|
|
2
|
+
Metrics aggregation from audit logs.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Reads audit-*.jsonl files (the single source of truth for execution data)
|
|
5
|
+
and produces aggregated summaries. No write path — audit/logger.py owns writes.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
import json
|
|
8
9
|
import logging
|
|
9
|
-
import os
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from datetime import datetime, timedelta
|
|
12
12
|
from typing import Dict, Any, List, Optional
|
|
13
13
|
from collections import defaultdict
|
|
14
14
|
|
|
15
|
-
from ..core.paths import
|
|
15
|
+
from ..core.paths import get_logs_dir
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
duration: Duration in seconds
|
|
51
|
-
success: Whether execution succeeded
|
|
52
|
-
tier: Security tier
|
|
53
|
-
"""
|
|
54
|
-
timestamp = datetime.now().isoformat()
|
|
55
|
-
|
|
56
|
-
metrics_record = {
|
|
57
|
-
"timestamp": timestamp,
|
|
58
|
-
"tool_name": tool_name,
|
|
59
|
-
"command_type": self._classify_command(command),
|
|
60
|
-
"duration_ms": round(duration * 1000, 2),
|
|
61
|
-
# NOTE: 'success' field removed -- Claude Code's PostToolUse hook
|
|
62
|
-
# always reports success=True due to API limitation, making the
|
|
63
|
-
# field unreliable. Use exit_code from audit logs instead.
|
|
64
|
-
"tier": tier,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
# Metrics files are a strict subset of audit logs (audit-*.jsonl).
|
|
68
|
-
# Writing is disabled by default; audit logs are the SSOT for command metrics.
|
|
69
|
-
# Set GAIA_WRITE_METRICS=1 to re-enable for backward compatibility.
|
|
70
|
-
if os.environ.get("GAIA_WRITE_METRICS") == "1":
|
|
71
|
-
metrics_file = self.metrics_dir / f"metrics-{datetime.now().strftime('%Y-%m')}.jsonl"
|
|
72
|
-
try:
|
|
73
|
-
with open(metrics_file, "a") as f:
|
|
74
|
-
f.write(json.dumps(metrics_record) + "\n")
|
|
75
|
-
except Exception as e:
|
|
76
|
-
logger.error(f"Error writing metrics: {e}")
|
|
77
|
-
|
|
78
|
-
def _classify_command(self, command: str) -> str:
|
|
79
|
-
"""Classify command type for metrics."""
|
|
80
|
-
command_lower = command.lower()
|
|
81
|
-
classifiers = [
|
|
82
|
-
("terraform", "terraform"),
|
|
83
|
-
("kubectl", "kubernetes"),
|
|
84
|
-
("helm", "helm"),
|
|
85
|
-
("gcloud", "gcp"),
|
|
86
|
-
("aws", "aws"),
|
|
87
|
-
("flux", "flux"),
|
|
88
|
-
("docker", "docker"),
|
|
89
|
-
("git", "git"),
|
|
90
|
-
]
|
|
91
|
-
for keyword, classification in classifiers:
|
|
92
|
-
if keyword in command_lower:
|
|
93
|
-
return classification
|
|
94
|
-
return "general"
|
|
95
|
-
|
|
96
|
-
def generate_summary(self, days: int = 7) -> Dict[str, Any]:
|
|
97
|
-
"""
|
|
98
|
-
Generate FUNCTIONAL metrics summary for the last N days.
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
days: Number of days to include
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
Dictionary with aggregated metrics
|
|
105
|
-
"""
|
|
106
|
-
cutoff_date = datetime.now() - timedelta(days=days)
|
|
107
|
-
records = self._load_records_since(cutoff_date)
|
|
108
|
-
|
|
109
|
-
if not records:
|
|
110
|
-
return {
|
|
111
|
-
"period_days": days,
|
|
112
|
-
"total_executions": 0,
|
|
113
|
-
"success_rate": 0.0,
|
|
114
|
-
"avg_duration_ms": 0.0,
|
|
115
|
-
"top_commands": [],
|
|
116
|
-
"tier_distribution": {},
|
|
117
|
-
"command_type_distribution": {},
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
# Calculate metrics
|
|
121
|
-
total = len(records)
|
|
122
|
-
successes = sum(1 for r in records if r.get("success", True))
|
|
123
|
-
total_duration = sum(r.get("duration_ms", 0) for r in records)
|
|
124
|
-
|
|
125
|
-
# Count by command type
|
|
126
|
-
command_types = defaultdict(int)
|
|
127
|
-
for r in records:
|
|
128
|
-
command_types[r.get("command_type", "unknown")] += 1
|
|
129
|
-
|
|
130
|
-
# Count by tier
|
|
131
|
-
tiers = defaultdict(int)
|
|
132
|
-
for r in records:
|
|
133
|
-
tiers[r.get("tier", "unknown")] += 1
|
|
134
|
-
|
|
135
|
-
# Top command types
|
|
136
|
-
top_commands = sorted(
|
|
137
|
-
command_types.items(),
|
|
138
|
-
key=lambda x: x[1],
|
|
139
|
-
reverse=True
|
|
140
|
-
)[:10]
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
"period_days": days,
|
|
144
|
-
"total_executions": total,
|
|
145
|
-
"success_rate": round(successes / total, 4) if total > 0 else 0.0,
|
|
146
|
-
"avg_duration_ms": round(total_duration / total, 2) if total > 0 else 0.0,
|
|
147
|
-
"top_commands": [{"type": t, "count": c} for t, c in top_commands],
|
|
148
|
-
"tier_distribution": dict(tiers),
|
|
149
|
-
"command_type_distribution": dict(command_types),
|
|
150
|
-
"generated_at": datetime.now().isoformat(),
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
def _load_records_since(self, cutoff_date: datetime) -> List[Dict]:
|
|
154
|
-
"""Load all records since cutoff date."""
|
|
155
|
-
records = []
|
|
20
|
+
def _classify_command(command: str) -> str:
|
|
21
|
+
"""Classify command type for metrics aggregation."""
|
|
22
|
+
command_lower = command.lower()
|
|
23
|
+
classifiers = [
|
|
24
|
+
("terraform", "terraform"),
|
|
25
|
+
("kubectl", "kubernetes"),
|
|
26
|
+
("helm", "helm"),
|
|
27
|
+
("gcloud", "gcp"),
|
|
28
|
+
("aws", "aws"),
|
|
29
|
+
("flux", "flux"),
|
|
30
|
+
("docker", "docker"),
|
|
31
|
+
("git", "git"),
|
|
32
|
+
]
|
|
33
|
+
for keyword, classification in classifiers:
|
|
34
|
+
if keyword in command_lower:
|
|
35
|
+
return classification
|
|
36
|
+
return "general"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_audit_records_since(
|
|
40
|
+
logs_dir: Path, cutoff_date: datetime
|
|
41
|
+
) -> List[Dict]:
|
|
42
|
+
"""Load audit records from audit-*.jsonl files since cutoff date."""
|
|
43
|
+
records = []
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
audit_files = list(logs_dir.glob("audit-*.jsonl"))
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Error listing audit files: {e}")
|
|
49
|
+
return records
|
|
156
50
|
|
|
157
|
-
|
|
51
|
+
for audit_file in audit_files:
|
|
158
52
|
try:
|
|
159
|
-
|
|
53
|
+
with open(audit_file, "r") as f:
|
|
54
|
+
for line in f:
|
|
55
|
+
line = line.strip()
|
|
56
|
+
if not line:
|
|
57
|
+
continue
|
|
58
|
+
try:
|
|
59
|
+
record = json.loads(line)
|
|
60
|
+
record_time = datetime.fromisoformat(
|
|
61
|
+
record.get("timestamp", "")
|
|
62
|
+
)
|
|
63
|
+
if record_time >= cutoff_date:
|
|
64
|
+
records.append(record)
|
|
65
|
+
except (json.JSONDecodeError, ValueError):
|
|
66
|
+
continue
|
|
160
67
|
except Exception as e:
|
|
161
|
-
logger.
|
|
162
|
-
return records
|
|
163
|
-
|
|
164
|
-
for metrics_file in metrics_files:
|
|
165
|
-
try:
|
|
166
|
-
with open(metrics_file, "r") as f:
|
|
167
|
-
for line in f:
|
|
168
|
-
line = line.strip()
|
|
169
|
-
if not line:
|
|
170
|
-
continue
|
|
171
|
-
try:
|
|
172
|
-
record = json.loads(line)
|
|
173
|
-
record_time = datetime.fromisoformat(
|
|
174
|
-
record.get("timestamp", "")
|
|
175
|
-
)
|
|
176
|
-
if record_time >= cutoff_date:
|
|
177
|
-
records.append(record)
|
|
178
|
-
except (json.JSONDecodeError, ValueError):
|
|
179
|
-
continue
|
|
180
|
-
except Exception as e:
|
|
181
|
-
logger.debug(f"Error reading {metrics_file}: {e}")
|
|
182
|
-
|
|
183
|
-
return records
|
|
68
|
+
logger.debug(f"Error reading {audit_file}: {e}")
|
|
184
69
|
|
|
70
|
+
return records
|
|
185
71
|
|
|
186
|
-
# Singleton collector
|
|
187
|
-
_metrics_collector: Optional[MetricsCollector] = None
|
|
188
72
|
|
|
73
|
+
def generate_summary(
|
|
74
|
+
days: int = 7, logs_dir: Optional[Path] = None
|
|
75
|
+
) -> Dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Generate metrics summary from audit logs for the last N days.
|
|
189
78
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if _metrics_collector is None:
|
|
194
|
-
_metrics_collector = MetricsCollector()
|
|
195
|
-
return _metrics_collector
|
|
79
|
+
Args:
|
|
80
|
+
days: Number of days to include
|
|
81
|
+
logs_dir: Override logs directory (for testing)
|
|
196
82
|
|
|
83
|
+
Returns:
|
|
84
|
+
Dictionary with aggregated metrics:
|
|
85
|
+
- period_days, total_executions, avg_duration_ms
|
|
86
|
+
- top_commands (by classified command_type)
|
|
87
|
+
- tier_distribution, command_type_distribution
|
|
88
|
+
"""
|
|
89
|
+
if logs_dir is None:
|
|
90
|
+
logs_dir = get_logs_dir()
|
|
197
91
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
command: str,
|
|
201
|
-
duration: float,
|
|
202
|
-
success: bool,
|
|
203
|
-
tier: str = "unknown"
|
|
204
|
-
) -> None:
|
|
205
|
-
"""Record execution metric (convenience function)."""
|
|
206
|
-
get_metrics_collector().record_execution(
|
|
207
|
-
tool_name, command, duration, success, tier
|
|
208
|
-
)
|
|
92
|
+
cutoff_date = datetime.now() - timedelta(days=days)
|
|
93
|
+
records = _load_audit_records_since(logs_dir, cutoff_date)
|
|
209
94
|
|
|
95
|
+
if not records:
|
|
96
|
+
return {
|
|
97
|
+
"period_days": days,
|
|
98
|
+
"total_executions": 0,
|
|
99
|
+
"avg_duration_ms": 0.0,
|
|
100
|
+
"top_commands": [],
|
|
101
|
+
"tier_distribution": {},
|
|
102
|
+
"command_type_distribution": {},
|
|
103
|
+
}
|
|
210
104
|
|
|
211
|
-
|
|
212
|
-
""
|
|
213
|
-
|
|
105
|
+
total = len(records)
|
|
106
|
+
total_duration = sum(r.get("duration_ms", 0) for r in records)
|
|
107
|
+
|
|
108
|
+
# Classify commands from audit log 'command' field
|
|
109
|
+
command_types = defaultdict(int)
|
|
110
|
+
for r in records:
|
|
111
|
+
cmd = r.get("command", "")
|
|
112
|
+
command_types[_classify_command(cmd)] += 1
|
|
113
|
+
|
|
114
|
+
# Count by tier
|
|
115
|
+
tiers = defaultdict(int)
|
|
116
|
+
for r in records:
|
|
117
|
+
tiers[r.get("tier", "unknown")] += 1
|
|
118
|
+
|
|
119
|
+
# Top command types
|
|
120
|
+
top_commands = sorted(
|
|
121
|
+
command_types.items(),
|
|
122
|
+
key=lambda x: x[1],
|
|
123
|
+
reverse=True,
|
|
124
|
+
)[:10]
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"period_days": days,
|
|
128
|
+
"total_executions": total,
|
|
129
|
+
"avg_duration_ms": round(total_duration / total, 2) if total > 0 else 0.0,
|
|
130
|
+
"top_commands": [{"type": t, "count": c} for t, c in top_commands],
|
|
131
|
+
"tier_distribution": dict(tiers),
|
|
132
|
+
"command_type_distribution": dict(command_types),
|
|
133
|
+
"generated_at": datetime.now().isoformat(),
|
|
134
|
+
}
|
|
@@ -5,7 +5,6 @@ Renamed from anomaly_detector.py and expanded with additional anomaly types.
|
|
|
5
5
|
|
|
6
6
|
Provides:
|
|
7
7
|
- audit(): Full anomaly detection suite -> list of anomaly dicts
|
|
8
|
-
- detect_anomalies(): Backward-compatible alias for audit()
|
|
9
8
|
- signal_gaia_analysis(): Create flag file for Gaia analysis
|
|
10
9
|
"""
|
|
11
10
|
|
|
@@ -529,9 +528,6 @@ def audit(
|
|
|
529
528
|
return anomalies
|
|
530
529
|
|
|
531
530
|
|
|
532
|
-
# Backward-compatible alias
|
|
533
|
-
detect_anomalies = audit
|
|
534
|
-
|
|
535
531
|
|
|
536
532
|
def signal_gaia_analysis(
|
|
537
533
|
anomalies: List[Dict],
|
|
@@ -6,7 +6,6 @@ Renamed from metrics_recorder.py for clarity.
|
|
|
6
6
|
Provides:
|
|
7
7
|
- get_workflow_memory_dir(): Resolve workflow memory directory
|
|
8
8
|
- record(): Build metrics dict, write to JSONL
|
|
9
|
-
- capture_workflow_metrics(): Backward-compatible alias for record()
|
|
10
9
|
"""
|
|
11
10
|
|
|
12
11
|
import json
|
|
@@ -295,7 +294,3 @@ def record(
|
|
|
295
294
|
)
|
|
296
295
|
|
|
297
296
|
return metrics
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
# Backward-compatible alias
|
|
301
|
-
capture_workflow_metrics = record
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context cache for PreToolUse -> SubagentStart handoff.
|
|
3
|
+
|
|
4
|
+
PreToolUse:Agent builds context (needs the prompt for surface routing) but the
|
|
5
|
+
context must reach the subagent, not the orchestrator. SubagentStart is where
|
|
6
|
+
context should be injected, but SubagentStart does not receive the prompt.
|
|
7
|
+
|
|
8
|
+
Solution: PreToolUse caches the built context to a temp file keyed by
|
|
9
|
+
session_id. SubagentStart reads and consumes the cache (one-shot).
|
|
10
|
+
|
|
11
|
+
Cache location: /tmp/gaia-context-cache/{session_id}-{timestamp}.json
|
|
12
|
+
TTL: 60 seconds (stale files cleaned on write).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
CACHE_DIR = Path("/tmp/gaia-context-cache")
|
|
27
|
+
CACHE_TTL_SECONDS = 60
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_context_cache(
|
|
31
|
+
session_id: str,
|
|
32
|
+
context: str,
|
|
33
|
+
agent_type: str = "",
|
|
34
|
+
) -> Path:
|
|
35
|
+
"""Write context to a cache file for later consumption by SubagentStart.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
session_id: Hook session identifier (shared between PreToolUse and SubagentStart).
|
|
39
|
+
context: The full additionalContext string to inject into the subagent.
|
|
40
|
+
agent_type: The agent type (for logging/diagnostics).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Path to the written cache file.
|
|
44
|
+
"""
|
|
45
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
# Clean stale files first
|
|
48
|
+
_cleanup_stale_caches()
|
|
49
|
+
|
|
50
|
+
timestamp = int(time.time() * 1000)
|
|
51
|
+
cache_file = CACHE_DIR / f"{session_id}-{timestamp}.json"
|
|
52
|
+
payload = {
|
|
53
|
+
"context": context,
|
|
54
|
+
"agent_type": agent_type,
|
|
55
|
+
"timestamp": timestamp,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cache_file.write_text(json.dumps(payload))
|
|
59
|
+
logger.info(
|
|
60
|
+
"Cached context for session=%s agent=%s (%d bytes) -> %s",
|
|
61
|
+
session_id, agent_type, len(context), cache_file.name,
|
|
62
|
+
)
|
|
63
|
+
return cache_file
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_context_cache(session_id: str) -> Optional[dict]:
|
|
67
|
+
"""Read and consume the most recent context cache for a session.
|
|
68
|
+
|
|
69
|
+
Returns the cache payload dict if found, or None if no cache exists.
|
|
70
|
+
The cache file is deleted after reading (one-shot consumption).
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
session_id: Hook session identifier.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dict with keys: context, agent_type, timestamp. Or None.
|
|
77
|
+
"""
|
|
78
|
+
if not CACHE_DIR.exists():
|
|
79
|
+
logger.debug("No cache directory found")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
# Find all cache files for this session, sorted by timestamp (newest first)
|
|
83
|
+
prefix = f"{session_id}-"
|
|
84
|
+
candidates = sorted(
|
|
85
|
+
[f for f in CACHE_DIR.iterdir() if f.name.startswith(prefix) and f.suffix == ".json"],
|
|
86
|
+
key=lambda p: p.stat().st_mtime,
|
|
87
|
+
reverse=True,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not candidates:
|
|
91
|
+
logger.debug("No cache files found for session=%s", session_id)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
cache_file = candidates[0]
|
|
95
|
+
try:
|
|
96
|
+
payload = json.loads(cache_file.read_text())
|
|
97
|
+
logger.info(
|
|
98
|
+
"Read context cache for session=%s agent=%s from %s",
|
|
99
|
+
session_id, payload.get("agent_type", "unknown"), cache_file.name,
|
|
100
|
+
)
|
|
101
|
+
# One-shot: delete after reading
|
|
102
|
+
cache_file.unlink(missing_ok=True)
|
|
103
|
+
|
|
104
|
+
# Clean up any older duplicates for this session
|
|
105
|
+
for stale in candidates[1:]:
|
|
106
|
+
stale.unlink(missing_ok=True)
|
|
107
|
+
|
|
108
|
+
return payload
|
|
109
|
+
|
|
110
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
111
|
+
logger.warning("Failed to read cache file %s: %s", cache_file, exc)
|
|
112
|
+
cache_file.unlink(missing_ok=True)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _cleanup_stale_caches() -> None:
|
|
117
|
+
"""Remove cache files older than CACHE_TTL_SECONDS."""
|
|
118
|
+
if not CACHE_DIR.exists():
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
cutoff = time.time() - CACHE_TTL_SECONDS
|
|
122
|
+
for f in CACHE_DIR.iterdir():
|
|
123
|
+
if f.suffix == ".json":
|
|
124
|
+
try:
|
|
125
|
+
if f.stat().st_mtime < cutoff:
|
|
126
|
+
f.unlink(missing_ok=True)
|
|
127
|
+
logger.debug("Cleaned stale cache: %s", f.name)
|
|
128
|
+
except OSError:
|
|
129
|
+
pass
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Core context injection subsystem for project agents.
|
|
2
2
|
|
|
3
3
|
Handles:
|
|
4
|
-
- build_project_context: builds context string
|
|
5
|
-
- inject_project_context: legacy wrapper that mutates parameters (backward compat)
|
|
4
|
+
- build_project_context: builds context string for additionalContext injection
|
|
6
5
|
- check_pending_updates_threshold: warns when pending updates accumulate
|
|
7
6
|
- check_recent_critical_anomalies: surfaces critical anomalies from JSONL log
|
|
8
7
|
- consume_anomaly_flag: reads and deletes anomaly signal flags
|
|
@@ -269,16 +268,6 @@ def build_project_context(
|
|
|
269
268
|
logger.warning(f"No prompt provided for {subagent_type}, skipping context injection")
|
|
270
269
|
return None, {}
|
|
271
270
|
|
|
272
|
-
# Deduplication guard: if context was already injected (e.g., by a
|
|
273
|
-
# previous hook or retry), do not inject again. The "# Project Context"
|
|
274
|
-
# header is the canonical marker written by this function.
|
|
275
|
-
if "# Project Context" in prompt:
|
|
276
|
-
logger.warning(
|
|
277
|
-
"Duplicate context injection prevented for %s — prompt already "
|
|
278
|
-
"contains '# Project Context' header", subagent_type,
|
|
279
|
-
)
|
|
280
|
-
return None, {}
|
|
281
|
-
|
|
282
271
|
try:
|
|
283
272
|
# Find context_provider.py
|
|
284
273
|
context_provider_paths = [
|
|
@@ -398,6 +387,23 @@ def build_project_context(
|
|
|
398
387
|
if critical_summary:
|
|
399
388
|
context_string += critical_summary
|
|
400
389
|
|
|
390
|
+
# Inject recent operational events (non-blocking)
|
|
391
|
+
try:
|
|
392
|
+
from ..events.event_writer import read_events
|
|
393
|
+
recent = read_events(hours=24, limit=20)
|
|
394
|
+
if recent:
|
|
395
|
+
lines = ["\n# Recent Events (last 24h)"]
|
|
396
|
+
for evt in recent:
|
|
397
|
+
ts_short = evt.get("ts", "")[:16]
|
|
398
|
+
etype = evt.get("type", "")
|
|
399
|
+
agent_name = evt.get("agent", "")
|
|
400
|
+
result_str = evt.get("result", "")
|
|
401
|
+
label = f"{agent_name}: " if agent_name else ""
|
|
402
|
+
lines.append(f"- [{ts_short}] {etype}: {label}{result_str}")
|
|
403
|
+
context_string += "\n".join(lines) + "\n"
|
|
404
|
+
except Exception as exc:
|
|
405
|
+
logger.debug("Event context injection failed (non-fatal): %s", exc)
|
|
406
|
+
|
|
401
407
|
# Build telemetry snapshot
|
|
402
408
|
telemetry = build_context_telemetry_snapshot(context_payload)
|
|
403
409
|
|
|
@@ -419,31 +425,3 @@ def build_project_context(
|
|
|
419
425
|
return None, {}
|
|
420
426
|
|
|
421
427
|
|
|
422
|
-
def inject_project_context(
|
|
423
|
-
parameters: dict,
|
|
424
|
-
project_agents: list,
|
|
425
|
-
hooks_dir: Path = None,
|
|
426
|
-
) -> dict:
|
|
427
|
-
"""
|
|
428
|
-
Legacy wrapper: inject project context by mutating parameters["prompt"].
|
|
429
|
-
|
|
430
|
-
Retained for backward compatibility (tests import this function).
|
|
431
|
-
New code should use build_project_context() with additionalContext instead.
|
|
432
|
-
|
|
433
|
-
Args:
|
|
434
|
-
parameters: Original Task tool parameters (will be mutated).
|
|
435
|
-
project_agents: List of valid project agent names.
|
|
436
|
-
hooks_dir: Path to the hooks directory.
|
|
437
|
-
|
|
438
|
-
Returns:
|
|
439
|
-
Modified parameters with context injected into prompt.
|
|
440
|
-
"""
|
|
441
|
-
context_string, _telemetry = build_project_context(parameters, project_agents, hooks_dir)
|
|
442
|
-
if context_string is None:
|
|
443
|
-
return parameters
|
|
444
|
-
|
|
445
|
-
prompt = parameters.get("prompt", "")
|
|
446
|
-
enriched_prompt = f"# Task\n\n{prompt}\n{context_string}"
|
|
447
|
-
parameters["prompt"] = enriched_prompt
|
|
448
|
-
|
|
449
|
-
return parameters
|
|
@@ -14,7 +14,6 @@ Public API:
|
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
16
|
import logging
|
|
17
|
-
import re
|
|
18
17
|
from datetime import datetime, timezone
|
|
19
18
|
from pathlib import Path
|
|
20
19
|
from typing import Dict, List, Optional
|
|
@@ -107,15 +106,6 @@ LEGACY_AGENT_CONTRACTS: Dict[str, List[str]] = {
|
|
|
107
106
|
_contracts_cache: Dict[str, dict] = {}
|
|
108
107
|
|
|
109
108
|
|
|
110
|
-
# ---------------------------------------------------------------------------
|
|
111
|
-
# Known markers that terminate CONTEXT_UPDATE extraction
|
|
112
|
-
# ---------------------------------------------------------------------------
|
|
113
|
-
_KNOWN_MARKERS = re.compile(
|
|
114
|
-
r"^(?:AGENT_STATUS|CONTEXT_UPDATE|<!-- AGENT_STATUS):",
|
|
115
|
-
re.MULTILINE,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
109
|
# ============================================================================
|
|
120
110
|
# 1. parse_context_update
|
|
121
111
|
# ============================================================================
|
|
@@ -334,7 +324,6 @@ def _merge_base_and_cloud(provider: str, config_dir: Path) -> dict:
|
|
|
334
324
|
"""
|
|
335
325
|
base_file = config_dir / "context-contracts.json"
|
|
336
326
|
cloud_file = config_dir / "cloud" / f"{provider}.json"
|
|
337
|
-
legacy_file = config_dir / f"context-contracts.{provider}.json"
|
|
338
327
|
|
|
339
328
|
# Step 1: Load base contracts
|
|
340
329
|
base_contracts = None
|
|
@@ -344,16 +333,7 @@ def _merge_base_and_cloud(provider: str, config_dir: Path) -> dict:
|
|
|
344
333
|
except (json.JSONDecodeError, OSError) as exc:
|
|
345
334
|
logger.warning("Failed to load base contracts from %s: %s", base_file, exc)
|
|
346
335
|
|
|
347
|
-
# Step 2:
|
|
348
|
-
if base_contracts is None:
|
|
349
|
-
if legacy_file.exists():
|
|
350
|
-
try:
|
|
351
|
-
base_contracts = json.loads(legacy_file.read_text())
|
|
352
|
-
logger.info("Using legacy contracts from %s", legacy_file)
|
|
353
|
-
except (json.JSONDecodeError, OSError) as exc:
|
|
354
|
-
logger.warning("Failed to load legacy contracts from %s: %s", legacy_file, exc)
|
|
355
|
-
|
|
356
|
-
# Step 3: Final fallback to hardcoded LEGACY_AGENT_CONTRACTS
|
|
336
|
+
# Step 2: Final fallback to hardcoded LEGACY_AGENT_CONTRACTS
|
|
357
337
|
if base_contracts is None:
|
|
358
338
|
logger.warning("No contract files found in %s, using hardcoded legacy contracts", config_dir)
|
|
359
339
|
return {
|
|
@@ -536,7 +516,3 @@ def process_context_updates(
|
|
|
536
516
|
except Exception as e:
|
|
537
517
|
logger.debug("Context update processing failed (non-fatal): %s", e)
|
|
538
518
|
return None
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
# Module-level alias for shorter import name
|
|
542
|
-
update = process_context_updates
|
|
@@ -93,20 +93,17 @@ def build_context_update_reminder(
|
|
|
93
93
|
for config_dir in config_dirs:
|
|
94
94
|
if not config_dir.is_dir():
|
|
95
95
|
continue
|
|
96
|
-
#
|
|
96
|
+
# Load base contracts
|
|
97
97
|
base_file = config_dir / "context-contracts.json"
|
|
98
98
|
cloud_file = config_dir / "cloud" / f"{cloud_provider}.json"
|
|
99
|
-
legacy_file = config_dir / f"context-contracts.{cloud_provider}.json"
|
|
100
99
|
|
|
101
100
|
merged_agents = {}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
except Exception:
|
|
109
|
-
continue
|
|
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
|
|
110
107
|
|
|
111
108
|
# Merge cloud overrides
|
|
112
109
|
if merged_agents and cloud_file.exists():
|