@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,458 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transcript analysis and compliance scoring for Claude Code agent transcripts.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- ToolCall, DuplicateCall, TranscriptAnalysis: Data structures for analysis results
|
|
6
|
+
- analyze(): Single-pass JSONL transcript parser
|
|
7
|
+
- ComplianceScore: Compliance scoring data structure
|
|
8
|
+
- compute_compliance_score(): Score agent behavior against compliance factors
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ============================================================================
|
|
24
|
+
# T004 — Dataclasses
|
|
25
|
+
# ============================================================================
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ToolCall:
|
|
30
|
+
"""A single tool invocation extracted from the transcript."""
|
|
31
|
+
|
|
32
|
+
index: int # 1-based position in the tool sequence
|
|
33
|
+
tool_name: str
|
|
34
|
+
arguments: Dict[str, Any]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class DuplicateCall:
|
|
39
|
+
"""A group of identical tool calls detected via argument hashing."""
|
|
40
|
+
|
|
41
|
+
tool_name: str
|
|
42
|
+
arguments_hash: str
|
|
43
|
+
indices: List[int]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class TranscriptAnalysis:
|
|
48
|
+
"""Aggregated metrics from a single-pass JSONL transcript parse."""
|
|
49
|
+
|
|
50
|
+
input_tokens: int = 0
|
|
51
|
+
cache_creation_tokens: int = 0
|
|
52
|
+
cache_read_tokens: int = 0
|
|
53
|
+
output_tokens: int = 0
|
|
54
|
+
duration_ms: Optional[int] = None
|
|
55
|
+
first_timestamp: Optional[str] = None
|
|
56
|
+
last_timestamp: Optional[str] = None
|
|
57
|
+
model: str = ""
|
|
58
|
+
stop_reasons: List[str] = field(default_factory=list)
|
|
59
|
+
api_call_count: int = 0
|
|
60
|
+
tool_sequence: List[ToolCall] = field(default_factory=list)
|
|
61
|
+
tool_call_count: int = 0
|
|
62
|
+
bash_commands: List[str] = field(default_factory=list)
|
|
63
|
+
skills_injected: List[str] = field(default_factory=list)
|
|
64
|
+
duplicate_tool_calls: List[DuplicateCall] = field(default_factory=list)
|
|
65
|
+
pipe_commands: List[str] = field(default_factory=list)
|
|
66
|
+
first_tool_name: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ============================================================================
|
|
70
|
+
# T005 — analyze() function
|
|
71
|
+
# ============================================================================
|
|
72
|
+
|
|
73
|
+
# Matches <command-name>...</command-name> tags in user messages
|
|
74
|
+
_COMMAND_NAME_RE = re.compile(r"<command-name>\s*(.+?)\s*</command-name>")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _has_unquoted_pipe(command: str) -> bool:
|
|
78
|
+
"""Detect unquoted pipe characters in a bash command string.
|
|
79
|
+
|
|
80
|
+
Uses a character-walk approach to track quote state, which is more
|
|
81
|
+
reliable than regex for nested quotes.
|
|
82
|
+
"""
|
|
83
|
+
in_single = False
|
|
84
|
+
in_double = False
|
|
85
|
+
escaped = False
|
|
86
|
+
|
|
87
|
+
for ch in command:
|
|
88
|
+
if escaped:
|
|
89
|
+
escaped = False
|
|
90
|
+
continue
|
|
91
|
+
if ch == "\\":
|
|
92
|
+
escaped = True
|
|
93
|
+
continue
|
|
94
|
+
if ch == "'" and not in_double:
|
|
95
|
+
in_single = not in_single
|
|
96
|
+
continue
|
|
97
|
+
if ch == '"' and not in_single:
|
|
98
|
+
in_double = not in_double
|
|
99
|
+
continue
|
|
100
|
+
if ch == "|" and not in_single and not in_double:
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _hash_tool_call(tool_name: str, arguments: Dict[str, Any]) -> str:
|
|
107
|
+
"""Produce a stable hash for a (tool_name, arguments) pair."""
|
|
108
|
+
canonical = json.dumps(
|
|
109
|
+
{"tool_name": tool_name, "arguments": arguments},
|
|
110
|
+
sort_keys=True,
|
|
111
|
+
separators=(",", ":"),
|
|
112
|
+
)
|
|
113
|
+
return hashlib.sha256(canonical.encode()).hexdigest()[:16]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parse_timestamp(ts_str: str) -> Optional[datetime]:
|
|
117
|
+
"""Best-effort ISO timestamp parse."""
|
|
118
|
+
for fmt in (
|
|
119
|
+
"%Y-%m-%dT%H:%M:%S.%fZ",
|
|
120
|
+
"%Y-%m-%dT%H:%M:%SZ",
|
|
121
|
+
"%Y-%m-%dT%H:%M:%S.%f%z",
|
|
122
|
+
"%Y-%m-%dT%H:%M:%S%z",
|
|
123
|
+
"%Y-%m-%dT%H:%M:%S.%f",
|
|
124
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
125
|
+
):
|
|
126
|
+
try:
|
|
127
|
+
return datetime.strptime(ts_str, fmt)
|
|
128
|
+
except ValueError:
|
|
129
|
+
continue
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _extract_tool_calls_from_content(
|
|
134
|
+
content: Any,
|
|
135
|
+
result: TranscriptAnalysis,
|
|
136
|
+
tool_index_counter: List[int],
|
|
137
|
+
hash_map: Dict[str, Dict[str, Any]],
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Extract tool_use blocks from a content list and update result in place."""
|
|
140
|
+
if not isinstance(content, list):
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
for block in content:
|
|
144
|
+
if not isinstance(block, dict):
|
|
145
|
+
continue
|
|
146
|
+
if block.get("type") != "tool_use":
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
tool_name = block.get("name", "")
|
|
150
|
+
arguments = block.get("input", {})
|
|
151
|
+
if not isinstance(arguments, dict):
|
|
152
|
+
arguments = {}
|
|
153
|
+
|
|
154
|
+
idx = tool_index_counter[0]
|
|
155
|
+
tool_index_counter[0] += 1
|
|
156
|
+
|
|
157
|
+
tc = ToolCall(index=idx, tool_name=tool_name, arguments=arguments)
|
|
158
|
+
result.tool_sequence.append(tc)
|
|
159
|
+
result.tool_call_count += 1
|
|
160
|
+
|
|
161
|
+
if result.first_tool_name is None:
|
|
162
|
+
result.first_tool_name = tool_name
|
|
163
|
+
|
|
164
|
+
# Bash command extraction
|
|
165
|
+
if tool_name in ("Bash", "bash"):
|
|
166
|
+
cmd = arguments.get("command", "")
|
|
167
|
+
if isinstance(cmd, str) and cmd:
|
|
168
|
+
result.bash_commands.append(cmd)
|
|
169
|
+
if _has_unquoted_pipe(cmd):
|
|
170
|
+
result.pipe_commands.append(cmd)
|
|
171
|
+
|
|
172
|
+
# Duplicate detection
|
|
173
|
+
h = _hash_tool_call(tool_name, arguments)
|
|
174
|
+
hash_map.setdefault(h, {"tool_name": tool_name, "indices": []})
|
|
175
|
+
hash_map[h]["indices"].append(idx)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _extract_skills_from_content(content: Any, result: TranscriptAnalysis) -> None:
|
|
179
|
+
"""Search content for <command-name> tags and populate skills_injected."""
|
|
180
|
+
if isinstance(content, str):
|
|
181
|
+
for m in _COMMAND_NAME_RE.finditer(content):
|
|
182
|
+
skill = m.group(1).strip()
|
|
183
|
+
if skill and skill not in result.skills_injected:
|
|
184
|
+
result.skills_injected.append(skill)
|
|
185
|
+
elif isinstance(content, list):
|
|
186
|
+
for block in content:
|
|
187
|
+
if isinstance(block, str):
|
|
188
|
+
for m in _COMMAND_NAME_RE.finditer(block):
|
|
189
|
+
skill = m.group(1).strip()
|
|
190
|
+
if skill and skill not in result.skills_injected:
|
|
191
|
+
result.skills_injected.append(skill)
|
|
192
|
+
elif isinstance(block, dict) and block.get("type") == "text":
|
|
193
|
+
text = block.get("text", "")
|
|
194
|
+
for m in _COMMAND_NAME_RE.finditer(text):
|
|
195
|
+
skill = m.group(1).strip()
|
|
196
|
+
if skill and skill not in result.skills_injected:
|
|
197
|
+
result.skills_injected.append(skill)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def analyze(transcript_path: str) -> TranscriptAnalysis:
|
|
201
|
+
"""Single-pass JSONL parser for Claude Code agent transcripts.
|
|
202
|
+
|
|
203
|
+
Reads the transcript file line by line and accumulates:
|
|
204
|
+
- Token usage (input, cache_creation, cache_read, output)
|
|
205
|
+
- Model name (from first assistant turn)
|
|
206
|
+
- Stop reasons
|
|
207
|
+
- API call count (assistant turns)
|
|
208
|
+
- Tool sequence with ToolCall entries
|
|
209
|
+
- Bash commands and pipe violations
|
|
210
|
+
- Skills injected (from <command-name> tags in user messages)
|
|
211
|
+
- Timestamps and duration
|
|
212
|
+
- Duplicate tool call detection
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
transcript_path: Path to the JSONL transcript file.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
TranscriptAnalysis with all accumulated metrics.
|
|
219
|
+
Returns default TranscriptAnalysis() for empty or missing files.
|
|
220
|
+
"""
|
|
221
|
+
result = TranscriptAnalysis()
|
|
222
|
+
|
|
223
|
+
if not transcript_path:
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
path = Path(transcript_path).expanduser()
|
|
227
|
+
if not path.exists():
|
|
228
|
+
logger.debug("Transcript file not found: %s", path)
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
text = path.read_text()
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.debug("Failed to read transcript: %s", e)
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
lines = text.strip().splitlines()
|
|
238
|
+
if not lines:
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
# Mutable counter for tool indexing (1-based)
|
|
242
|
+
tool_index_counter = [1]
|
|
243
|
+
# Hash map for duplicate detection: hash -> {tool_name, indices}
|
|
244
|
+
hash_map: Dict[str, Dict[str, Any]] = {}
|
|
245
|
+
|
|
246
|
+
first_ts_dt: Optional[datetime] = None
|
|
247
|
+
last_ts_dt: Optional[datetime] = None
|
|
248
|
+
|
|
249
|
+
for line in lines:
|
|
250
|
+
line = line.strip()
|
|
251
|
+
if not line:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
entry = json.loads(line)
|
|
256
|
+
except (json.JSONDecodeError, TypeError):
|
|
257
|
+
logger.debug("Skipping malformed JSON line: %.80s", line)
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
if not isinstance(entry, dict):
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# --- Timestamp tracking ---
|
|
264
|
+
timestamp = entry.get("timestamp", "")
|
|
265
|
+
if isinstance(timestamp, str) and timestamp:
|
|
266
|
+
parsed_ts = _parse_timestamp(timestamp)
|
|
267
|
+
if parsed_ts is not None:
|
|
268
|
+
if result.first_timestamp is None:
|
|
269
|
+
result.first_timestamp = timestamp
|
|
270
|
+
first_ts_dt = parsed_ts
|
|
271
|
+
result.last_timestamp = timestamp
|
|
272
|
+
last_ts_dt = parsed_ts
|
|
273
|
+
|
|
274
|
+
msg = entry.get("message", entry)
|
|
275
|
+
if not isinstance(msg, dict):
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
role = msg.get("role", "")
|
|
279
|
+
content = msg.get("content", "")
|
|
280
|
+
|
|
281
|
+
# --- Assistant turns ---
|
|
282
|
+
if role == "assistant":
|
|
283
|
+
# Usage accumulation — check both top-level and nested in message
|
|
284
|
+
# Claude Code transcripts store usage/model/stop_reason inside
|
|
285
|
+
# message object, but some formats keep them at entry level.
|
|
286
|
+
usage = entry.get("usage") or msg.get("usage") or {}
|
|
287
|
+
if isinstance(usage, dict):
|
|
288
|
+
result.input_tokens += int(usage.get("input_tokens", 0))
|
|
289
|
+
result.cache_creation_tokens += int(
|
|
290
|
+
usage.get("cache_creation_input_tokens", 0)
|
|
291
|
+
)
|
|
292
|
+
result.cache_read_tokens += int(
|
|
293
|
+
usage.get("cache_read_input_tokens", 0)
|
|
294
|
+
)
|
|
295
|
+
result.output_tokens += int(usage.get("output_tokens", 0))
|
|
296
|
+
|
|
297
|
+
# Model from first assistant turn — check both locations
|
|
298
|
+
model = entry.get("model") or msg.get("model") or ""
|
|
299
|
+
if isinstance(model, str) and model and not result.model:
|
|
300
|
+
result.model = model
|
|
301
|
+
|
|
302
|
+
# Stop reason — check both locations
|
|
303
|
+
stop_reason = entry.get("stop_reason") or msg.get("stop_reason") or ""
|
|
304
|
+
if isinstance(stop_reason, str) and stop_reason:
|
|
305
|
+
result.stop_reasons.append(stop_reason)
|
|
306
|
+
|
|
307
|
+
# API call count
|
|
308
|
+
result.api_call_count += 1
|
|
309
|
+
|
|
310
|
+
# --- Tool calls from all content lists ---
|
|
311
|
+
_extract_tool_calls_from_content(
|
|
312
|
+
content, result, tool_index_counter, hash_map
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# --- User messages: skill injection detection ---
|
|
316
|
+
if role == "user":
|
|
317
|
+
_extract_skills_from_content(content, result)
|
|
318
|
+
|
|
319
|
+
# --- Duration computation ---
|
|
320
|
+
if first_ts_dt is not None and last_ts_dt is not None:
|
|
321
|
+
delta = last_ts_dt - first_ts_dt
|
|
322
|
+
result.duration_ms = int(delta.total_seconds() * 1000)
|
|
323
|
+
|
|
324
|
+
# --- Duplicate detection finalization ---
|
|
325
|
+
for h, info in hash_map.items():
|
|
326
|
+
if len(info["indices"]) > 1:
|
|
327
|
+
result.duplicate_tool_calls.append(
|
|
328
|
+
DuplicateCall(
|
|
329
|
+
tool_name=info["tool_name"],
|
|
330
|
+
arguments_hash=h,
|
|
331
|
+
indices=info["indices"],
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ============================================================================
|
|
339
|
+
# T006 — ComplianceScore
|
|
340
|
+
# ============================================================================
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@dataclass(frozen=True)
|
|
344
|
+
class ComplianceScore:
|
|
345
|
+
"""Compliance score computed from transcript analysis and external signals."""
|
|
346
|
+
|
|
347
|
+
total: int
|
|
348
|
+
grade: str
|
|
349
|
+
factors: Dict[str, int]
|
|
350
|
+
deductions: List[str]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# Tools considered disciplined as first-tool choices
|
|
354
|
+
_DISCIPLINED_FIRST_TOOLS = {"Read", "Glob", "Grep"}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _grade_from_total(total: int) -> str:
|
|
358
|
+
"""Map a numeric score to a letter grade."""
|
|
359
|
+
if total >= 90:
|
|
360
|
+
return "A"
|
|
361
|
+
if total >= 75:
|
|
362
|
+
return "B"
|
|
363
|
+
if total >= 50:
|
|
364
|
+
return "C"
|
|
365
|
+
return "F"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def compute_compliance_score(
|
|
369
|
+
analysis: TranscriptAnalysis,
|
|
370
|
+
contract_valid: bool,
|
|
371
|
+
has_scope_escalation: bool,
|
|
372
|
+
anchor_hit_rate: float,
|
|
373
|
+
) -> ComplianceScore:
|
|
374
|
+
"""Compute a compliance score from transcript analysis and external signals.
|
|
375
|
+
|
|
376
|
+
Factors (100 points total):
|
|
377
|
+
- contract_valid: 25 pts (binary)
|
|
378
|
+
- investigation_discipline: 20 pts (first tool is Read/Glob/Grep/None)
|
|
379
|
+
- context_utilization: 15 pts (proportional to anchor_hit_rate)
|
|
380
|
+
- no_pipe_violations: 15 pts (minus 3 per pipe command, floor 0)
|
|
381
|
+
- no_duplicate_calls: 10 pts (minus 2 per duplicate group, floor 0)
|
|
382
|
+
- no_scope_escalation: 15 pts (binary)
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
analysis: TranscriptAnalysis from analyze().
|
|
386
|
+
contract_valid: Whether the agent's response contract passed validation.
|
|
387
|
+
has_scope_escalation: Whether the agent escalated beyond its scope.
|
|
388
|
+
anchor_hit_rate: Float 0.0-1.0 representing how many context anchors
|
|
389
|
+
the agent referenced in its evidence.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
ComplianceScore with total, grade, factors breakdown, and deductions.
|
|
393
|
+
"""
|
|
394
|
+
factors: Dict[str, int] = {}
|
|
395
|
+
deductions: List[str] = []
|
|
396
|
+
|
|
397
|
+
# 1. contract_valid (25 pts, binary)
|
|
398
|
+
if contract_valid:
|
|
399
|
+
factors["contract_valid"] = 25
|
|
400
|
+
else:
|
|
401
|
+
factors["contract_valid"] = 0
|
|
402
|
+
deductions.append("contract_valid: invalid contract (-25)")
|
|
403
|
+
|
|
404
|
+
# 2. investigation_discipline (20 pts)
|
|
405
|
+
first_tool = analysis.first_tool_name
|
|
406
|
+
if first_tool is None or first_tool in _DISCIPLINED_FIRST_TOOLS:
|
|
407
|
+
factors["investigation_discipline"] = 20
|
|
408
|
+
else:
|
|
409
|
+
factors["investigation_discipline"] = 0
|
|
410
|
+
deductions.append(
|
|
411
|
+
f"investigation_discipline: first tool was {first_tool}, "
|
|
412
|
+
f"expected Read/Glob/Grep/None (-20)"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# 3. context_utilization (15 pts, proportional)
|
|
416
|
+
clamped_rate = max(0.0, min(1.0, anchor_hit_rate))
|
|
417
|
+
ctx_points = round(15 * clamped_rate)
|
|
418
|
+
factors["context_utilization"] = ctx_points
|
|
419
|
+
if ctx_points < 15:
|
|
420
|
+
deductions.append(
|
|
421
|
+
f"context_utilization: anchor_hit_rate={clamped_rate:.2f} "
|
|
422
|
+
f"(-{15 - ctx_points})"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# 4. no_pipe_violations (15 pts, -3 per pipe, floor 0)
|
|
426
|
+
pipe_count = len(analysis.pipe_commands)
|
|
427
|
+
pipe_points = max(0, 15 - 3 * pipe_count)
|
|
428
|
+
factors["no_pipe_violations"] = pipe_points
|
|
429
|
+
if pipe_count > 0:
|
|
430
|
+
deductions.append(
|
|
431
|
+
f"no_pipe_violations: {pipe_count} pipe command(s) (-{15 - pipe_points})"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# 5. no_duplicate_calls (10 pts, -2 per duplicate group, floor 0)
|
|
435
|
+
dup_count = len(analysis.duplicate_tool_calls)
|
|
436
|
+
dup_points = max(0, 10 - 2 * dup_count)
|
|
437
|
+
factors["no_duplicate_calls"] = dup_points
|
|
438
|
+
if dup_count > 0:
|
|
439
|
+
deductions.append(
|
|
440
|
+
f"no_duplicate_calls: {dup_count} duplicate group(s) (-{10 - dup_points})"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# 6. no_scope_escalation (15 pts, binary)
|
|
444
|
+
if not has_scope_escalation:
|
|
445
|
+
factors["no_scope_escalation"] = 15
|
|
446
|
+
else:
|
|
447
|
+
factors["no_scope_escalation"] = 0
|
|
448
|
+
deductions.append("no_scope_escalation: scope escalation detected (-15)")
|
|
449
|
+
|
|
450
|
+
total = sum(factors.values())
|
|
451
|
+
grade = _grade_from_total(total)
|
|
452
|
+
|
|
453
|
+
return ComplianceScore(
|
|
454
|
+
total=total,
|
|
455
|
+
grade=grade,
|
|
456
|
+
factors=factors,
|
|
457
|
+
deductions=deductions,
|
|
458
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transcript reading and parsing for Claude Code agent transcripts.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- read_transcript(): Read assistant messages from transcript JSONL
|
|
6
|
+
- read_first_user_content_from_transcript(): Read first user message content
|
|
7
|
+
- extract_task_description_from_transcript(): Extract task description
|
|
8
|
+
- extract_injected_context_payload_from_transcript(): Extract auto-injected JSON
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_transcript(transcript_path: str) -> str:
|
|
20
|
+
"""Read agent transcript from file path provided by Claude Code.
|
|
21
|
+
|
|
22
|
+
Claude Code provides ``agent_transcript_path`` pointing to a JSONL file.
|
|
23
|
+
Each line has the structure:
|
|
24
|
+
{"type": "assistant", "message": {"role": "assistant", "content": [...]}, ...}
|
|
25
|
+
The role/content are nested inside the ``message`` field.
|
|
26
|
+
|
|
27
|
+
Falls back to empty string on any error so the hook never crashes.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
# Expand ~ to home directory (Claude Code may use ~ in paths)
|
|
31
|
+
path = Path(transcript_path).expanduser()
|
|
32
|
+
logger.debug("Reading transcript from: %s", path)
|
|
33
|
+
|
|
34
|
+
if not path.exists():
|
|
35
|
+
logger.warning("Transcript file not found: %s", path)
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
lines = path.read_text().strip().splitlines()
|
|
39
|
+
|
|
40
|
+
text_parts: List[str] = []
|
|
41
|
+
for line in lines:
|
|
42
|
+
if not line.strip():
|
|
43
|
+
continue
|
|
44
|
+
try:
|
|
45
|
+
entry = json.loads(line)
|
|
46
|
+
|
|
47
|
+
# Claude Code transcript format: content is inside entry["message"]
|
|
48
|
+
msg = entry.get("message", entry) # fallback to entry itself for simple format
|
|
49
|
+
role = msg.get("role", "")
|
|
50
|
+
if role != "assistant":
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
content = msg.get("content", "")
|
|
54
|
+
if isinstance(content, str):
|
|
55
|
+
text_parts.append(content)
|
|
56
|
+
elif isinstance(content, list):
|
|
57
|
+
for block in content:
|
|
58
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
59
|
+
text_parts.append(block.get("text", ""))
|
|
60
|
+
elif isinstance(block, str):
|
|
61
|
+
text_parts.append(block)
|
|
62
|
+
except (json.JSONDecodeError, TypeError):
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
result = "\n".join(text_parts)
|
|
66
|
+
logger.debug("Extracted %d text parts, total length: %d chars", len(text_parts), len(result))
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.debug("Failed to read transcript from %s: %s", transcript_path, e)
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def read_first_user_content_from_transcript(transcript_path: str) -> Optional[str]:
|
|
75
|
+
"""Read the raw content string of the first user message from a transcript JSONL.
|
|
76
|
+
|
|
77
|
+
Handles: empty path guard, path expansion, existence check, JSONL iteration,
|
|
78
|
+
JSON parse, role=="user" check, content normalization (str vs list).
|
|
79
|
+
Returns the raw content string or None.
|
|
80
|
+
"""
|
|
81
|
+
if not transcript_path:
|
|
82
|
+
return None
|
|
83
|
+
try:
|
|
84
|
+
path = Path(transcript_path).expanduser()
|
|
85
|
+
if not path.exists():
|
|
86
|
+
return None
|
|
87
|
+
with open(path, "r") as f:
|
|
88
|
+
for line in f:
|
|
89
|
+
line = line.strip()
|
|
90
|
+
if not line:
|
|
91
|
+
continue
|
|
92
|
+
try:
|
|
93
|
+
entry = json.loads(line)
|
|
94
|
+
msg = entry.get("message", entry)
|
|
95
|
+
if msg.get("role") != "user":
|
|
96
|
+
continue
|
|
97
|
+
content = msg.get("content", "")
|
|
98
|
+
if isinstance(content, str):
|
|
99
|
+
return content
|
|
100
|
+
elif isinstance(content, list):
|
|
101
|
+
return " ".join(
|
|
102
|
+
b.get("text", "") for b in content
|
|
103
|
+
if isinstance(b, dict) and b.get("type") == "text"
|
|
104
|
+
)
|
|
105
|
+
return None
|
|
106
|
+
except (json.JSONDecodeError, TypeError):
|
|
107
|
+
continue
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.debug("Failed to read first user content from transcript: %s", e)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def extract_task_description_from_transcript(transcript_path: str) -> str:
|
|
114
|
+
"""Read the first user message from the subagent transcript JSONL.
|
|
115
|
+
|
|
116
|
+
Claude Code's agent_transcript_path contains the full subagent conversation.
|
|
117
|
+
The first ``role: "user"`` entry is the task prompt sent by the orchestrator --
|
|
118
|
+
which is the most meaningful description of what the agent was asked to do.
|
|
119
|
+
|
|
120
|
+
Context is delivered via additionalContext (not prompt mutation), so the
|
|
121
|
+
first user message IS the original prompt without any wrapping.
|
|
122
|
+
|
|
123
|
+
Returns empty string on any error so the hook never crashes.
|
|
124
|
+
"""
|
|
125
|
+
content = read_first_user_content_from_transcript(transcript_path)
|
|
126
|
+
if not content:
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
return content.strip()[:500]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def extract_injected_context_payload_from_transcript(
|
|
133
|
+
transcript_path: str,
|
|
134
|
+
) -> Dict[str, Any]:
|
|
135
|
+
"""Extract the auto-injected context payload from disk cache.
|
|
136
|
+
|
|
137
|
+
Context is delivered via additionalContext and the payload is persisted to
|
|
138
|
+
disk by context_injector. Prompts do not contain embedded payloads.
|
|
139
|
+
"""
|
|
140
|
+
import os
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
payload_dir = Path(os.environ.get("TMPDIR", "/tmp")) / "gaia-context-payloads"
|
|
144
|
+
if payload_dir.exists():
|
|
145
|
+
agent_file = Path(transcript_path).stem # e.g. "agent-ae190a4da68d626d4"
|
|
146
|
+
# Match by agent ID substring
|
|
147
|
+
for candidate in payload_dir.glob("*.json"):
|
|
148
|
+
if candidate.stem in agent_file or agent_file in candidate.stem:
|
|
149
|
+
return json.loads(candidate.read_text())
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
return {}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit module - Logging, metrics aggregation, and event detection.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- logger: AuditLogger for tool executions (write path)
|
|
6
|
+
- metrics: generate_summary reads audit logs and aggregates (read path)
|
|
7
|
+
- event_detector: CriticalEventDetector
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .logger import AuditLogger, log_execution
|
|
11
|
+
from .metrics import generate_summary
|
|
12
|
+
from .event_detector import (
|
|
13
|
+
CriticalEventDetector,
|
|
14
|
+
detect_critical_event,
|
|
15
|
+
EventType,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# Logger
|
|
20
|
+
"AuditLogger",
|
|
21
|
+
"log_execution",
|
|
22
|
+
# Metrics
|
|
23
|
+
"generate_summary",
|
|
24
|
+
# Event detector
|
|
25
|
+
"CriticalEventDetector",
|
|
26
|
+
"detect_critical_event",
|
|
27
|
+
"EventType",
|
|
28
|
+
]
|