@jaguilar87/gaia 5.0.0-rc.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 +33 -0
- package/.claude-plugin/plugin.json +26 -0
- package/ARCHITECTURE.md +335 -0
- package/CHANGELOG.md +1298 -0
- package/CODE_OF_CONDUCT.md +11 -0
- package/CONTRIBUTING.md +146 -0
- package/INSTALL.md +436 -0
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/SECURITY.md +47 -0
- package/agents/README.md +78 -0
- package/agents/cloud-troubleshooter.md +73 -0
- package/agents/developer.md +65 -0
- package/agents/gaia-operator.md +64 -0
- package/agents/gaia-orchestrator.md +111 -0
- package/agents/gaia-planner.md +53 -0
- package/agents/gaia-system.md +71 -0
- package/agents/gitops-operator.md +61 -0
- package/agents/terraform-architect.md +63 -0
- package/bin/README.md +106 -0
- package/bin/cli/__init__.py +1 -0
- package/bin/cli/approvals.py +740 -0
- package/bin/cli/cleanup.py +562 -0
- package/bin/cli/context.py +283 -0
- package/bin/cli/doctor.py +651 -0
- package/bin/cli/history.py +305 -0
- package/bin/cli/memory.py +483 -0
- package/bin/cli/metrics.py +1068 -0
- package/bin/cli/plans.py +515 -0
- package/bin/cli/status.py +302 -0
- package/bin/cli/update.py +382 -0
- package/bin/gaia +112 -0
- package/bin/gaia-cleanup.js +531 -0
- package/bin/gaia-doctor.js +635 -0
- package/bin/gaia-evidence +126 -0
- package/bin/gaia-history.js +251 -0
- package/bin/gaia-metrics.js +1278 -0
- package/bin/gaia-review.js +269 -0
- package/bin/gaia-scan +44 -0
- package/bin/gaia-scan.py +589 -0
- package/bin/gaia-skills-diagnose.js +929 -0
- package/bin/gaia-status.js +278 -0
- package/bin/gaia-uninstall.js +111 -0
- package/bin/gaia-update.js +919 -0
- package/bin/pre-publish-validate.js +610 -0
- package/bin/python-detect.js +60 -0
- package/bin/validate-sandbox.sh +601 -0
- package/commands/README.md +64 -0
- package/commands/gaia.md +37 -0
- package/commands/scan-project.md +67 -0
- package/config/README.md +71 -0
- package/config/cloud/aws.json +134 -0
- package/config/cloud/gcp.json +139 -0
- package/config/context-contracts.json +158 -0
- package/config/crons-schema.md +81 -0
- package/config/git_standards.json +72 -0
- package/config/surface-routing.json +417 -0
- package/config/universal-rules.json +102 -0
- package/dist/gaia-ops/.claude-plugin/plugin.json +24 -0
- package/dist/gaia-ops/README.md +80 -0
- package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
- package/dist/gaia-ops/agents/developer.md +65 -0
- package/dist/gaia-ops/agents/gaia-operator.md +64 -0
- package/dist/gaia-ops/agents/gaia-orchestrator.md +111 -0
- package/dist/gaia-ops/agents/gaia-planner.md +53 -0
- package/dist/gaia-ops/agents/gaia-system.md +71 -0
- package/dist/gaia-ops/agents/gitops-operator.md +61 -0
- package/dist/gaia-ops/agents/terraform-architect.md +63 -0
- package/dist/gaia-ops/commands/gaia.md +37 -0
- package/dist/gaia-ops/config/README.md +71 -0
- package/dist/gaia-ops/config/cloud/aws.json +134 -0
- package/dist/gaia-ops/config/cloud/gcp.json +139 -0
- package/dist/gaia-ops/config/context-contracts.json +158 -0
- package/dist/gaia-ops/config/crons-schema.md +81 -0
- package/dist/gaia-ops/config/git_standards.json +72 -0
- package/dist/gaia-ops/config/surface-routing.json +417 -0
- package/dist/gaia-ops/config/universal-rules.json +102 -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 +1890 -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 +192 -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 +120 -0
- package/dist/gaia-ops/hooks/modules/agents/state_tracker.py +267 -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 +611 -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/agentic_loop_detector.py +165 -0
- package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +218 -0
- package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +558 -0
- package/dist/gaia-ops/hooks/modules/context/context_writer.py +530 -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 +577 -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/memory/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +216 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +122 -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 +120 -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 +1638 -0
- package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +222 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +595 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/command_semantics.py +181 -0
- package/dist/gaia-ops/hooks/modules/security/composition_rules.py +547 -0
- package/dist/gaia-ops/hooks/modules/security/flag_classifiers.py +873 -0
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +1131 -0
- package/dist/gaia-ops/hooks/modules/security/network_hosts.py +481 -0
- package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-ops/hooks/modules/security/shell_unwrapper.py +165 -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/pending_scanner.py +174 -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 +160 -0
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-ops/hooks/modules/session/session_registry.py +333 -0
- package/dist/gaia-ops/hooks/modules/tools/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +1008 -0
- package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +231 -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/stage_decomposer.py +315 -0
- package/dist/gaia-ops/hooks/modules/tools/task_validator.py +294 -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_compact.py +60 -0
- package/dist/gaia-ops/hooks/pre_tool_use.py +413 -0
- package/dist/gaia-ops/hooks/session_end_hook.py +77 -0
- package/dist/gaia-ops/hooks/session_start.py +81 -0
- package/dist/gaia-ops/hooks/stop_hook.py +70 -0
- package/dist/gaia-ops/hooks/subagent_start.py +71 -0
- package/dist/gaia-ops/hooks/subagent_stop.py +295 -0
- package/dist/gaia-ops/hooks/task_completed.py +70 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +246 -0
- package/dist/gaia-ops/settings.json +72 -0
- package/dist/gaia-ops/skills/README.md +158 -0
- package/dist/gaia-ops/skills/agent-creation/SKILL.md +87 -0
- package/dist/gaia-ops/skills/agent-creation/examples.md +170 -0
- package/dist/gaia-ops/skills/agent-creation/reference.md +191 -0
- package/dist/gaia-ops/skills/agent-protocol/SKILL.md +93 -0
- package/dist/gaia-ops/skills/agent-protocol/examples.md +223 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +69 -0
- package/dist/gaia-ops/skills/agentic-loop/SKILL.md +80 -0
- package/dist/gaia-ops/skills/agentic-loop/reference.md +378 -0
- package/dist/gaia-ops/skills/blog-writing/SKILL.md +98 -0
- package/dist/gaia-ops/skills/blog-writing/reference.md +130 -0
- package/dist/gaia-ops/skills/brief-spec/SKILL.md +185 -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 +87 -0
- package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
- package/dist/gaia-ops/skills/developer-patterns/SKILL.md +50 -0
- package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
- package/dist/gaia-ops/skills/execution/SKILL.md +99 -0
- package/dist/gaia-ops/skills/fast-queries/SKILL.md +43 -0
- package/dist/gaia-ops/skills/gaia-compact/SKILL.md +74 -0
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +108 -0
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +395 -0
- package/dist/gaia-ops/skills/gaia-planner/SKILL.md +37 -0
- package/dist/gaia-ops/skills/gaia-planner/reference.md +107 -0
- package/dist/gaia-ops/skills/gaia-release/SKILL.md +85 -0
- package/dist/gaia-ops/skills/gaia-release/reference.md +92 -0
- package/dist/gaia-ops/skills/gaia-self-check/SKILL.md +114 -0
- package/dist/gaia-ops/skills/gaia-self-check/reference.md +453 -0
- package/dist/gaia-ops/skills/gaia-verify/SKILL.md +77 -0
- package/dist/gaia-ops/skills/gaia-verify/reference.md +80 -0
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +47 -0
- package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +60 -0
- package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
- package/dist/gaia-ops/skills/gmail-policy/SKILL.md +200 -0
- package/dist/gaia-ops/skills/gmail-policy/reference.md +150 -0
- package/dist/gaia-ops/skills/gmail-triage/SKILL.md +100 -0
- package/dist/gaia-ops/skills/gws-setup/SKILL.md +99 -0
- package/dist/gaia-ops/skills/gws-setup/reference.md +73 -0
- package/dist/gaia-ops/skills/investigation/SKILL.md +100 -0
- package/dist/gaia-ops/skills/memory-curation/SKILL.md +83 -0
- package/dist/gaia-ops/skills/memory-search/SKILL.md +88 -0
- package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +160 -0
- package/dist/gaia-ops/skills/orchestrator-approval/reference.md +174 -0
- package/dist/gaia-ops/skills/pending-approvals/SKILL.md +72 -0
- package/dist/gaia-ops/skills/pending-approvals/reference.md +214 -0
- package/dist/gaia-ops/skills/readme-writing/SKILL.md +71 -0
- package/dist/gaia-ops/skills/readme-writing/reference.md +188 -0
- package/dist/gaia-ops/skills/reference.md +135 -0
- package/dist/gaia-ops/skills/request-approval/SKILL.md +140 -0
- package/dist/gaia-ops/skills/request-approval/examples.md +140 -0
- package/dist/gaia-ops/skills/request-approval/reference.md +57 -0
- package/dist/gaia-ops/skills/schedule-task/SKILL.md +64 -0
- package/dist/gaia-ops/skills/schedule-task/reference.md +233 -0
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +141 -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/session-reflection/SKILL.md +69 -0
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +92 -0
- package/dist/gaia-ops/skills/skill-creation/reference.md +29 -0
- package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +89 -0
- package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
- package/dist/gaia-ops/tools/__init__.py +9 -0
- package/dist/gaia-ops/tools/agentic-loop/decide-status.py +210 -0
- package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +106 -0
- package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +221 -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 +721 -0
- package/dist/gaia-ops/tools/context/context_section_reader.py +342 -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 +264 -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/backfill_fts5.py +107 -0
- package/dist/gaia-ops/tools/memory/conflict_detector.py +295 -0
- package/dist/gaia-ops/tools/memory/episodic.py +1210 -0
- package/dist/gaia-ops/tools/memory/git_invalidator.py +262 -0
- package/dist/gaia-ops/tools/memory/paths.py +102 -0
- package/dist/gaia-ops/tools/memory/scoring.py +193 -0
- package/dist/gaia-ops/tools/memory/search_store.py +375 -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 +349 -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 +686 -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 +270 -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 +24 -0
- package/dist/gaia-security/README.md +90 -0
- package/dist/gaia-security/config/universal-rules.json +102 -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 +1890 -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 +113 -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 +120 -0
- package/dist/gaia-security/hooks/modules/agents/state_tracker.py +267 -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 +611 -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/agentic_loop_detector.py +165 -0
- package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +218 -0
- package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-security/hooks/modules/context/context_injector.py +558 -0
- package/dist/gaia-security/hooks/modules/context/context_writer.py +530 -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 +577 -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/memory/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/memory/episode_writer.py +216 -0
- package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +122 -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 +120 -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 +1638 -0
- package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-security/hooks/modules/security/approval_scopes.py +222 -0
- package/dist/gaia-security/hooks/modules/security/blocked_commands.py +595 -0
- package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/dist/gaia-security/hooks/modules/security/command_semantics.py +181 -0
- package/dist/gaia-security/hooks/modules/security/composition_rules.py +547 -0
- package/dist/gaia-security/hooks/modules/security/flag_classifiers.py +873 -0
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +1131 -0
- package/dist/gaia-security/hooks/modules/security/network_hosts.py +481 -0
- package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-security/hooks/modules/security/shell_unwrapper.py +165 -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/pending_scanner.py +174 -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 +160 -0
- package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-security/hooks/modules/session/session_registry.py +333 -0
- package/dist/gaia-security/hooks/modules/tools/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +1008 -0
- package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +231 -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/stage_decomposer.py +315 -0
- package/dist/gaia-security/hooks/modules/tools/task_validator.py +294 -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 +413 -0
- package/dist/gaia-security/hooks/session_end_hook.py +77 -0
- package/dist/gaia-security/hooks/session_start.py +81 -0
- package/dist/gaia-security/hooks/stop_hook.py +70 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +246 -0
- package/dist/gaia-security/settings.json +58 -0
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +100 -0
- package/hooks/adapters/__init__.py +52 -0
- package/hooks/adapters/base.py +219 -0
- package/hooks/adapters/channel.py +17 -0
- package/hooks/adapters/claude_code.py +1890 -0
- package/hooks/adapters/types.py +194 -0
- package/hooks/adapters/utils.py +25 -0
- package/hooks/elicitation_result.py +179 -0
- package/hooks/hooks.json +84 -0
- package/hooks/modules/README.md +189 -0
- package/hooks/modules/__init__.py +15 -0
- package/hooks/modules/agents/__init__.py +29 -0
- package/hooks/modules/agents/contract_validator.py +647 -0
- package/hooks/modules/agents/response_contract.py +496 -0
- package/hooks/modules/agents/skill_injection_verifier.py +120 -0
- package/hooks/modules/agents/state_tracker.py +267 -0
- package/hooks/modules/agents/task_info_builder.py +74 -0
- package/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/hooks/modules/agents/transcript_reader.py +152 -0
- package/hooks/modules/audit/__init__.py +28 -0
- package/hooks/modules/audit/event_detector.py +168 -0
- package/hooks/modules/audit/logger.py +131 -0
- package/hooks/modules/audit/metrics.py +134 -0
- package/hooks/modules/audit/workflow_auditor.py +611 -0
- package/hooks/modules/audit/workflow_recorder.py +296 -0
- package/hooks/modules/context/__init__.py +11 -0
- package/hooks/modules/context/agentic_loop_detector.py +165 -0
- package/hooks/modules/context/anchor_tracker.py +317 -0
- package/hooks/modules/context/compact_context_builder.py +218 -0
- package/hooks/modules/context/context_freshness.py +145 -0
- package/hooks/modules/context/context_injector.py +558 -0
- package/hooks/modules/context/context_writer.py +530 -0
- package/hooks/modules/context/contracts_loader.py +161 -0
- package/hooks/modules/core/__init__.py +40 -0
- package/hooks/modules/core/hook_entry.py +78 -0
- package/hooks/modules/core/paths.py +160 -0
- package/hooks/modules/core/plugin_mode.py +149 -0
- package/hooks/modules/core/plugin_setup.py +577 -0
- package/hooks/modules/core/state.py +179 -0
- package/hooks/modules/core/stdin.py +24 -0
- package/hooks/modules/events/__init__.py +1 -0
- package/hooks/modules/events/event_writer.py +210 -0
- package/hooks/modules/evidence/__init__.py +34 -0
- package/hooks/modules/evidence/assertions.py +137 -0
- package/hooks/modules/evidence/index_writer.py +57 -0
- package/hooks/modules/evidence/loader.py +126 -0
- package/hooks/modules/evidence/runner.py +241 -0
- package/hooks/modules/memory/__init__.py +8 -0
- package/hooks/modules/memory/episode_writer.py +216 -0
- package/hooks/modules/orchestrator/__init__.py +1 -0
- package/hooks/modules/orchestrator/delegate_mode.py +122 -0
- package/hooks/modules/scanning/__init__.py +8 -0
- package/hooks/modules/scanning/scan_trigger.py +84 -0
- package/hooks/modules/security/__init__.py +120 -0
- package/hooks/modules/security/approval_cleanup.py +87 -0
- package/hooks/modules/security/approval_constants.py +23 -0
- package/hooks/modules/security/approval_grants.py +1638 -0
- package/hooks/modules/security/approval_messages.py +71 -0
- package/hooks/modules/security/approval_scopes.py +222 -0
- package/hooks/modules/security/blocked_commands.py +595 -0
- package/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/hooks/modules/security/command_semantics.py +181 -0
- package/hooks/modules/security/composition_rules.py +547 -0
- package/hooks/modules/security/flag_classifiers.py +873 -0
- package/hooks/modules/security/gitops_validator.py +179 -0
- package/hooks/modules/security/mutative_verbs.py +1131 -0
- package/hooks/modules/security/network_hosts.py +481 -0
- package/hooks/modules/security/prompt_validator.py +40 -0
- package/hooks/modules/security/shell_unwrapper.py +165 -0
- package/hooks/modules/security/tiers.py +196 -0
- package/hooks/modules/session/__init__.py +10 -0
- package/hooks/modules/session/pending_scanner.py +174 -0
- package/hooks/modules/session/session_context_writer.py +100 -0
- package/hooks/modules/session/session_event_injector.py +160 -0
- package/hooks/modules/session/session_manager.py +31 -0
- package/hooks/modules/session/session_registry.py +333 -0
- package/hooks/modules/tools/__init__.py +29 -0
- package/hooks/modules/tools/bash_validator.py +1008 -0
- package/hooks/modules/tools/cloud_pipe_validator.py +231 -0
- package/hooks/modules/tools/hook_response.py +55 -0
- package/hooks/modules/tools/shell_parser.py +227 -0
- package/hooks/modules/tools/stage_decomposer.py +315 -0
- package/hooks/modules/tools/task_validator.py +294 -0
- package/hooks/modules/validation/__init__.py +23 -0
- package/hooks/modules/validation/commit_validator.py +380 -0
- package/hooks/post_compact.py +43 -0
- package/hooks/post_tool_use.py +54 -0
- package/hooks/pre_compact.py +60 -0
- package/hooks/pre_tool_use.py +413 -0
- package/hooks/session_end_hook.py +77 -0
- package/hooks/session_start.py +81 -0
- package/hooks/stop_hook.py +70 -0
- package/hooks/subagent_start.py +71 -0
- package/hooks/subagent_stop.py +295 -0
- package/hooks/task_completed.py +70 -0
- package/hooks/user_prompt_submit.py +246 -0
- package/index.js +83 -0
- package/package.json +103 -0
- package/pyproject.toml +32 -0
- package/skills/README.md +158 -0
- package/skills/agent-creation/SKILL.md +87 -0
- package/skills/agent-creation/examples.md +170 -0
- package/skills/agent-creation/reference.md +191 -0
- package/skills/agent-protocol/SKILL.md +93 -0
- package/skills/agent-protocol/examples.md +223 -0
- package/skills/agent-response/SKILL.md +69 -0
- package/skills/agentic-loop/SKILL.md +80 -0
- package/skills/agentic-loop/reference.md +378 -0
- package/skills/blog-writing/SKILL.md +98 -0
- package/skills/blog-writing/reference.md +130 -0
- package/skills/brief-spec/SKILL.md +185 -0
- package/skills/command-execution/SKILL.md +64 -0
- package/skills/command-execution/reference.md +83 -0
- package/skills/context-updater/SKILL.md +87 -0
- package/skills/context-updater/examples.md +71 -0
- package/skills/developer-patterns/SKILL.md +50 -0
- package/skills/developer-patterns/reference.md +112 -0
- package/skills/execution/SKILL.md +99 -0
- package/skills/fast-queries/SKILL.md +43 -0
- package/skills/gaia-compact/SKILL.md +74 -0
- package/skills/gaia-patterns/SKILL.md +108 -0
- package/skills/gaia-patterns/reference.md +395 -0
- package/skills/gaia-planner/SKILL.md +37 -0
- package/skills/gaia-planner/reference.md +107 -0
- package/skills/gaia-release/SKILL.md +85 -0
- package/skills/gaia-release/reference.md +92 -0
- package/skills/gaia-self-check/SKILL.md +114 -0
- package/skills/gaia-self-check/reference.md +453 -0
- package/skills/gaia-verify/SKILL.md +77 -0
- package/skills/gaia-verify/reference.md +80 -0
- package/skills/git-conventions/SKILL.md +47 -0
- package/skills/gitops-patterns/SKILL.md +60 -0
- package/skills/gitops-patterns/reference.md +183 -0
- package/skills/gmail-policy/SKILL.md +200 -0
- package/skills/gmail-policy/reference.md +150 -0
- package/skills/gmail-triage/SKILL.md +100 -0
- package/skills/gws-setup/SKILL.md +99 -0
- package/skills/gws-setup/reference.md +73 -0
- package/skills/investigation/SKILL.md +100 -0
- package/skills/memory-curation/SKILL.md +83 -0
- package/skills/memory-search/SKILL.md +88 -0
- package/skills/orchestrator-approval/SKILL.md +160 -0
- package/skills/orchestrator-approval/reference.md +174 -0
- package/skills/pending-approvals/SKILL.md +72 -0
- package/skills/pending-approvals/reference.md +214 -0
- package/skills/readme-writing/SKILL.md +71 -0
- package/skills/readme-writing/reference.md +188 -0
- package/skills/reference.md +135 -0
- package/skills/request-approval/SKILL.md +140 -0
- package/skills/request-approval/examples.md +140 -0
- package/skills/request-approval/reference.md +57 -0
- package/skills/schedule-task/SKILL.md +64 -0
- package/skills/schedule-task/reference.md +233 -0
- package/skills/security-tiers/SKILL.md +141 -0
- package/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/skills/security-tiers/reference.md +39 -0
- package/skills/session-reflection/SKILL.md +69 -0
- package/skills/skill-creation/SKILL.md +92 -0
- package/skills/skill-creation/reference.md +29 -0
- package/skills/terraform-patterns/SKILL.md +89 -0
- package/skills/terraform-patterns/reference.md +93 -0
- package/templates/README.md +69 -0
- package/templates/managed-settings.template.json +43 -0
- package/tools/__init__.py +9 -0
- package/tools/agentic-loop/decide-status.py +210 -0
- package/tools/agentic-loop/parse-metric.py +106 -0
- package/tools/agentic-loop/record-iteration.py +221 -0
- package/tools/context/README.md +132 -0
- package/tools/context/__init__.py +42 -0
- package/tools/context/_paths.py +20 -0
- package/tools/context/context_provider.py +721 -0
- package/tools/context/context_section_reader.py +342 -0
- package/tools/context/deep_merge.py +159 -0
- package/tools/context/pending_updates.py +760 -0
- package/tools/context/surface_router.py +278 -0
- package/tools/fast-queries/README.md +65 -0
- package/tools/fast-queries/__init__.py +30 -0
- package/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/tools/fast-queries/run_triage.sh +59 -0
- package/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/tools/gaia_simulator/__init__.py +33 -0
- package/tools/gaia_simulator/cli.py +354 -0
- package/tools/gaia_simulator/extractor.py +457 -0
- package/tools/gaia_simulator/reporter.py +258 -0
- package/tools/gaia_simulator/routing_simulator.py +334 -0
- package/tools/gaia_simulator/runner.py +539 -0
- package/tools/gaia_simulator/skills_mapper.py +264 -0
- package/tools/memory/README.md +0 -0
- package/tools/memory/__init__.py +20 -0
- package/tools/memory/backfill_fts5.py +107 -0
- package/tools/memory/conflict_detector.py +295 -0
- package/tools/memory/episodic.py +1210 -0
- package/tools/memory/git_invalidator.py +262 -0
- package/tools/memory/paths.py +102 -0
- package/tools/memory/scoring.py +193 -0
- package/tools/memory/search_store.py +375 -0
- package/tools/persist_transcript_analysis.py +85 -0
- package/tools/review/__init__.py +1 -0
- package/tools/review/review_engine.py +157 -0
- package/tools/scan/__init__.py +35 -0
- package/tools/scan/config.py +247 -0
- package/tools/scan/merge.py +212 -0
- package/tools/scan/orchestrator.py +549 -0
- package/tools/scan/registry.py +127 -0
- package/tools/scan/scanners/__init__.py +18 -0
- package/tools/scan/scanners/base.py +137 -0
- package/tools/scan/scanners/environment.py +349 -0
- package/tools/scan/scanners/git.py +570 -0
- package/tools/scan/scanners/infrastructure.py +875 -0
- package/tools/scan/scanners/orchestration.py +600 -0
- package/tools/scan/scanners/stack.py +1085 -0
- package/tools/scan/scanners/tools.py +260 -0
- package/tools/scan/setup.py +686 -0
- package/tools/scan/tests/__init__.py +1 -0
- package/tools/scan/tests/conftest.py +796 -0
- package/tools/scan/tests/test_environment.py +323 -0
- package/tools/scan/tests/test_git.py +419 -0
- package/tools/scan/tests/test_infrastructure.py +382 -0
- package/tools/scan/tests/test_integration.py +920 -0
- package/tools/scan/tests/test_merge.py +269 -0
- package/tools/scan/tests/test_orchestration.py +304 -0
- package/tools/scan/tests/test_stack.py +604 -0
- package/tools/scan/tests/test_tools.py +349 -0
- package/tools/scan/ui.py +624 -0
- package/tools/scan/verify.py +270 -0
- package/tools/scan/walk.py +118 -0
- package/tools/scan/workspace.py +85 -0
- package/tools/validation/README.md +244 -0
- package/tools/validation/__init__.py +17 -0
- package/tools/validation/approval_gate.py +321 -0
- package/tools/validation/validate_skills.py +189 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context anchor hit tracking for project context effectiveness measurement.
|
|
3
|
+
|
|
4
|
+
Extracts "anchors" (paths, names, IDs) from injected project context and checks
|
|
5
|
+
whether the agent's early tool calls reference them. This measures whether agents
|
|
6
|
+
use injected context as search anchors versus discovering on their own.
|
|
7
|
+
|
|
8
|
+
Provides:
|
|
9
|
+
- extract_anchors(): Extract searchable anchors from a context payload
|
|
10
|
+
- save_anchors(): Persist anchors to a session-scoped temp file
|
|
11
|
+
- load_anchors(): Load persisted anchors for a session
|
|
12
|
+
- extract_tool_calls_from_transcript(): Parse early tool calls from JSONL transcript
|
|
13
|
+
- compute_anchor_hits(): Compare tool call args against anchors
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import re
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional, Set
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# How many early tool calls to check
|
|
25
|
+
MAX_TOOL_CALLS_TO_CHECK = 5
|
|
26
|
+
|
|
27
|
+
# Tool types that have inspectable path/keyword arguments
|
|
28
|
+
TRACKABLE_TOOLS = {"Glob", "Grep", "Read", "Bash"}
|
|
29
|
+
|
|
30
|
+
# Minimum anchor length to avoid false-positive matches on short strings
|
|
31
|
+
MIN_ANCHOR_LENGTH = 4
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _anchors_dir() -> Path:
|
|
35
|
+
"""Return the directory for anchor temp files."""
|
|
36
|
+
return Path("/tmp/gaia-context-anchors")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def extract_anchors(context_payload: Dict[str, Any]) -> Set[str]:
|
|
40
|
+
"""Extract searchable anchor strings from a context payload.
|
|
41
|
+
|
|
42
|
+
Walks the project knowledge sections and collects values from fields that
|
|
43
|
+
are likely to appear in agent tool calls: paths, names, IDs, clusters,
|
|
44
|
+
regions, namespaces, service accounts.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
context_payload: The full context JSON payload injected into agent prompt.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Set of anchor strings (paths, names, identifiers).
|
|
51
|
+
"""
|
|
52
|
+
anchors: Set[str] = set()
|
|
53
|
+
contract = context_payload.get("project_knowledge", {})
|
|
54
|
+
|
|
55
|
+
# Anchor-worthy field name patterns
|
|
56
|
+
anchor_fields = re.compile(
|
|
57
|
+
r"(path|name|cluster|project|region|namespace|service|image|"
|
|
58
|
+
r"base_path|config_path|module_path|repository|bucket|sa$|"
|
|
59
|
+
r"service_account|pod_name|terragrunt_path)",
|
|
60
|
+
re.IGNORECASE,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _walk(obj: Any, depth: int = 0) -> None:
|
|
64
|
+
if depth > 10:
|
|
65
|
+
return
|
|
66
|
+
if isinstance(obj, dict):
|
|
67
|
+
for key, value in obj.items():
|
|
68
|
+
if isinstance(value, str) and value and anchor_fields.search(key):
|
|
69
|
+
# Normalize: strip leading ./ for path matching
|
|
70
|
+
clean = value.lstrip("./")
|
|
71
|
+
if len(clean) >= MIN_ANCHOR_LENGTH:
|
|
72
|
+
anchors.add(clean)
|
|
73
|
+
elif isinstance(value, (dict, list)):
|
|
74
|
+
_walk(value, depth + 1)
|
|
75
|
+
elif isinstance(obj, list):
|
|
76
|
+
for item in obj:
|
|
77
|
+
_walk(item, depth + 1)
|
|
78
|
+
|
|
79
|
+
_walk(contract)
|
|
80
|
+
|
|
81
|
+
# Also extract from top-level metadata
|
|
82
|
+
metadata = context_payload.get("metadata", {})
|
|
83
|
+
for key in ("project_id", "cluster_name", "region"):
|
|
84
|
+
val = metadata.get(key)
|
|
85
|
+
if isinstance(val, str) and len(val) >= MIN_ANCHOR_LENGTH:
|
|
86
|
+
anchors.add(val)
|
|
87
|
+
|
|
88
|
+
return anchors
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def save_anchors(session_id: str, agent_type: str, anchors: Set[str]) -> Optional[Path]:
|
|
92
|
+
"""Persist anchors to a session+agent-scoped temp file.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
session_id: Current session identifier.
|
|
96
|
+
agent_type: Agent name (e.g. "terraform-architect").
|
|
97
|
+
anchors: Set of anchor strings to save.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Path to the saved file, or None on failure.
|
|
101
|
+
"""
|
|
102
|
+
if not anchors:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
anchor_dir = _anchors_dir()
|
|
107
|
+
anchor_dir.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
|
|
109
|
+
safe_session = re.sub(r"[^a-zA-Z0-9_-]", "_", session_id or "unknown")[:32]
|
|
110
|
+
safe_agent = re.sub(r"[^a-zA-Z0-9_-]", "_", agent_type or "unknown")[:32]
|
|
111
|
+
anchor_file = anchor_dir / f"{safe_session}-{safe_agent}.json"
|
|
112
|
+
|
|
113
|
+
anchor_file.write_text(json.dumps(sorted(anchors)))
|
|
114
|
+
logger.debug(
|
|
115
|
+
"Saved %d anchors for %s/%s -> %s",
|
|
116
|
+
len(anchors), session_id, agent_type, anchor_file,
|
|
117
|
+
)
|
|
118
|
+
return anchor_file
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.debug("Failed to save anchors: %s", e)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load_anchors(session_id: str, agent_type: str) -> Set[str]:
|
|
125
|
+
"""Load persisted anchors for a session+agent.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
session_id: Current session identifier.
|
|
129
|
+
agent_type: Agent name.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Set of anchor strings, or empty set if not found.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
safe_session = re.sub(r"[^a-zA-Z0-9_-]", "_", session_id or "unknown")[:32]
|
|
136
|
+
safe_agent = re.sub(r"[^a-zA-Z0-9_-]", "_", agent_type or "unknown")[:32]
|
|
137
|
+
anchor_file = _anchors_dir() / f"{safe_session}-{safe_agent}.json"
|
|
138
|
+
|
|
139
|
+
if not anchor_file.exists():
|
|
140
|
+
return set()
|
|
141
|
+
|
|
142
|
+
data = json.loads(anchor_file.read_text())
|
|
143
|
+
return set(data) if isinstance(data, list) else set()
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.debug("Failed to load anchors: %s", e)
|
|
146
|
+
return set()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def extract_tool_calls_from_transcript(
|
|
150
|
+
transcript_path: str,
|
|
151
|
+
max_calls: int = MAX_TOOL_CALLS_TO_CHECK,
|
|
152
|
+
) -> List[Dict[str, Any]]:
|
|
153
|
+
"""Extract the first N trackable tool calls from a Claude Code transcript JSONL.
|
|
154
|
+
|
|
155
|
+
Claude Code transcripts contain tool_use entries in the assistant messages
|
|
156
|
+
(content blocks with type "tool_use").
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
transcript_path: Path to the transcript JSONL file.
|
|
160
|
+
max_calls: Maximum number of tool calls to extract.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of dicts with keys: tool_name, arguments (dict), call_index (1-based).
|
|
164
|
+
"""
|
|
165
|
+
if not transcript_path:
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
path = Path(transcript_path).expanduser()
|
|
170
|
+
if not path.exists():
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
tool_calls: List[Dict[str, Any]] = []
|
|
174
|
+
call_index = 0
|
|
175
|
+
|
|
176
|
+
for line in path.read_text().strip().splitlines():
|
|
177
|
+
if not line.strip():
|
|
178
|
+
continue
|
|
179
|
+
if call_index >= max_calls:
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
entry = json.loads(line)
|
|
184
|
+
msg = entry.get("message", entry)
|
|
185
|
+
|
|
186
|
+
if msg.get("role") != "assistant":
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
content = msg.get("content", [])
|
|
190
|
+
if not isinstance(content, list):
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
for block in content:
|
|
194
|
+
if call_index >= max_calls:
|
|
195
|
+
break
|
|
196
|
+
if not isinstance(block, dict):
|
|
197
|
+
continue
|
|
198
|
+
if block.get("type") != "tool_use":
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
tool_name = block.get("name", "")
|
|
202
|
+
if tool_name not in TRACKABLE_TOOLS:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
call_index += 1
|
|
206
|
+
tool_calls.append({
|
|
207
|
+
"tool_name": tool_name,
|
|
208
|
+
"arguments": block.get("input", {}),
|
|
209
|
+
"call_index": call_index,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
except (json.JSONDecodeError, TypeError):
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
return tool_calls
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.debug("Failed to extract tool calls from transcript: %s", e)
|
|
219
|
+
return []
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _extract_searchable_text(tool_name: str, arguments: Dict[str, Any]) -> str:
|
|
223
|
+
"""Extract the searchable text from a tool call's arguments.
|
|
224
|
+
|
|
225
|
+
Returns a single string containing all path/keyword-relevant arguments
|
|
226
|
+
concatenated for substring matching.
|
|
227
|
+
"""
|
|
228
|
+
parts: List[str] = []
|
|
229
|
+
|
|
230
|
+
if tool_name == "Glob":
|
|
231
|
+
parts.append(arguments.get("pattern", ""))
|
|
232
|
+
parts.append(arguments.get("path", ""))
|
|
233
|
+
elif tool_name == "Grep":
|
|
234
|
+
parts.append(arguments.get("pattern", ""))
|
|
235
|
+
parts.append(arguments.get("path", ""))
|
|
236
|
+
parts.append(arguments.get("glob", ""))
|
|
237
|
+
elif tool_name == "Read":
|
|
238
|
+
parts.append(arguments.get("file_path", ""))
|
|
239
|
+
elif tool_name == "Bash":
|
|
240
|
+
parts.append(arguments.get("command", ""))
|
|
241
|
+
|
|
242
|
+
return " ".join(p for p in parts if p)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def compute_anchor_hits(
|
|
246
|
+
tool_calls: List[Dict[str, Any]],
|
|
247
|
+
anchors: Set[str],
|
|
248
|
+
) -> Dict[str, Any]:
|
|
249
|
+
"""Compare tool call arguments against known anchors.
|
|
250
|
+
|
|
251
|
+
For each tool call, checks if any anchor appears as a substring in the
|
|
252
|
+
tool's searchable arguments. This is a lightweight prefix/substring match.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
tool_calls: List from extract_tool_calls_from_transcript().
|
|
256
|
+
anchors: Set of anchor strings from extract_anchors().
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Dict with hit tracking data.
|
|
260
|
+
"""
|
|
261
|
+
if not tool_calls or not anchors:
|
|
262
|
+
return {
|
|
263
|
+
"total_checked": len(tool_calls),
|
|
264
|
+
"hits": 0,
|
|
265
|
+
"hit_rate": 0.0,
|
|
266
|
+
"details": [],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
details: List[Dict[str, Any]] = []
|
|
270
|
+
hits = 0
|
|
271
|
+
|
|
272
|
+
for call in tool_calls:
|
|
273
|
+
searchable = _extract_searchable_text(call["tool_name"], call["arguments"])
|
|
274
|
+
matched_anchor: Optional[str] = None
|
|
275
|
+
|
|
276
|
+
if searchable:
|
|
277
|
+
for anchor in anchors:
|
|
278
|
+
if anchor in searchable:
|
|
279
|
+
matched_anchor = anchor
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
is_hit = matched_anchor is not None
|
|
283
|
+
if is_hit:
|
|
284
|
+
hits += 1
|
|
285
|
+
|
|
286
|
+
details.append({
|
|
287
|
+
"call_index": call["call_index"],
|
|
288
|
+
"tool": call["tool_name"],
|
|
289
|
+
"anchor": matched_anchor,
|
|
290
|
+
"hit": is_hit,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
total = len(tool_calls)
|
|
294
|
+
return {
|
|
295
|
+
"total_checked": total,
|
|
296
|
+
"hits": hits,
|
|
297
|
+
"hit_rate": round(hits / total, 2) if total > 0 else 0.0,
|
|
298
|
+
"details": details,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def cleanup_anchors(session_id: str, agent_type: str) -> None:
|
|
303
|
+
"""Remove the anchor temp file after use.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
session_id: Current session identifier.
|
|
307
|
+
agent_type: Agent name.
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
safe_session = re.sub(r"[^a-zA-Z0-9_-]", "_", session_id or "unknown")[:32]
|
|
311
|
+
safe_agent = re.sub(r"[^a-zA-Z0-9_-]", "_", agent_type or "unknown")[:32]
|
|
312
|
+
anchor_file = _anchors_dir() / f"{safe_session}-{safe_agent}.json"
|
|
313
|
+
if anchor_file.exists():
|
|
314
|
+
anchor_file.unlink()
|
|
315
|
+
logger.debug("Cleaned up anchor file: %s", anchor_file)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.debug("Failed to cleanup anchors: %s", e)
|
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
"""Minimal post-compaction identity reminder.
|
|
64
|
+
|
|
65
|
+
Full identity lives in agents/gaia-orchestrator.md and is injected at
|
|
66
|
+
session start. This block only restores the core posture after context
|
|
67
|
+
compaction — it intentionally does NOT list specific agents because
|
|
68
|
+
the agent roster can change and a stale list causes drift.
|
|
69
|
+
"""
|
|
70
|
+
return (
|
|
71
|
+
"# Post-Compaction Context Refresh\n\n"
|
|
72
|
+
"You are the orchestrator. Dispatch work via Agent, resume agents via "
|
|
73
|
+
"SendMessage(to: agentId), get user approval via AskUserQuestion."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_activity_block(max_snapshots: int) -> str | None:
|
|
78
|
+
"""Build session activity summary from run-snapshots.jsonl."""
|
|
79
|
+
snapshots_path = (
|
|
80
|
+
get_plugin_data_dir() / "project-context" / "workflow-episodic-memory" / "run-snapshots.jsonl"
|
|
81
|
+
)
|
|
82
|
+
if not snapshots_path.exists():
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
lines = snapshots_path.read_text().splitlines()
|
|
87
|
+
# Take last N lines
|
|
88
|
+
recent = lines[-max_snapshots:] if len(lines) > max_snapshots else lines
|
|
89
|
+
|
|
90
|
+
entries = []
|
|
91
|
+
for line in recent:
|
|
92
|
+
if not line.strip():
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
snap = json.loads(line)
|
|
96
|
+
agent = snap.get("agent", "unknown")
|
|
97
|
+
status = snap.get("plan_status", "unknown")
|
|
98
|
+
prompt = snap.get("prompt", "")[:80]
|
|
99
|
+
cmd_count = snap.get("commands_executed_count", 0)
|
|
100
|
+
entries.append(f"- {agent} → {status} ({prompt}, {cmd_count} commands)")
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
if not entries:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
return "## Session Activity\n" + "\n".join(entries)
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.debug("Failed to build activity block (non-fatal): %s", e)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _build_anomalies_block(window_hours: int) -> str | None:
|
|
115
|
+
"""Build active anomalies summary from anomalies.jsonl."""
|
|
116
|
+
anomaly_path = (
|
|
117
|
+
get_plugin_data_dir() / "project-context" / "workflow-episodic-memory" / "anomalies.jsonl"
|
|
118
|
+
)
|
|
119
|
+
if not anomaly_path.exists():
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
lines = anomaly_path.read_text().splitlines()[-20:]
|
|
124
|
+
cutoff = datetime.now().timestamp() - (window_hours * 3600)
|
|
125
|
+
|
|
126
|
+
critical_types: list[str] = []
|
|
127
|
+
warning_types: list[str] = []
|
|
128
|
+
|
|
129
|
+
for line in lines:
|
|
130
|
+
if not line.strip():
|
|
131
|
+
continue
|
|
132
|
+
try:
|
|
133
|
+
entry = json.loads(line)
|
|
134
|
+
ts = entry.get("timestamp", "")
|
|
135
|
+
if ts:
|
|
136
|
+
try:
|
|
137
|
+
entry_time = datetime.fromisoformat(ts).timestamp()
|
|
138
|
+
if entry_time < cutoff:
|
|
139
|
+
continue
|
|
140
|
+
except (ValueError, TypeError):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
for anomaly in entry.get("anomalies", []):
|
|
144
|
+
severity = anomaly.get("severity", "")
|
|
145
|
+
atype = anomaly.get("type", "unknown")
|
|
146
|
+
if severity == "critical":
|
|
147
|
+
critical_types.append(atype)
|
|
148
|
+
elif severity == "warning":
|
|
149
|
+
warning_types.append(atype)
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
if not critical_types and not warning_types:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
parts = []
|
|
157
|
+
if critical_types:
|
|
158
|
+
unique = sorted(set(critical_types))
|
|
159
|
+
parts.append(f"- {len(critical_types)} critical: {', '.join(unique)}")
|
|
160
|
+
if warning_types:
|
|
161
|
+
unique = sorted(set(warning_types))
|
|
162
|
+
parts.append(f"- {len(warning_types)} warning: {', '.join(unique)}")
|
|
163
|
+
|
|
164
|
+
return "## Active Anomalies\n" + "\n".join(parts)
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.debug("Failed to build anomalies block (non-fatal): %s", e)
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _build_events_block(max_events: int) -> str | None:
|
|
172
|
+
"""Build recent events summary from session context.json."""
|
|
173
|
+
context_path = Path(".claude/session/active/context.json")
|
|
174
|
+
if not context_path.exists():
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
with open(context_path) as f:
|
|
179
|
+
context = json.load(f)
|
|
180
|
+
|
|
181
|
+
events = context.get("critical_events", [])
|
|
182
|
+
if not events:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
# Take last N events
|
|
186
|
+
recent = events[-max_events:]
|
|
187
|
+
|
|
188
|
+
lines = []
|
|
189
|
+
for event in recent:
|
|
190
|
+
etype = event.get("event_type", "")
|
|
191
|
+
ts = event.get("timestamp", "")[:16]
|
|
192
|
+
|
|
193
|
+
if etype == "git_commit":
|
|
194
|
+
msg = event.get("commit_message", "")
|
|
195
|
+
hash_val = event.get("commit_hash", "")[:7]
|
|
196
|
+
if hash_val and msg:
|
|
197
|
+
lines.append(f"- [{ts}] Commit {hash_val}: {msg}")
|
|
198
|
+
elif etype == "git_push":
|
|
199
|
+
branch = event.get("branch", "")
|
|
200
|
+
if branch:
|
|
201
|
+
lines.append(f"- [{ts}] Pushed to {branch}")
|
|
202
|
+
elif etype == "file_modifications":
|
|
203
|
+
count = event.get("modification_count", 0)
|
|
204
|
+
if count:
|
|
205
|
+
lines.append(f"- [{ts}] Modified {count} files")
|
|
206
|
+
elif etype == "infrastructure_change":
|
|
207
|
+
cmd = event.get("command", "")
|
|
208
|
+
if cmd:
|
|
209
|
+
lines.append(f"- [{ts}] Infrastructure: {cmd}")
|
|
210
|
+
|
|
211
|
+
if not lines:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
return "## Recent Events\n" + "\n".join(lines)
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.debug("Failed to build events block (non-fatal): %s", e)
|
|
218
|
+
return None
|
|
@@ -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)
|