@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,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Results reporter for gaia-ops replay testing.
|
|
3
|
+
|
|
4
|
+
Formats and presents ReplayResult data for human consumption and
|
|
5
|
+
programmatic analysis. Completely decoupled from execution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from gaia_simulator.extractor import ReplayEvent
|
|
17
|
+
from gaia_simulator.runner import ReplayResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReplayReporter:
|
|
21
|
+
"""Formats replay results for human consumption and export."""
|
|
22
|
+
|
|
23
|
+
def events_payload(self, events: list[ReplayEvent]) -> list[dict[str, Any]]:
|
|
24
|
+
"""Convert extracted events to JSON-serializable dicts."""
|
|
25
|
+
return [asdict(event) for event in events]
|
|
26
|
+
|
|
27
|
+
def results_payload(self, results: list[ReplayResult]) -> list[dict[str, Any]]:
|
|
28
|
+
"""Convert replay results to JSON-serializable dicts."""
|
|
29
|
+
output: list[dict[str, Any]] = []
|
|
30
|
+
for r in results:
|
|
31
|
+
entry = {
|
|
32
|
+
"timestamp": r.event.timestamp,
|
|
33
|
+
"hook_name": r.event.hook_name,
|
|
34
|
+
"tool_name": r.event.tool_name,
|
|
35
|
+
"source_file": r.event.source_file,
|
|
36
|
+
"limitations": list(r.event.limitations),
|
|
37
|
+
"expected": {
|
|
38
|
+
"decision": r.event.expected_decision,
|
|
39
|
+
"exit_code": r.event.expected_exit_code,
|
|
40
|
+
"tier": r.event.expected_tier,
|
|
41
|
+
"metadata": r.event.expected_metadata,
|
|
42
|
+
},
|
|
43
|
+
"actual": {
|
|
44
|
+
"decision": r.actual_decision,
|
|
45
|
+
"exit_code": r.actual_exit_code,
|
|
46
|
+
"tier": r.actual_tier,
|
|
47
|
+
"metadata": r.actual_metadata,
|
|
48
|
+
},
|
|
49
|
+
"matched": r.matched,
|
|
50
|
+
"regression_type": r.regression_type,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
tool_input = r.event.stdin_payload.get("tool_input", {})
|
|
54
|
+
if r.event.tool_name == "Bash":
|
|
55
|
+
entry["command"] = tool_input.get("command", "")
|
|
56
|
+
elif r.event.tool_name == "Agent":
|
|
57
|
+
entry["agent"] = tool_input.get("subagent_type", "")
|
|
58
|
+
|
|
59
|
+
output.append(entry)
|
|
60
|
+
return output
|
|
61
|
+
|
|
62
|
+
def summary(self, results: list[ReplayResult]) -> str:
|
|
63
|
+
"""Quick summary: X events, Y matched, Z regressions.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
results: List of ReplayResult instances.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Multi-line summary string.
|
|
70
|
+
"""
|
|
71
|
+
if not results:
|
|
72
|
+
return "No events replayed."
|
|
73
|
+
|
|
74
|
+
total = len(results)
|
|
75
|
+
matched = sum(1 for r in results if r.matched)
|
|
76
|
+
regressions = total - matched
|
|
77
|
+
|
|
78
|
+
lines = [
|
|
79
|
+
"=" * 60,
|
|
80
|
+
"REPLAY SUMMARY",
|
|
81
|
+
"=" * 60,
|
|
82
|
+
f"Total events: {total}",
|
|
83
|
+
f"Matched: {matched}",
|
|
84
|
+
f"Regressions: {regressions}",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
if regressions > 0:
|
|
88
|
+
lines.append("")
|
|
89
|
+
lines.append("Regression breakdown:")
|
|
90
|
+
reg_types = Counter(
|
|
91
|
+
r.regression_type for r in results if not r.matched
|
|
92
|
+
)
|
|
93
|
+
for rtype, count in reg_types.most_common():
|
|
94
|
+
lines.append(f" {rtype}: {count}")
|
|
95
|
+
|
|
96
|
+
# Quick stats by decision
|
|
97
|
+
lines.append("")
|
|
98
|
+
decision_counts = Counter(r.event.expected_decision for r in results)
|
|
99
|
+
lines.append("Events by expected decision:")
|
|
100
|
+
for dec, count in decision_counts.most_common():
|
|
101
|
+
lines.append(f" {dec}: {count}")
|
|
102
|
+
|
|
103
|
+
lines.append("=" * 60)
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
def regressions_only(self, results: list[ReplayResult]) -> str:
|
|
107
|
+
"""Show only regressions with details.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
results: List of ReplayResult instances.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Formatted string showing regression details, or a success message.
|
|
114
|
+
"""
|
|
115
|
+
regressions = [r for r in results if not r.matched]
|
|
116
|
+
|
|
117
|
+
if not regressions:
|
|
118
|
+
return "No regressions detected. All events matched expected behavior."
|
|
119
|
+
|
|
120
|
+
lines = [
|
|
121
|
+
"=" * 60,
|
|
122
|
+
f"REGRESSIONS FOUND: {len(regressions)}",
|
|
123
|
+
"=" * 60,
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
for i, r in enumerate(regressions, 1):
|
|
127
|
+
lines.append("")
|
|
128
|
+
lines.append(f"--- Regression #{i} [{r.regression_type}] ---")
|
|
129
|
+
lines.append(f" Timestamp: {r.event.timestamp}")
|
|
130
|
+
lines.append(f" Hook: {r.event.hook_name}")
|
|
131
|
+
lines.append(f" Tool: {r.event.tool_name}")
|
|
132
|
+
lines.append(f" Source: {r.event.source_file}")
|
|
133
|
+
|
|
134
|
+
# Show the command or agent name
|
|
135
|
+
tool_input = r.event.stdin_payload.get("tool_input", {})
|
|
136
|
+
if r.event.tool_name == "Bash":
|
|
137
|
+
cmd = tool_input.get("command", "")
|
|
138
|
+
if len(cmd) > 120:
|
|
139
|
+
cmd = cmd[:120] + "..."
|
|
140
|
+
lines.append(f" Command: {cmd}")
|
|
141
|
+
elif r.event.tool_name == "Agent":
|
|
142
|
+
agent = tool_input.get("subagent_type", tool_input.get("description", ""))
|
|
143
|
+
lines.append(f" Agent: {agent}")
|
|
144
|
+
|
|
145
|
+
lines.append(f" Expected: decision={r.event.expected_decision}, "
|
|
146
|
+
f"exit_code={r.event.expected_exit_code}, "
|
|
147
|
+
f"tier={r.event.expected_tier or 'n/a'}")
|
|
148
|
+
lines.append(f" Actual: decision={r.actual_decision}, "
|
|
149
|
+
f"exit_code={r.actual_exit_code}, "
|
|
150
|
+
f"tier={r.actual_tier or 'n/a'}")
|
|
151
|
+
|
|
152
|
+
if r.actual_stderr and len(r.actual_stderr) < 200:
|
|
153
|
+
lines.append(f" Stderr: {r.actual_stderr.strip()}")
|
|
154
|
+
|
|
155
|
+
lines.append("")
|
|
156
|
+
lines.append("=" * 60)
|
|
157
|
+
return "\n".join(lines)
|
|
158
|
+
|
|
159
|
+
def full_report(self, results: list[ReplayResult]) -> str:
|
|
160
|
+
"""Full report with stats per hook, per tier, per decision type.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
results: List of ReplayResult instances.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Comprehensive formatted report string.
|
|
167
|
+
"""
|
|
168
|
+
if not results:
|
|
169
|
+
return "No events to report on."
|
|
170
|
+
|
|
171
|
+
lines = [self.summary(results)]
|
|
172
|
+
|
|
173
|
+
# Stats by hook
|
|
174
|
+
hooks: dict[str, list[ReplayResult]] = {}
|
|
175
|
+
for r in results:
|
|
176
|
+
hooks.setdefault(r.event.hook_name, []).append(r)
|
|
177
|
+
|
|
178
|
+
lines.append("")
|
|
179
|
+
lines.append("BREAKDOWN BY HOOK:")
|
|
180
|
+
lines.append("-" * 40)
|
|
181
|
+
for hook_name, hook_results in sorted(hooks.items()):
|
|
182
|
+
total = len(hook_results)
|
|
183
|
+
matched = sum(1 for r in hook_results if r.matched)
|
|
184
|
+
lines.append(f" {hook_name}: {total} events, {matched} matched, "
|
|
185
|
+
f"{total - matched} regressions")
|
|
186
|
+
|
|
187
|
+
# Stats by tier
|
|
188
|
+
tiers: dict[str, list[ReplayResult]] = {}
|
|
189
|
+
for r in results:
|
|
190
|
+
tier = r.event.expected_tier or "n/a"
|
|
191
|
+
tiers.setdefault(tier, []).append(r)
|
|
192
|
+
|
|
193
|
+
lines.append("")
|
|
194
|
+
lines.append("BREAKDOWN BY TIER:")
|
|
195
|
+
lines.append("-" * 40)
|
|
196
|
+
for tier_name, tier_results in sorted(tiers.items()):
|
|
197
|
+
total = len(tier_results)
|
|
198
|
+
matched = sum(1 for r in tier_results if r.matched)
|
|
199
|
+
lines.append(f" {tier_name}: {total} events, {matched} matched, "
|
|
200
|
+
f"{total - matched} regressions")
|
|
201
|
+
|
|
202
|
+
# Stats by tool
|
|
203
|
+
tools: dict[str, list[ReplayResult]] = {}
|
|
204
|
+
for r in results:
|
|
205
|
+
tools.setdefault(r.event.tool_name, []).append(r)
|
|
206
|
+
|
|
207
|
+
lines.append("")
|
|
208
|
+
lines.append("BREAKDOWN BY TOOL:")
|
|
209
|
+
lines.append("-" * 40)
|
|
210
|
+
for tool_name, tool_results in sorted(tools.items()):
|
|
211
|
+
total = len(tool_results)
|
|
212
|
+
matched = sum(1 for r in tool_results if r.matched)
|
|
213
|
+
lines.append(f" {tool_name}: {total} events, {matched} matched, "
|
|
214
|
+
f"{total - matched} regressions")
|
|
215
|
+
|
|
216
|
+
# Stats by source file / artifact
|
|
217
|
+
sources: dict[str, list[ReplayResult]] = {}
|
|
218
|
+
for r in results:
|
|
219
|
+
sources.setdefault(r.event.source_file, []).append(r)
|
|
220
|
+
|
|
221
|
+
lines.append("")
|
|
222
|
+
lines.append("BREAKDOWN BY SOURCE:")
|
|
223
|
+
lines.append("-" * 40)
|
|
224
|
+
for source_name, source_results in sorted(sources.items()):
|
|
225
|
+
total = len(source_results)
|
|
226
|
+
matched = sum(1 for r in source_results if r.matched)
|
|
227
|
+
lines.append(f" {source_name}: {total} events, {matched} matched, "
|
|
228
|
+
f"{total - matched} regressions")
|
|
229
|
+
|
|
230
|
+
limitations = sorted({
|
|
231
|
+
limitation
|
|
232
|
+
for r in results
|
|
233
|
+
for limitation in r.event.limitations
|
|
234
|
+
if limitation
|
|
235
|
+
})
|
|
236
|
+
if limitations:
|
|
237
|
+
lines.append("")
|
|
238
|
+
lines.append("SOURCE LIMITATIONS:")
|
|
239
|
+
lines.append("-" * 40)
|
|
240
|
+
for limitation in limitations:
|
|
241
|
+
lines.append(f" - {limitation}")
|
|
242
|
+
|
|
243
|
+
# Show regressions if any
|
|
244
|
+
regressions = [r for r in results if not r.matched]
|
|
245
|
+
if regressions:
|
|
246
|
+
lines.append("")
|
|
247
|
+
lines.append(self.regressions_only(results))
|
|
248
|
+
|
|
249
|
+
return "\n".join(lines)
|
|
250
|
+
|
|
251
|
+
def save_json(self, results: list[ReplayResult], path: Path) -> None:
|
|
252
|
+
"""Save results as JSON for programmatic analysis.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
results: List of ReplayResult instances.
|
|
256
|
+
path: Output file path.
|
|
257
|
+
"""
|
|
258
|
+
path.write_text(json.dumps(self.results_payload(results), indent=2, default=str))
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Routing simulator for gaia-ops surface routing analysis.
|
|
3
|
+
|
|
4
|
+
Simulates what would happen when a prompt enters the orchestrator:
|
|
5
|
+
which surfaces activate, which agent is selected, what skills load,
|
|
6
|
+
what context sections are injected, and what contract permissions apply.
|
|
7
|
+
|
|
8
|
+
Uses the real classify_surfaces() from surface_router.py for fidelity.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
_TOOLS_DIR = Path(__file__).resolve().parent.parent
|
|
21
|
+
if str(_TOOLS_DIR) not in sys.path:
|
|
22
|
+
sys.path.insert(0, str(_TOOLS_DIR))
|
|
23
|
+
|
|
24
|
+
from context.surface_router import classify_surfaces, load_surface_routing_config
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Data classes
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class RoutingResult:
|
|
34
|
+
"""Result of simulating a prompt through the routing pipeline."""
|
|
35
|
+
|
|
36
|
+
prompt: str
|
|
37
|
+
surfaces_active: list[str]
|
|
38
|
+
primary_agent: str
|
|
39
|
+
adjacent_agents: list[str]
|
|
40
|
+
skills_loaded: list[str]
|
|
41
|
+
context_sections: list[str]
|
|
42
|
+
tokens_estimate: int
|
|
43
|
+
contracts: dict[str, list[str]] # {"read": [...], "write": [...]}
|
|
44
|
+
confidence: float
|
|
45
|
+
multi_surface: bool
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Frontmatter parsing
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
_RE_FRONTMATTER = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)
|
|
53
|
+
_RE_SKILLS_LINE = re.compile(r"^\s+-\s+(.+)$", re.MULTILINE)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse_frontmatter(content: str) -> dict[str, Any]:
|
|
57
|
+
"""Parse YAML frontmatter from agent .md files."""
|
|
58
|
+
if not content.startswith("---"):
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
match = _RE_FRONTMATTER.match(content)
|
|
62
|
+
if not match:
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
yaml_block = match.group(1)
|
|
66
|
+
result: dict[str, Any] = {}
|
|
67
|
+
|
|
68
|
+
for line in yaml_block.splitlines():
|
|
69
|
+
line_stripped = line.strip()
|
|
70
|
+
if not line_stripped or line_stripped.startswith("#"):
|
|
71
|
+
continue
|
|
72
|
+
if ":" not in line_stripped:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
key, _, value = line_stripped.partition(":")
|
|
76
|
+
key = key.strip()
|
|
77
|
+
value = value.strip()
|
|
78
|
+
|
|
79
|
+
if key == "skills":
|
|
80
|
+
skills: list[str] = []
|
|
81
|
+
if value.startswith("[") and value.endswith("]"):
|
|
82
|
+
skills = [s.strip() for s in value[1:-1].split(",") if s.strip()]
|
|
83
|
+
else:
|
|
84
|
+
skills_start = yaml_block.index("skills:")
|
|
85
|
+
skills_section = yaml_block[skills_start:]
|
|
86
|
+
for skill_match in _RE_SKILLS_LINE.finditer(skills_section):
|
|
87
|
+
skill_name = skill_match.group(1).strip()
|
|
88
|
+
if skill_name:
|
|
89
|
+
skills.append(skill_name)
|
|
90
|
+
result["skills"] = skills
|
|
91
|
+
else:
|
|
92
|
+
result[key] = value
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _load_agent_skills(agents_dir: Path) -> dict[str, list[str]]:
|
|
98
|
+
"""Load skills lists from all agent .md files in a directory.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dict mapping agent name to list of skill names.
|
|
102
|
+
"""
|
|
103
|
+
agent_skills: dict[str, list[str]] = {}
|
|
104
|
+
|
|
105
|
+
if not agents_dir.is_dir():
|
|
106
|
+
return agent_skills
|
|
107
|
+
|
|
108
|
+
for md_file in sorted(agents_dir.glob("*.md")):
|
|
109
|
+
content = md_file.read_text(encoding="utf-8", errors="replace")
|
|
110
|
+
frontmatter = _parse_frontmatter(content)
|
|
111
|
+
agent_name = frontmatter.get("name", md_file.stem)
|
|
112
|
+
agent_skills[agent_name] = frontmatter.get("skills", [])
|
|
113
|
+
|
|
114
|
+
return agent_skills
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# RoutingSimulator
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class RoutingSimulator:
|
|
123
|
+
"""Simulates the gaia-ops routing pipeline for a given prompt.
|
|
124
|
+
|
|
125
|
+
Loads surface-routing.json, context-contracts.json, and agent frontmatter
|
|
126
|
+
to predict: which surfaces activate, which agent handles, what skills and
|
|
127
|
+
context sections are injected, and what contract permissions apply.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, config_dir: Path, agents_dir: Path):
|
|
131
|
+
"""Initialize the simulator with config and agents directories.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
config_dir: Path to the config/ directory containing
|
|
135
|
+
surface-routing.json and context-contracts.json.
|
|
136
|
+
agents_dir: Path to the agents/ directory containing agent .md files.
|
|
137
|
+
"""
|
|
138
|
+
self._config_dir = config_dir
|
|
139
|
+
self._agents_dir = agents_dir
|
|
140
|
+
|
|
141
|
+
# Load routing config
|
|
142
|
+
routing_file = config_dir / "surface-routing.json"
|
|
143
|
+
self._routing_config = load_surface_routing_config(routing_file)
|
|
144
|
+
|
|
145
|
+
# Load contracts
|
|
146
|
+
contracts_file = config_dir / "context-contracts.json"
|
|
147
|
+
if contracts_file.is_file():
|
|
148
|
+
self._contracts = json.loads(contracts_file.read_text(encoding="utf-8"))
|
|
149
|
+
else:
|
|
150
|
+
self._contracts = {"agents": {}}
|
|
151
|
+
|
|
152
|
+
# Load agent skills from frontmatter
|
|
153
|
+
self._agent_skills = _load_agent_skills(agents_dir)
|
|
154
|
+
|
|
155
|
+
def simulate(self, prompt: str, agent_type: Optional[str] = None) -> RoutingResult:
|
|
156
|
+
"""Simulate routing for a prompt.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
prompt: The user prompt to classify.
|
|
160
|
+
agent_type: If provided, show what this specific agent would receive.
|
|
161
|
+
If not, determine the agent from surface routing.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
RoutingResult with full routing prediction.
|
|
165
|
+
"""
|
|
166
|
+
routing = classify_surfaces(
|
|
167
|
+
prompt,
|
|
168
|
+
current_agent=agent_type or "",
|
|
169
|
+
routing_config=self._routing_config,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
active_surfaces = routing.get("active_surfaces", [])
|
|
173
|
+
confidence = routing.get("confidence", 0.0)
|
|
174
|
+
multi_surface = routing.get("multi_surface", False)
|
|
175
|
+
recommended_agents = routing.get("recommended_agents", [])
|
|
176
|
+
|
|
177
|
+
surfaces_cfg = self._routing_config.get("surfaces", {})
|
|
178
|
+
if agent_type:
|
|
179
|
+
primary_agent = agent_type
|
|
180
|
+
elif recommended_agents:
|
|
181
|
+
primary_agent = recommended_agents[0]
|
|
182
|
+
else:
|
|
183
|
+
primary_agent = self._routing_config.get(
|
|
184
|
+
"reconnaissance_agent", "devops-developer"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
skills = self._agent_skills.get(primary_agent, [])
|
|
188
|
+
|
|
189
|
+
context_sections: list[str] = []
|
|
190
|
+
seen_sections: set[str] = set()
|
|
191
|
+
for surface in active_surfaces:
|
|
192
|
+
surface_cfg = surfaces_cfg.get(surface, {})
|
|
193
|
+
for section in surface_cfg.get("contract_sections", []):
|
|
194
|
+
if section not in seen_sections:
|
|
195
|
+
context_sections.append(section)
|
|
196
|
+
seen_sections.add(section)
|
|
197
|
+
|
|
198
|
+
agent_contract = self._contracts.get("agents", {}).get(primary_agent, {})
|
|
199
|
+
read_sections = agent_contract.get("read", [])
|
|
200
|
+
write_sections = agent_contract.get("write", [])
|
|
201
|
+
|
|
202
|
+
tokens_estimate = len(context_sections) * 100
|
|
203
|
+
|
|
204
|
+
adjacent_agents = [a for a in recommended_agents if a != primary_agent]
|
|
205
|
+
|
|
206
|
+
return RoutingResult(
|
|
207
|
+
prompt=prompt,
|
|
208
|
+
surfaces_active=active_surfaces,
|
|
209
|
+
primary_agent=primary_agent,
|
|
210
|
+
adjacent_agents=adjacent_agents,
|
|
211
|
+
skills_loaded=skills,
|
|
212
|
+
context_sections=context_sections,
|
|
213
|
+
tokens_estimate=tokens_estimate,
|
|
214
|
+
contracts={"read": read_sections, "write": write_sections},
|
|
215
|
+
confidence=confidence,
|
|
216
|
+
multi_surface=multi_surface,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def simulate_from_log(self, events: list[dict[str, Any]]) -> list[RoutingResult]:
|
|
220
|
+
"""Simulate routing for all events from logs.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
events: List of event dicts, each with prompt or tool_input data.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
List of RoutingResult instances.
|
|
227
|
+
"""
|
|
228
|
+
results: list[RoutingResult] = []
|
|
229
|
+
for event in events:
|
|
230
|
+
prompt = event.get("prompt", "")
|
|
231
|
+
if not prompt:
|
|
232
|
+
tool_input = event.get("tool_input", {})
|
|
233
|
+
if isinstance(tool_input, dict):
|
|
234
|
+
prompt = tool_input.get(
|
|
235
|
+
"command", tool_input.get("description", "")
|
|
236
|
+
)
|
|
237
|
+
if not prompt:
|
|
238
|
+
continue
|
|
239
|
+
agent_used = event.get("agent", event.get("subagent_type", None))
|
|
240
|
+
result = self.simulate(prompt, agent_type=agent_used)
|
|
241
|
+
results.append(result)
|
|
242
|
+
return results
|
|
243
|
+
|
|
244
|
+
def compare_routing(self, events: list[dict[str, Any]]) -> dict[str, Any]:
|
|
245
|
+
"""Compare simulated routing vs actual agent used in logs.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
events: List of event dicts, each with prompt and agent keys.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Dict with matches, mismatches, and statistics.
|
|
252
|
+
"""
|
|
253
|
+
matches: list[dict[str, Any]] = []
|
|
254
|
+
mismatches: list[dict[str, Any]] = []
|
|
255
|
+
|
|
256
|
+
for event in events:
|
|
257
|
+
prompt = event.get("prompt", "")
|
|
258
|
+
actual_agent = event.get("agent", event.get("subagent_type", ""))
|
|
259
|
+
if not prompt or not actual_agent:
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
result = self.simulate(prompt)
|
|
263
|
+
|
|
264
|
+
entry = {
|
|
265
|
+
"prompt": prompt[:120],
|
|
266
|
+
"simulated_agent": result.primary_agent,
|
|
267
|
+
"actual_agent": actual_agent,
|
|
268
|
+
"surfaces": result.surfaces_active,
|
|
269
|
+
"confidence": result.confidence,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if result.primary_agent == actual_agent:
|
|
273
|
+
matches.append(entry)
|
|
274
|
+
else:
|
|
275
|
+
mismatches.append(entry)
|
|
276
|
+
|
|
277
|
+
total = len(matches) + len(mismatches)
|
|
278
|
+
return {
|
|
279
|
+
"total": total,
|
|
280
|
+
"matches": len(matches),
|
|
281
|
+
"mismatches": len(mismatches),
|
|
282
|
+
"match_rate": round(len(matches) / max(total, 1), 2),
|
|
283
|
+
"match_details": matches,
|
|
284
|
+
"mismatch_details": mismatches,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def format_routing_result(result: RoutingResult) -> str:
|
|
289
|
+
"""Format a RoutingResult as human-readable text.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
result: The routing simulation result.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Formatted multi-line string.
|
|
296
|
+
"""
|
|
297
|
+
lines = [
|
|
298
|
+
"=" * 60,
|
|
299
|
+
"ROUTING SIMULATION",
|
|
300
|
+
"=" * 60,
|
|
301
|
+
]
|
|
302
|
+
lines.append("Prompt: " + result.prompt[:100])
|
|
303
|
+
lines.append("Primary agent: " + result.primary_agent)
|
|
304
|
+
adj = ", ".join(result.adjacent_agents) or "none"
|
|
305
|
+
lines.append("Adjacent agents: " + adj)
|
|
306
|
+
lines.append("Confidence: " + str(result.confidence))
|
|
307
|
+
lines.append("Multi-surface: " + str(result.multi_surface))
|
|
308
|
+
lines.append("")
|
|
309
|
+
lines.append("Active surfaces:")
|
|
310
|
+
for surface in result.surfaces_active:
|
|
311
|
+
lines.append(" - " + surface)
|
|
312
|
+
if not result.surfaces_active:
|
|
313
|
+
lines.append(" (none)")
|
|
314
|
+
lines.append("")
|
|
315
|
+
lines.append("Skills loaded:")
|
|
316
|
+
for skill in result.skills_loaded:
|
|
317
|
+
lines.append(" - " + skill)
|
|
318
|
+
if not result.skills_loaded:
|
|
319
|
+
lines.append(" (none)")
|
|
320
|
+
lines.append("")
|
|
321
|
+
lines.append("Context sections:")
|
|
322
|
+
for section in result.context_sections:
|
|
323
|
+
lines.append(" - " + section)
|
|
324
|
+
if not result.context_sections:
|
|
325
|
+
lines.append(" (none)")
|
|
326
|
+
lines.append("Tokens estimate: ~" + str(result.tokens_estimate))
|
|
327
|
+
lines.append("")
|
|
328
|
+
lines.append("Contracts:")
|
|
329
|
+
read_str = ", ".join(result.contracts.get("read", [])) or "none"
|
|
330
|
+
write_str = ", ".join(result.contracts.get("write", [])) or "none"
|
|
331
|
+
lines.append(" Read: " + read_str)
|
|
332
|
+
lines.append(" Write: " + write_str)
|
|
333
|
+
lines.append("=" * 60)
|
|
334
|
+
return chr(10).join(lines)
|