@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,457 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Log parser for gaia-ops hook replay testing.
|
|
3
|
+
|
|
4
|
+
Extracts ReplayEvent instances from production hook logs and audit JSONL files.
|
|
5
|
+
Completely decoupled from hooks -- only understands log formats.
|
|
6
|
+
|
|
7
|
+
Supported log formats:
|
|
8
|
+
hooks-YYYY-MM-DD.log - Human-readable hook execution logs
|
|
9
|
+
audit-YYYY-MM-DD.jsonl - Structured JSON audit trail (post_tool_use events)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ReplayEvent:
|
|
23
|
+
"""A single hook invocation extracted from logs."""
|
|
24
|
+
|
|
25
|
+
timestamp: str
|
|
26
|
+
hook_name: str # "pre_tool_use", "post_tool_use", "subagent_stop"
|
|
27
|
+
tool_name: str # "Bash", "Agent", "Read", etc.
|
|
28
|
+
stdin_payload: dict # The JSON that was sent to the hook
|
|
29
|
+
expected_decision: str # "ALLOW", "BLOCK", "DENY"
|
|
30
|
+
expected_exit_code: int # 0, 1, 2
|
|
31
|
+
expected_tier: str # "T0", "T1", "T2", "T3", "" for non-bash
|
|
32
|
+
source_file: str # Which log file this came from
|
|
33
|
+
expected_metadata: dict[str, Any] = field(default_factory=dict)
|
|
34
|
+
compare_tier: bool = False
|
|
35
|
+
limitations: tuple[str, ...] = field(default_factory=tuple)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Internal regex patterns for hooks-*.log parsing
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
# Matches: 2026-03-11 00:03:32,080 [pre_tool_use] __main__ - INFO - Hook invoked: tool=Bash, params={...}
|
|
43
|
+
_RE_HOOK_INVOKED = re.compile(
|
|
44
|
+
r"^(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d+)"
|
|
45
|
+
r"\s+\[(?P<hook>[^\]]+)\]\s+"
|
|
46
|
+
r"__main__\s+-\s+INFO\s+-\s+"
|
|
47
|
+
r"Hook invoked:\s+tool=(?P<tool>\w+),\s+params=(?P<params>\{.+)"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Matches: ... - INFO - ALLOWED: <command> - tier=T0
|
|
51
|
+
_RE_ALLOWED_BASH = re.compile(
|
|
52
|
+
r"^(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d+)"
|
|
53
|
+
r"\s+\[(?P<hook>[^\]]+)\]\s+"
|
|
54
|
+
r"__main__\s+-\s+INFO\s+-\s+"
|
|
55
|
+
r"ALLOWED:\s+(?P<cmd>.+?)\s+-\s+tier=(?P<tier>T\d)"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Matches: ... - INFO - ALLOWED Task: <agent-name>
|
|
59
|
+
_RE_ALLOWED_TASK = re.compile(
|
|
60
|
+
r"^(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d+)"
|
|
61
|
+
r"\s+\[(?P<hook>[^\]]+)\]\s+"
|
|
62
|
+
r"__main__\s+-\s+INFO\s+-\s+"
|
|
63
|
+
r"ALLOWED Task:\s+(?P<agent>\S+)"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Matches: ... - WARNING - BLOCKED: <command> - <reason>
|
|
67
|
+
_RE_BLOCKED = re.compile(
|
|
68
|
+
r"^(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d+)"
|
|
69
|
+
r"\s+\[(?P<hook>[^\]]+)\]\s+"
|
|
70
|
+
r"__main__\s+-\s+WARNING\s+-\s+"
|
|
71
|
+
r"BLOCKED:\s+(?P<cmd>.+?)\s+-\s+(?P<reason>.+)"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Matches: ... - WARNING - BLOCKED Task: <agent> - <reason>
|
|
75
|
+
_RE_BLOCKED_TASK = re.compile(
|
|
76
|
+
r"^(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d+)"
|
|
77
|
+
r"\s+\[(?P<hook>[^\]]+)\]\s+"
|
|
78
|
+
r"__main__\s+-\s+WARNING\s+-\s+"
|
|
79
|
+
r"BLOCKED Task:\s+(?P<agent>\S+)\s+-\s+(?P<reason>.+)"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Matches: ... - INFO - Hook event: SubagentStop, agent: <agent-name>
|
|
83
|
+
_RE_SUBAGENT_STOP = re.compile(
|
|
84
|
+
r"^(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d+)"
|
|
85
|
+
r"\s+\[subagent_stop\]\s+"
|
|
86
|
+
r"__main__\s+-\s+INFO\s+-\s+"
|
|
87
|
+
r"Hook event:\s+SubagentStop,\s+agent:\s+(?P<agent>\S+)"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Matches: ... - INFO - Post-hook event: PostToolUse
|
|
91
|
+
_RE_POST_TOOL_USE = re.compile(
|
|
92
|
+
r"^(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d+)"
|
|
93
|
+
r"\s+\[post_tool_use\]\s+"
|
|
94
|
+
r"__main__\s+-\s+INFO\s+-\s+"
|
|
95
|
+
r"Post-hook event:\s+PostToolUse"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Matches: ... - INFO - Stop: reason=user_requested, quality_sufficient=True, score=1.00
|
|
99
|
+
_RE_STOP_RESULT = re.compile(
|
|
100
|
+
r"^(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d+)"
|
|
101
|
+
r"\s+\[stop_hook\]\s+"
|
|
102
|
+
r"__main__\s+-\s+INFO\s+-\s+"
|
|
103
|
+
r"Stop:\s+reason=(?P<reason>[^,]+),\s+quality_sufficient=(?P<quality>True|False),\s+score=(?P<score>\d+(?:\.\d+)?)"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _try_parse_params(raw: str) -> Optional[dict]:
|
|
108
|
+
"""Try to parse the params JSON from a Hook invoked log line.
|
|
109
|
+
|
|
110
|
+
The params value may be truncated in the log, so we attempt parsing
|
|
111
|
+
and return None on failure.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
return json.loads(raw)
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Truncated JSON -- try to find a valid JSON prefix
|
|
119
|
+
# The log line cuts off params, so we try progressively shorter substrings
|
|
120
|
+
# that end with }
|
|
121
|
+
for i in range(len(raw) - 1, 0, -1):
|
|
122
|
+
if raw[i] == "}":
|
|
123
|
+
try:
|
|
124
|
+
return json.loads(raw[: i + 1])
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
continue
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class LogExtractor:
|
|
131
|
+
"""Extracts ReplayEvents from gaia-ops hook logs.
|
|
132
|
+
|
|
133
|
+
Parses two log formats:
|
|
134
|
+
- hooks-YYYY-MM-DD.log: Human-readable hook execution logs
|
|
135
|
+
- audit-YYYY-MM-DD.jsonl: Structured JSON audit trail
|
|
136
|
+
|
|
137
|
+
Each format produces ReplayEvent instances that can be replayed
|
|
138
|
+
against current hooks to detect regressions.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def extract_from_hooks_log(self, path: Path) -> list[ReplayEvent]:
|
|
142
|
+
"""Parse hooks-YYYY-MM-DD.log and extract hook invocation events.
|
|
143
|
+
|
|
144
|
+
Strategy:
|
|
145
|
+
1. Find "Hook invoked" lines to get the tool + params (stdin payload)
|
|
146
|
+
2. Find the corresponding ALLOWED/BLOCKED line to get the decision
|
|
147
|
+
3. Pair them by timestamp proximity to build ReplayEvents
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
List of ReplayEvent instances, ordered by timestamp.
|
|
151
|
+
"""
|
|
152
|
+
if not path.exists():
|
|
153
|
+
return []
|
|
154
|
+
|
|
155
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
156
|
+
source = path.name
|
|
157
|
+
events: list[ReplayEvent] = []
|
|
158
|
+
|
|
159
|
+
# Two-pass approach:
|
|
160
|
+
# Pass 1: collect all "Hook invoked" entries with their params
|
|
161
|
+
# Pass 2: match them to ALLOWED/BLOCKED outcomes
|
|
162
|
+
|
|
163
|
+
pending_invocations: list[dict] = []
|
|
164
|
+
outcomes: list[dict] = []
|
|
165
|
+
|
|
166
|
+
for line in lines:
|
|
167
|
+
# Hook invoked line
|
|
168
|
+
m = _RE_HOOK_INVOKED.match(line)
|
|
169
|
+
if m:
|
|
170
|
+
params = _try_parse_params(m.group("params"))
|
|
171
|
+
pending_invocations.append({
|
|
172
|
+
"ts": m.group("ts"),
|
|
173
|
+
"hook": m.group("hook"),
|
|
174
|
+
"tool": m.group("tool"),
|
|
175
|
+
"params": params,
|
|
176
|
+
})
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# ALLOWED bash command
|
|
180
|
+
m = _RE_ALLOWED_BASH.match(line)
|
|
181
|
+
if m:
|
|
182
|
+
outcomes.append({
|
|
183
|
+
"ts": m.group("ts"),
|
|
184
|
+
"hook": m.group("hook"),
|
|
185
|
+
"decision": "ALLOW",
|
|
186
|
+
"exit_code": 0,
|
|
187
|
+
"tier": m.group("tier"),
|
|
188
|
+
"type": "bash",
|
|
189
|
+
})
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
# ALLOWED task (agent)
|
|
193
|
+
m = _RE_ALLOWED_TASK.match(line)
|
|
194
|
+
if m:
|
|
195
|
+
outcomes.append({
|
|
196
|
+
"ts": m.group("ts"),
|
|
197
|
+
"hook": m.group("hook"),
|
|
198
|
+
"decision": "ALLOW",
|
|
199
|
+
"exit_code": 0,
|
|
200
|
+
"tier": "",
|
|
201
|
+
"type": "task",
|
|
202
|
+
"agent": m.group("agent"),
|
|
203
|
+
})
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
# BLOCKED bash command
|
|
207
|
+
m = _RE_BLOCKED.match(line)
|
|
208
|
+
if m:
|
|
209
|
+
reason = m.group("reason")
|
|
210
|
+
# The hook logs "BLOCKED:" for both permanent blocks (exit 2,
|
|
211
|
+
# plain string) and structured deny responses (exit 0, JSON
|
|
212
|
+
# with permissionDecision: "deny"). Distinguish them by
|
|
213
|
+
# reason text:
|
|
214
|
+
#
|
|
215
|
+
# Exit 0 DENY (block_response is set):
|
|
216
|
+
# - "Dangerous ..." -- mutative verb T3 nonce flow
|
|
217
|
+
# - "Command-execution rule violated ..." -- cloud pipe
|
|
218
|
+
# - "Failed to persist pending approval ..." -- T3 nonce write error
|
|
219
|
+
# - Compound wrappers that propagate a component's block_response
|
|
220
|
+
#
|
|
221
|
+
# Exit 2 BLOCK (block_response is None):
|
|
222
|
+
# - "Command blocked by security policy ..." -- permanent deny list
|
|
223
|
+
# - "Commit message validation failed ..." -- validation error
|
|
224
|
+
# - "GitOps policy violation ..." -- GitOps validation
|
|
225
|
+
# - "Empty command not allowed"
|
|
226
|
+
if (
|
|
227
|
+
reason.startswith("Dangerous")
|
|
228
|
+
or reason.startswith("Command-execution rule violated")
|
|
229
|
+
or reason.startswith("Failed to persist pending approval")
|
|
230
|
+
or (reason.startswith("Compound command blocked") and "Dangerous" in reason)
|
|
231
|
+
):
|
|
232
|
+
decision = "DENY"
|
|
233
|
+
exit_code = 0
|
|
234
|
+
else:
|
|
235
|
+
decision = "BLOCK"
|
|
236
|
+
exit_code = 2
|
|
237
|
+
outcomes.append({
|
|
238
|
+
"ts": m.group("ts"),
|
|
239
|
+
"hook": m.group("hook"),
|
|
240
|
+
"decision": decision,
|
|
241
|
+
"exit_code": exit_code,
|
|
242
|
+
"tier": "",
|
|
243
|
+
"type": "bash",
|
|
244
|
+
"reason": reason,
|
|
245
|
+
})
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# BLOCKED task
|
|
249
|
+
m = _RE_BLOCKED_TASK.match(line)
|
|
250
|
+
if m:
|
|
251
|
+
outcomes.append({
|
|
252
|
+
"ts": m.group("ts"),
|
|
253
|
+
"hook": m.group("hook"),
|
|
254
|
+
"decision": "BLOCK",
|
|
255
|
+
"exit_code": 2,
|
|
256
|
+
"tier": "",
|
|
257
|
+
"type": "task",
|
|
258
|
+
"agent": m.group("agent"),
|
|
259
|
+
"reason": m.group("reason"),
|
|
260
|
+
})
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Stop hook result (minimal replayable payload)
|
|
264
|
+
m = _RE_STOP_RESULT.match(line)
|
|
265
|
+
if m:
|
|
266
|
+
quality_sufficient = m.group("quality") == "True"
|
|
267
|
+
events.append(ReplayEvent(
|
|
268
|
+
timestamp=m.group("ts"),
|
|
269
|
+
hook_name="stop_hook",
|
|
270
|
+
tool_name="Stop",
|
|
271
|
+
stdin_payload={
|
|
272
|
+
"hook_event_name": "Stop",
|
|
273
|
+
"session_id": "replay",
|
|
274
|
+
"stop_reason": m.group("reason"),
|
|
275
|
+
},
|
|
276
|
+
expected_decision="PASS",
|
|
277
|
+
expected_exit_code=0,
|
|
278
|
+
expected_tier="",
|
|
279
|
+
source_file=source,
|
|
280
|
+
expected_metadata={
|
|
281
|
+
"quality_sufficient": quality_sufficient,
|
|
282
|
+
"score": float(m.group("score")),
|
|
283
|
+
},
|
|
284
|
+
limitations=(
|
|
285
|
+
"hooks log captures stop reason and quality summary, but not last_assistant_message",
|
|
286
|
+
),
|
|
287
|
+
))
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
# Match invocations to outcomes by sequential pairing
|
|
291
|
+
# Each Hook invoked is followed by exactly one ALLOWED or BLOCKED
|
|
292
|
+
outcome_idx = 0
|
|
293
|
+
for inv in pending_invocations:
|
|
294
|
+
if outcome_idx >= len(outcomes):
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
outcome = outcomes[outcome_idx]
|
|
298
|
+
outcome_idx += 1
|
|
299
|
+
|
|
300
|
+
# Build the stdin_payload that the hook expects
|
|
301
|
+
tool = inv["tool"]
|
|
302
|
+
params = inv["params"]
|
|
303
|
+
|
|
304
|
+
# Skip events with truncated/unparseable params -- they cannot
|
|
305
|
+
# be replayed meaningfully. The log line was cut off before the
|
|
306
|
+
# JSON closed, so we don't have the full payload.
|
|
307
|
+
if params is None:
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
# Validate minimum payload: Bash needs "command", Agent needs
|
|
311
|
+
# at least "subagent_type" or "description"
|
|
312
|
+
if tool == "Bash" and "command" not in params:
|
|
313
|
+
continue
|
|
314
|
+
if tool == "Agent" and not any(
|
|
315
|
+
k in params for k in ("subagent_type", "description", "prompt")
|
|
316
|
+
):
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
stdin_payload = {
|
|
320
|
+
"tool_name": tool,
|
|
321
|
+
"tool_input": params,
|
|
322
|
+
"hook_event_name": "PreToolUse",
|
|
323
|
+
"session_id": "replay",
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
events.append(ReplayEvent(
|
|
327
|
+
timestamp=inv["ts"],
|
|
328
|
+
hook_name=inv["hook"],
|
|
329
|
+
tool_name=tool,
|
|
330
|
+
stdin_payload=stdin_payload,
|
|
331
|
+
expected_decision=outcome["decision"],
|
|
332
|
+
expected_exit_code=outcome["exit_code"],
|
|
333
|
+
expected_tier=outcome.get("tier", ""),
|
|
334
|
+
source_file=source,
|
|
335
|
+
compare_tier=tool in {"Bash", "Task", "Agent"} and inv["hook"] == "pre_tool_use",
|
|
336
|
+
))
|
|
337
|
+
|
|
338
|
+
return sorted(events, key=lambda event: event.timestamp)
|
|
339
|
+
|
|
340
|
+
def extract_from_audit_jsonl(self, path: Path) -> list[ReplayEvent]:
|
|
341
|
+
"""Parse audit-YYYY-MM-DD.jsonl for structured event data.
|
|
342
|
+
|
|
343
|
+
Audit JSONL files contain post_tool_use records with:
|
|
344
|
+
- timestamp, session_id, tool_name, command, parameters, duration_ms,
|
|
345
|
+
exit_code, tier
|
|
346
|
+
|
|
347
|
+
These records are emitted by the post_tool_use hook after a tool ran,
|
|
348
|
+
so they are replayed back into post_tool_use with the best available
|
|
349
|
+
synthetic tool_result payload reconstructed from the audit entry.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
List of ReplayEvent instances, ordered by timestamp.
|
|
353
|
+
"""
|
|
354
|
+
if not path.exists():
|
|
355
|
+
return []
|
|
356
|
+
|
|
357
|
+
source = path.name
|
|
358
|
+
events: list[ReplayEvent] = []
|
|
359
|
+
|
|
360
|
+
for line_text in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
361
|
+
line_text = line_text.strip()
|
|
362
|
+
if not line_text:
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
record = json.loads(line_text)
|
|
367
|
+
except json.JSONDecodeError:
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
tool_name = record.get("tool_name", "")
|
|
371
|
+
if not tool_name:
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
# Build stdin payload matching PostToolUse format. The audit log does
|
|
375
|
+
# not persist tool stdout/stderr, so replay can only restore the
|
|
376
|
+
# tool metadata, exit code, and duration.
|
|
377
|
+
params = record.get("parameters", {})
|
|
378
|
+
tool_exit_code = int(record.get("exit_code", 0) or 0)
|
|
379
|
+
duration_ms = record.get("duration_ms", 0)
|
|
380
|
+
stdin_payload = {
|
|
381
|
+
"tool_name": tool_name,
|
|
382
|
+
"tool_input": params,
|
|
383
|
+
"tool_result": {
|
|
384
|
+
"output": "",
|
|
385
|
+
"stdout": "",
|
|
386
|
+
"exit_code": tool_exit_code,
|
|
387
|
+
"duration_ms": duration_ms,
|
|
388
|
+
},
|
|
389
|
+
"hook_event_name": "PostToolUse",
|
|
390
|
+
"session_id": record.get("session_id", "replay") or "replay",
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
tier = record.get("tier", "")
|
|
394
|
+
|
|
395
|
+
events.append(ReplayEvent(
|
|
396
|
+
timestamp=record.get("timestamp", ""),
|
|
397
|
+
hook_name="post_tool_use",
|
|
398
|
+
tool_name=tool_name,
|
|
399
|
+
stdin_payload=stdin_payload,
|
|
400
|
+
expected_decision="PASS",
|
|
401
|
+
expected_exit_code=0,
|
|
402
|
+
expected_tier=tier,
|
|
403
|
+
source_file=source,
|
|
404
|
+
expected_metadata={
|
|
405
|
+
"tool_exit_code": tool_exit_code,
|
|
406
|
+
},
|
|
407
|
+
compare_tier=bool(tier),
|
|
408
|
+
limitations=(
|
|
409
|
+
"audit JSONL does not persist tool output, so post_tool_use replay cannot validate output-dependent critical-event detection",
|
|
410
|
+
),
|
|
411
|
+
))
|
|
412
|
+
|
|
413
|
+
return events
|
|
414
|
+
|
|
415
|
+
def extract_all(
|
|
416
|
+
self,
|
|
417
|
+
logs_dir: Path,
|
|
418
|
+
date_filter: Optional[str] = None,
|
|
419
|
+
hook_filter: Optional[str] = None,
|
|
420
|
+
) -> list[ReplayEvent]:
|
|
421
|
+
"""Extract from all log files in a directory, merge by timestamp.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
logs_dir: Directory containing hooks-*.log and audit-*.jsonl files.
|
|
425
|
+
date_filter: Optional YYYY-MM-DD string to filter by date.
|
|
426
|
+
hook_filter: Optional hook name to filter (e.g. "pre_tool_use").
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Merged list of ReplayEvent instances, sorted by timestamp.
|
|
430
|
+
"""
|
|
431
|
+
events: list[ReplayEvent] = []
|
|
432
|
+
|
|
433
|
+
# Process hooks logs
|
|
434
|
+
pattern = f"hooks-{date_filter}.log" if date_filter else "hooks-*.log"
|
|
435
|
+
for log_path in sorted(logs_dir.glob(pattern)):
|
|
436
|
+
events.extend(self.extract_from_hooks_log(log_path))
|
|
437
|
+
|
|
438
|
+
# Process audit JSONL
|
|
439
|
+
pattern = f"audit-{date_filter}.jsonl" if date_filter else "audit-*.jsonl"
|
|
440
|
+
for jsonl_path in sorted(logs_dir.glob(pattern)):
|
|
441
|
+
events.extend(self.extract_from_audit_jsonl(jsonl_path))
|
|
442
|
+
|
|
443
|
+
# Apply hook filter
|
|
444
|
+
if hook_filter:
|
|
445
|
+
events = [e for e in events if e.hook_name == hook_filter]
|
|
446
|
+
|
|
447
|
+
# Deduplicate: same timestamp + same tool_name + same expected_decision
|
|
448
|
+
# Keep the hooks-log version (richer data) over audit version
|
|
449
|
+
seen: dict[tuple, ReplayEvent] = {}
|
|
450
|
+
for ev in events:
|
|
451
|
+
key = (ev.timestamp, ev.tool_name, ev.expected_decision)
|
|
452
|
+
if key not in seen or ev.source_file.startswith("hooks-"):
|
|
453
|
+
seen[key] = ev
|
|
454
|
+
|
|
455
|
+
# Sort by timestamp
|
|
456
|
+
result = sorted(seen.values(), key=lambda e: e.timestamp)
|
|
457
|
+
return result
|