@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,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workflow metrics capture and persistence.
|
|
3
|
+
|
|
4
|
+
Renamed from metrics_recorder.py for clarity.
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
- get_workflow_memory_dir(): Resolve workflow memory directory
|
|
8
|
+
- record(): Build metrics dict, write to JSONL
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from ..context.context_injector import build_context_telemetry_snapshot
|
|
19
|
+
from ..core.paths import get_plugin_data_dir
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_workflow_memory_dir() -> Path:
|
|
25
|
+
"""
|
|
26
|
+
Get workflow memory directory path.
|
|
27
|
+
|
|
28
|
+
Resolution order:
|
|
29
|
+
1. WORKFLOW_MEMORY_BASE_PATH env var (testing override)
|
|
30
|
+
2. CLAUDE_PLUGIN_DATA / project-context / workflow-episodic-memory
|
|
31
|
+
3. .claude/project-context/workflow-episodic-memory (backward compat)
|
|
32
|
+
"""
|
|
33
|
+
base_path = os.environ.get("WORKFLOW_MEMORY_BASE_PATH")
|
|
34
|
+
if base_path:
|
|
35
|
+
return Path(base_path) / "project-context" / "workflow-episodic-memory"
|
|
36
|
+
return get_plugin_data_dir() / "project-context" / "workflow-episodic-memory"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _append_jsonl(path: Path, payload: Dict[str, Any]) -> None:
|
|
40
|
+
"""Append one JSON record per line."""
|
|
41
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
with open(path, "a") as f:
|
|
43
|
+
f.write(json.dumps(payload) + "\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_frontmatter(text: str) -> Dict[str, Any]:
|
|
47
|
+
"""Parse simple markdown frontmatter without external dependencies."""
|
|
48
|
+
if not text.startswith("---"):
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
end = text.index("---", 3)
|
|
53
|
+
except ValueError:
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
frontmatter = text[3:end]
|
|
57
|
+
result: Dict[str, Any] = {}
|
|
58
|
+
current_key: Optional[str] = None
|
|
59
|
+
current_list: Optional[List[str]] = None
|
|
60
|
+
|
|
61
|
+
for line in frontmatter.splitlines():
|
|
62
|
+
stripped = line.strip()
|
|
63
|
+
if not stripped or stripped.startswith("#"):
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if stripped.startswith("- ") and current_key and current_list is not None:
|
|
67
|
+
current_list.append(stripped[2:].strip())
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if ":" in stripped:
|
|
71
|
+
if current_key and current_list is not None:
|
|
72
|
+
result[current_key] = current_list
|
|
73
|
+
|
|
74
|
+
key, _, value = stripped.partition(":")
|
|
75
|
+
key = key.strip()
|
|
76
|
+
value = value.strip()
|
|
77
|
+
|
|
78
|
+
if value:
|
|
79
|
+
result[key] = value
|
|
80
|
+
current_key = key
|
|
81
|
+
current_list = None
|
|
82
|
+
else:
|
|
83
|
+
current_key = key
|
|
84
|
+
current_list = []
|
|
85
|
+
|
|
86
|
+
if current_key and current_list is not None:
|
|
87
|
+
result[current_key] = current_list
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse_tools_field(value: Any) -> List[str]:
|
|
93
|
+
"""Normalize frontmatter tools into a list."""
|
|
94
|
+
if isinstance(value, list):
|
|
95
|
+
return [item for item in value if isinstance(item, str) and item.strip()]
|
|
96
|
+
if isinstance(value, str):
|
|
97
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _resolve_agent_file(agent_type: str) -> Optional[Path]:
|
|
102
|
+
"""Resolve the markdown definition for a project agent."""
|
|
103
|
+
if not agent_type:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
candidates = [
|
|
107
|
+
Path(".claude/agents") / f"{agent_type}.md",
|
|
108
|
+
Path(__file__).resolve().parents[3] / "agents" / f"{agent_type}.md",
|
|
109
|
+
]
|
|
110
|
+
for candidate in candidates:
|
|
111
|
+
if candidate.exists():
|
|
112
|
+
return candidate
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def load_agent_runtime_profile(agent_type: str) -> Dict[str, Any]:
|
|
117
|
+
"""Load runtime-default metadata from an agent definition file."""
|
|
118
|
+
agent_file = _resolve_agent_file(agent_type)
|
|
119
|
+
if not agent_file:
|
|
120
|
+
return {
|
|
121
|
+
"agent": agent_type or "unknown",
|
|
122
|
+
"model": "",
|
|
123
|
+
"tools": [],
|
|
124
|
+
"skills": [],
|
|
125
|
+
"skills_count": 0,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
frontmatter = _parse_frontmatter(agent_file.read_text())
|
|
129
|
+
skills = frontmatter.get("skills", [])
|
|
130
|
+
if not isinstance(skills, list):
|
|
131
|
+
skills = []
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"agent": agent_type or "unknown",
|
|
135
|
+
"model": frontmatter.get("model", ""),
|
|
136
|
+
"tools": _parse_tools_field(frontmatter.get("tools", [])),
|
|
137
|
+
"skills": skills,
|
|
138
|
+
"skills_count": len(skills),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def record_agent_skill_snapshot(
|
|
143
|
+
agent_type: str,
|
|
144
|
+
session_context: Optional[Dict[str, Any]] = None,
|
|
145
|
+
task_description: str = "",
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
"""Persist a historical snapshot of an agent's runtime defaults."""
|
|
148
|
+
session_context = session_context or {}
|
|
149
|
+
profile = load_agent_runtime_profile(agent_type)
|
|
150
|
+
snapshot = {
|
|
151
|
+
"timestamp": session_context.get("timestamp", datetime.now().isoformat()),
|
|
152
|
+
"session_id": session_context.get("session_id", ""),
|
|
153
|
+
"agent": profile.get("agent", agent_type or "unknown"),
|
|
154
|
+
"task_description": task_description[:200],
|
|
155
|
+
"model": profile.get("model", ""),
|
|
156
|
+
"tools": profile.get("tools", []),
|
|
157
|
+
"skills": profile.get("skills", []),
|
|
158
|
+
"skills_count": profile.get("skills_count", 0),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
_append_jsonl(get_workflow_memory_dir() / "agent-skills.jsonl", snapshot)
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
logger.debug("Could not persist agent skill snapshot: %s", exc)
|
|
165
|
+
|
|
166
|
+
return snapshot
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def record(
|
|
170
|
+
task_info: Dict[str, Any],
|
|
171
|
+
agent_output: str,
|
|
172
|
+
session_context: Dict[str, Any],
|
|
173
|
+
commands_executed: Optional[List[str]] = None,
|
|
174
|
+
context_update_result: Optional[Dict[str, Any]] = None,
|
|
175
|
+
anchor_hits: Optional[Dict[str, Any]] = None,
|
|
176
|
+
transcript_analysis: Optional[Any] = None,
|
|
177
|
+
compliance_result: Optional[Any] = None,
|
|
178
|
+
) -> Dict[str, Any]:
|
|
179
|
+
"""
|
|
180
|
+
Capture workflow execution metrics for analysis.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
task_info: Task metadata
|
|
184
|
+
agent_output: Output from agent execution
|
|
185
|
+
session_context: Current session context
|
|
186
|
+
commands_executed: List of commands the agent executed.
|
|
187
|
+
context_update_result: Result of context update processing.
|
|
188
|
+
anchor_hits: Context anchor hit data.
|
|
189
|
+
transcript_analysis: Optional TranscriptAnalysis from transcript_analyzer.
|
|
190
|
+
When provided, real token counts and tool metrics are added to the
|
|
191
|
+
metrics dict alongside the existing output_tokens_approx.
|
|
192
|
+
compliance_result: Optional ComplianceScore from transcript_analyzer.
|
|
193
|
+
When provided, compliance_score and compliance_grade are added.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dict with duration, exit_code, agent, tier, etc.
|
|
197
|
+
"""
|
|
198
|
+
# Duration cannot be reliably measured from within this hook because
|
|
199
|
+
# it fires only at agent completion (no start timestamp available).
|
|
200
|
+
duration_ms = None
|
|
201
|
+
|
|
202
|
+
# Use exit_code from task_info (derived from AGENT_STATUS block) instead
|
|
203
|
+
# of naive text matching which gives false positives on "No errors found".
|
|
204
|
+
exit_code = task_info.get("exit_code", 0)
|
|
205
|
+
|
|
206
|
+
# Approximate token count: 4 chars per token is a reliable heuristic for LLM output
|
|
207
|
+
output_tokens_approx = len(agent_output) // 4
|
|
208
|
+
|
|
209
|
+
commands_executed = commands_executed or []
|
|
210
|
+
context_update_result = context_update_result or {}
|
|
211
|
+
context_snapshot = build_context_telemetry_snapshot(
|
|
212
|
+
task_info.get("injected_context") or {}
|
|
213
|
+
)
|
|
214
|
+
default_skills_snapshot = load_agent_runtime_profile(task_info.get("agent", "unknown"))
|
|
215
|
+
|
|
216
|
+
metrics = {
|
|
217
|
+
"timestamp": session_context["timestamp"],
|
|
218
|
+
"session_id": session_context["session_id"],
|
|
219
|
+
"task_id": task_info.get("task_id", "unknown"),
|
|
220
|
+
"agent_id": task_info.get("agent_id", "unknown"),
|
|
221
|
+
"agent": task_info.get("agent", "unknown"),
|
|
222
|
+
"tier": task_info.get("tier", "T0"),
|
|
223
|
+
"duration_ms": duration_ms,
|
|
224
|
+
"exit_code": exit_code,
|
|
225
|
+
"plan_status": task_info.get("plan_status", ""),
|
|
226
|
+
"output_length": len(agent_output),
|
|
227
|
+
"output_tokens_approx": output_tokens_approx,
|
|
228
|
+
"tags": task_info.get("tags", []),
|
|
229
|
+
"prompt": task_info.get("description", ""), # Store for episodic
|
|
230
|
+
"commands_executed": commands_executed,
|
|
231
|
+
"commands_executed_count": len(commands_executed),
|
|
232
|
+
"context_snapshot": context_snapshot,
|
|
233
|
+
"context_updated": bool(context_update_result.get("updated", False)),
|
|
234
|
+
"context_sections_updated": context_update_result.get("sections_updated", []),
|
|
235
|
+
"context_rejected_sections": context_update_result.get("rejected", []),
|
|
236
|
+
"default_skills_snapshot": default_skills_snapshot,
|
|
237
|
+
"context_anchor_hits": anchor_hits,
|
|
238
|
+
"context_anchor_hit_rate": anchor_hits.get("hit_rate") if anchor_hits else None,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# --- Transcript-analysis enrichment (T010) ---
|
|
242
|
+
if transcript_analysis is not None:
|
|
243
|
+
metrics["input_tokens"] = transcript_analysis.input_tokens
|
|
244
|
+
metrics["cache_creation_tokens"] = transcript_analysis.cache_creation_tokens
|
|
245
|
+
metrics["cache_read_tokens"] = transcript_analysis.cache_read_tokens
|
|
246
|
+
metrics["output_tokens_real"] = transcript_analysis.output_tokens
|
|
247
|
+
metrics["duration_ms"] = transcript_analysis.duration_ms
|
|
248
|
+
metrics["tool_call_count"] = transcript_analysis.tool_call_count
|
|
249
|
+
metrics["skills_injected"] = transcript_analysis.skills_injected
|
|
250
|
+
metrics["model_used"] = transcript_analysis.model
|
|
251
|
+
metrics["api_call_count"] = transcript_analysis.api_call_count
|
|
252
|
+
|
|
253
|
+
# --- Compliance enrichment (T010) ---
|
|
254
|
+
if compliance_result is not None:
|
|
255
|
+
metrics["compliance_score"] = compliance_result.total
|
|
256
|
+
metrics["compliance_grade"] = compliance_result.grade
|
|
257
|
+
|
|
258
|
+
run_snapshot = {
|
|
259
|
+
"timestamp": metrics["timestamp"],
|
|
260
|
+
"session_id": metrics["session_id"],
|
|
261
|
+
"task_id": metrics["task_id"],
|
|
262
|
+
"agent_id": metrics["agent_id"],
|
|
263
|
+
"agent": metrics["agent"],
|
|
264
|
+
"tier": metrics["tier"],
|
|
265
|
+
"plan_status": metrics["plan_status"],
|
|
266
|
+
"context_snapshot": context_snapshot,
|
|
267
|
+
"context_updated": metrics["context_updated"],
|
|
268
|
+
"context_sections_updated": metrics["context_sections_updated"],
|
|
269
|
+
"context_rejected_sections": metrics["context_rejected_sections"],
|
|
270
|
+
"default_skills_snapshot": default_skills_snapshot,
|
|
271
|
+
"context_anchor_hits": anchor_hits,
|
|
272
|
+
"context_anchor_hit_rate": anchor_hits.get("hit_rate") if anchor_hits else None,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
workflow_memory_dir = get_workflow_memory_dir()
|
|
277
|
+
workflow_memory_dir.mkdir(parents=True, exist_ok=True)
|
|
278
|
+
_append_jsonl(workflow_memory_dir / "run-snapshots.jsonl", run_snapshot)
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
logger.debug("Could not persist run telemetry snapshot: %s", exc)
|
|
281
|
+
|
|
282
|
+
# Save to workflow memory (gated behind env var; default: no write)
|
|
283
|
+
if os.environ.get("GAIA_WRITE_WORKFLOW_METRICS") == "1":
|
|
284
|
+
workflow_memory_dir = get_workflow_memory_dir()
|
|
285
|
+
workflow_memory_dir.mkdir(parents=True, exist_ok=True)
|
|
286
|
+
|
|
287
|
+
metrics_file = workflow_memory_dir / "metrics.jsonl"
|
|
288
|
+
with open(metrics_file, "a") as f:
|
|
289
|
+
f.write(json.dumps(metrics) + "\n")
|
|
290
|
+
|
|
291
|
+
logger.debug(
|
|
292
|
+
"Captured workflow metrics: %s (duration: %sms, exit: %s, commands: %s)",
|
|
293
|
+
metrics["agent"], duration_ms, exit_code, len(commands_executed),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return metrics
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context Management Module
|
|
3
|
+
|
|
4
|
+
This module provides tools for managing context in agent conversations:
|
|
5
|
+
- context_writer: Progressive enrichment of project-context.json via CONTEXT_UPDATE blocks
|
|
6
|
+
- contracts_loader: Load context contracts, detect cloud provider, merge agent permissions
|
|
7
|
+
- context_injector: Core context injection subsystem for project agents
|
|
8
|
+
- context_freshness: Check staleness of project-context.json for SessionStart
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__all__ = []
|
|
@@ -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)
|