@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,215 @@
|
|
|
1
|
+
"""Compact context builder for post-compaction re-injection.
|
|
2
|
+
|
|
3
|
+
Builds a lightweight context summary from session data sources.
|
|
4
|
+
Each source is independent and fail-safe.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ..core.paths import get_plugin_data_dir
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Defaults
|
|
18
|
+
DEFAULT_MAX_SNAPSHOTS = 5
|
|
19
|
+
DEFAULT_ANOMALY_WINDOW_HOURS = 1
|
|
20
|
+
DEFAULT_MAX_EVENTS = 5
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_compact_context(
|
|
24
|
+
*,
|
|
25
|
+
max_snapshots: int = DEFAULT_MAX_SNAPSHOTS,
|
|
26
|
+
anomaly_window_hours: int = DEFAULT_ANOMALY_WINDOW_HOURS,
|
|
27
|
+
max_events: int = DEFAULT_MAX_EVENTS,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Build compact context for post-compaction re-injection.
|
|
30
|
+
|
|
31
|
+
Returns a markdown string with 4 blocks:
|
|
32
|
+
1. Orchestrator identity reminder
|
|
33
|
+
2. Session activity summary (from run-snapshots.jsonl)
|
|
34
|
+
3. Active anomalies (from anomalies.jsonl)
|
|
35
|
+
4. Recent session events (from context.json)
|
|
36
|
+
|
|
37
|
+
Each block is independent — if a source fails, the others still produce output.
|
|
38
|
+
"""
|
|
39
|
+
blocks = []
|
|
40
|
+
|
|
41
|
+
# Block 1: Orchestrator identity (always present, static)
|
|
42
|
+
blocks.append(_build_identity_block())
|
|
43
|
+
|
|
44
|
+
# Block 2: Session activity from run-snapshots.jsonl
|
|
45
|
+
activity = _build_activity_block(max_snapshots)
|
|
46
|
+
if activity:
|
|
47
|
+
blocks.append(activity)
|
|
48
|
+
|
|
49
|
+
# Block 3: Active anomalies from anomalies.jsonl
|
|
50
|
+
anomalies = _build_anomalies_block(anomaly_window_hours)
|
|
51
|
+
if anomalies:
|
|
52
|
+
blocks.append(anomalies)
|
|
53
|
+
|
|
54
|
+
# Block 4: Recent events from context.json
|
|
55
|
+
events = _build_events_block(max_events)
|
|
56
|
+
if events:
|
|
57
|
+
blocks.append(events)
|
|
58
|
+
|
|
59
|
+
return "\n\n".join(blocks)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _build_identity_block() -> str:
|
|
63
|
+
"""Static orchestrator identity reminder."""
|
|
64
|
+
return (
|
|
65
|
+
"# Post-Compaction Context Refresh\n\n"
|
|
66
|
+
"You are the orchestrator. Dispatch work via Agent, resume agents via "
|
|
67
|
+
"SendMessage(to: agentId), get user approval via AskUserQuestion. "
|
|
68
|
+
"Never execute infrastructure commands directly.\n"
|
|
69
|
+
"Agents: cloud-troubleshooter, gitops-operator, terraform-architect, "
|
|
70
|
+
"devops-developer, speckit-planner, gaia-system"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _build_activity_block(max_snapshots: int) -> str | None:
|
|
75
|
+
"""Build session activity summary from run-snapshots.jsonl."""
|
|
76
|
+
snapshots_path = (
|
|
77
|
+
get_plugin_data_dir() / "project-context" / "workflow-episodic-memory" / "run-snapshots.jsonl"
|
|
78
|
+
)
|
|
79
|
+
if not snapshots_path.exists():
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
lines = snapshots_path.read_text().splitlines()
|
|
84
|
+
# Take last N lines
|
|
85
|
+
recent = lines[-max_snapshots:] if len(lines) > max_snapshots else lines
|
|
86
|
+
|
|
87
|
+
entries = []
|
|
88
|
+
for line in recent:
|
|
89
|
+
if not line.strip():
|
|
90
|
+
continue
|
|
91
|
+
try:
|
|
92
|
+
snap = json.loads(line)
|
|
93
|
+
agent = snap.get("agent", "unknown")
|
|
94
|
+
status = snap.get("plan_status", "unknown")
|
|
95
|
+
prompt = snap.get("prompt", "")[:80]
|
|
96
|
+
cmd_count = snap.get("commands_executed_count", 0)
|
|
97
|
+
entries.append(f"- {agent} → {status} ({prompt}, {cmd_count} commands)")
|
|
98
|
+
except json.JSONDecodeError:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
if not entries:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
return "## Session Activity\n" + "\n".join(entries)
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.debug("Failed to build activity block (non-fatal): %s", e)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _build_anomalies_block(window_hours: int) -> str | None:
|
|
112
|
+
"""Build active anomalies summary from anomalies.jsonl."""
|
|
113
|
+
anomaly_path = (
|
|
114
|
+
get_plugin_data_dir() / "project-context" / "workflow-episodic-memory" / "anomalies.jsonl"
|
|
115
|
+
)
|
|
116
|
+
if not anomaly_path.exists():
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
lines = anomaly_path.read_text().splitlines()[-20:]
|
|
121
|
+
cutoff = datetime.now().timestamp() - (window_hours * 3600)
|
|
122
|
+
|
|
123
|
+
critical_types: list[str] = []
|
|
124
|
+
warning_types: list[str] = []
|
|
125
|
+
|
|
126
|
+
for line in lines:
|
|
127
|
+
if not line.strip():
|
|
128
|
+
continue
|
|
129
|
+
try:
|
|
130
|
+
entry = json.loads(line)
|
|
131
|
+
ts = entry.get("timestamp", "")
|
|
132
|
+
if ts:
|
|
133
|
+
try:
|
|
134
|
+
entry_time = datetime.fromisoformat(ts).timestamp()
|
|
135
|
+
if entry_time < cutoff:
|
|
136
|
+
continue
|
|
137
|
+
except (ValueError, TypeError):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
for anomaly in entry.get("anomalies", []):
|
|
141
|
+
severity = anomaly.get("severity", "")
|
|
142
|
+
atype = anomaly.get("type", "unknown")
|
|
143
|
+
if severity == "critical":
|
|
144
|
+
critical_types.append(atype)
|
|
145
|
+
elif severity == "warning":
|
|
146
|
+
warning_types.append(atype)
|
|
147
|
+
except json.JSONDecodeError:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if not critical_types and not warning_types:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
parts = []
|
|
154
|
+
if critical_types:
|
|
155
|
+
unique = sorted(set(critical_types))
|
|
156
|
+
parts.append(f"- {len(critical_types)} critical: {', '.join(unique)}")
|
|
157
|
+
if warning_types:
|
|
158
|
+
unique = sorted(set(warning_types))
|
|
159
|
+
parts.append(f"- {len(warning_types)} warning: {', '.join(unique)}")
|
|
160
|
+
|
|
161
|
+
return "## Active Anomalies\n" + "\n".join(parts)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.debug("Failed to build anomalies block (non-fatal): %s", e)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _build_events_block(max_events: int) -> str | None:
|
|
169
|
+
"""Build recent events summary from session context.json."""
|
|
170
|
+
context_path = Path(".claude/session/active/context.json")
|
|
171
|
+
if not context_path.exists():
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
with open(context_path) as f:
|
|
176
|
+
context = json.load(f)
|
|
177
|
+
|
|
178
|
+
events = context.get("critical_events", [])
|
|
179
|
+
if not events:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
# Take last N events
|
|
183
|
+
recent = events[-max_events:]
|
|
184
|
+
|
|
185
|
+
lines = []
|
|
186
|
+
for event in recent:
|
|
187
|
+
etype = event.get("event_type", "")
|
|
188
|
+
ts = event.get("timestamp", "")[:16]
|
|
189
|
+
|
|
190
|
+
if etype == "git_commit":
|
|
191
|
+
msg = event.get("commit_message", "")
|
|
192
|
+
hash_val = event.get("commit_hash", "")[:7]
|
|
193
|
+
if hash_val and msg:
|
|
194
|
+
lines.append(f"- [{ts}] Commit {hash_val}: {msg}")
|
|
195
|
+
elif etype == "git_push":
|
|
196
|
+
branch = event.get("branch", "")
|
|
197
|
+
if branch:
|
|
198
|
+
lines.append(f"- [{ts}] Pushed to {branch}")
|
|
199
|
+
elif etype == "file_modifications":
|
|
200
|
+
count = event.get("modification_count", 0)
|
|
201
|
+
if count:
|
|
202
|
+
lines.append(f"- [{ts}] Modified {count} files")
|
|
203
|
+
elif etype == "infrastructure_change":
|
|
204
|
+
cmd = event.get("command", "")
|
|
205
|
+
if cmd:
|
|
206
|
+
lines.append(f"- [{ts}] Infrastructure: {cmd}")
|
|
207
|
+
|
|
208
|
+
if not lines:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
return "## Recent Events\n" + "\n".join(lines)
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.debug("Failed to build events block (non-fatal): %s", e)
|
|
215
|
+
return None
|
|
@@ -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
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context freshness checker for SessionStart hook.
|
|
3
|
+
|
|
4
|
+
Determines whether project-context.json is fresh enough to skip a rescan.
|
|
5
|
+
Uses metadata.scan_config.last_scan (preferred) or file mtime as fallback.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
- check_freshness(project_root: Path) -> FreshnessResult
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime, timedelta, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from ..core.paths import find_claude_dir
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Context freshness threshold (hours) -- env var overrides default
|
|
24
|
+
DEFAULT_FRESHNESS_HOURS = 24
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class FreshnessResult:
|
|
29
|
+
"""Result of a context freshness check."""
|
|
30
|
+
|
|
31
|
+
is_fresh: bool
|
|
32
|
+
reason: str
|
|
33
|
+
age_hours: float = 0.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_context_path() -> Path:
|
|
37
|
+
"""Return path to project-context.json."""
|
|
38
|
+
claude_dir = find_claude_dir()
|
|
39
|
+
return claude_dir / "project-context" / "project-context.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _read_staleness_from_context(context_path: Path) -> Optional[int]:
|
|
43
|
+
"""Read staleness_hours from metadata.scan_config in the context file.
|
|
44
|
+
|
|
45
|
+
Returns None if the file cannot be read or the field is absent.
|
|
46
|
+
"""
|
|
47
|
+
if not context_path.is_file():
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
with open(context_path, "r") as f:
|
|
51
|
+
data = json.load(f)
|
|
52
|
+
return (
|
|
53
|
+
int(
|
|
54
|
+
data.get("metadata", {})
|
|
55
|
+
.get("scan_config", {})
|
|
56
|
+
.get("staleness_hours", 0)
|
|
57
|
+
)
|
|
58
|
+
or None
|
|
59
|
+
)
|
|
60
|
+
except (json.JSONDecodeError, OSError, ValueError, TypeError):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_effective_threshold() -> int:
|
|
65
|
+
"""Determine the effective freshness threshold in hours."""
|
|
66
|
+
return int(
|
|
67
|
+
os.environ.get(
|
|
68
|
+
"GAIA_SCAN_STALENESS_HOURS",
|
|
69
|
+
os.environ.get("CONTEXT_FRESHNESS_HOURS", str(DEFAULT_FRESHNESS_HOURS)),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_freshness(project_root: Path = None) -> FreshnessResult:
|
|
75
|
+
"""Check if project-context.json exists and is fresh (< threshold).
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
project_root: Unused, kept for API compatibility. Context path
|
|
79
|
+
is resolved via find_claude_dir().
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
FreshnessResult with is_fresh, reason, and age_hours.
|
|
83
|
+
"""
|
|
84
|
+
context_path = _get_context_path()
|
|
85
|
+
|
|
86
|
+
if not context_path.exists():
|
|
87
|
+
logger.info("project-context.json not found at %s", context_path)
|
|
88
|
+
return FreshnessResult(is_fresh=False, reason="missing", age_hours=0.0)
|
|
89
|
+
|
|
90
|
+
# Determine effective threshold: env var > context file > default
|
|
91
|
+
effective_hours = _get_effective_threshold()
|
|
92
|
+
ctx_hours = _read_staleness_from_context(context_path)
|
|
93
|
+
if ctx_hours and not os.environ.get("GAIA_SCAN_STALENESS_HOURS"):
|
|
94
|
+
effective_hours = ctx_hours
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Try metadata.scan_config.last_scan first (more accurate)
|
|
98
|
+
with open(context_path, "r") as f:
|
|
99
|
+
data = json.load(f)
|
|
100
|
+
last_scan = data.get("metadata", {}).get("scan_config", {}).get("last_scan")
|
|
101
|
+
|
|
102
|
+
if last_scan:
|
|
103
|
+
scan_dt = datetime.fromisoformat(last_scan)
|
|
104
|
+
now = datetime.now(timezone.utc)
|
|
105
|
+
age = now - scan_dt
|
|
106
|
+
age_hours = age.total_seconds() / 3600.0
|
|
107
|
+
threshold = timedelta(hours=effective_hours)
|
|
108
|
+
|
|
109
|
+
if age > threshold:
|
|
110
|
+
logger.info(
|
|
111
|
+
"project-context.json is stale (last_scan age: %s, threshold: %sh)",
|
|
112
|
+
age,
|
|
113
|
+
effective_hours,
|
|
114
|
+
)
|
|
115
|
+
return FreshnessResult(
|
|
116
|
+
is_fresh=False, reason="stale", age_hours=age_hours
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
logger.debug("project-context.json is fresh (last_scan age: %s)", age)
|
|
120
|
+
return FreshnessResult(
|
|
121
|
+
is_fresh=True, reason="fresh", age_hours=age_hours
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Fallback: use file mtime
|
|
125
|
+
mtime = datetime.fromtimestamp(context_path.stat().st_mtime)
|
|
126
|
+
age = datetime.now() - mtime
|
|
127
|
+
age_hours = age.total_seconds() / 3600.0
|
|
128
|
+
threshold = timedelta(hours=effective_hours)
|
|
129
|
+
|
|
130
|
+
if age > threshold:
|
|
131
|
+
logger.info(
|
|
132
|
+
"project-context.json is stale (mtime age: %s, threshold: %sh)",
|
|
133
|
+
age,
|
|
134
|
+
effective_hours,
|
|
135
|
+
)
|
|
136
|
+
return FreshnessResult(
|
|
137
|
+
is_fresh=False, reason="stale", age_hours=age_hours
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
logger.debug("project-context.json is fresh (mtime age: %s)", age)
|
|
141
|
+
return FreshnessResult(is_fresh=True, reason="fresh", age_hours=age_hours)
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.warning("Error checking context freshness: %s", e)
|
|
145
|
+
return FreshnessResult(is_fresh=False, reason="error", age_hours=0.0)
|