@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,330 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Context Section Reader for Claude Agent System
|
|
4
|
+
|
|
5
|
+
Reads specific sections from project-context.json for selective loading by agents.
|
|
6
|
+
Called by Claude orchestrator BEFORE invoking agents to reduce token usage.
|
|
7
|
+
|
|
8
|
+
Architecture:
|
|
9
|
+
- Claude orchestrator executes this script (NOT agents)
|
|
10
|
+
- Agents receive pre-filtered context in their prompts
|
|
11
|
+
- Reduces token usage by ~70% per agent invocation
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from .claude.tools.context_section_reader import ContextSectionReader
|
|
15
|
+
|
|
16
|
+
reader = ContextSectionReader()
|
|
17
|
+
context = reader.get_for_agent('gitops-operator')
|
|
18
|
+
|
|
19
|
+
# Pass context to agent in Task tool prompt
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import List, Dict, Optional, Any
|
|
24
|
+
import json
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_claude_dir() -> Path:
|
|
28
|
+
"""Find the .claude directory by searching upward from current location"""
|
|
29
|
+
current = Path.cwd()
|
|
30
|
+
|
|
31
|
+
# If we're already in a .claude directory, return it
|
|
32
|
+
if current.name == ".claude":
|
|
33
|
+
return current
|
|
34
|
+
|
|
35
|
+
# Look for .claude in current directory
|
|
36
|
+
claude_dir = current / ".claude"
|
|
37
|
+
if claude_dir.exists():
|
|
38
|
+
return claude_dir
|
|
39
|
+
|
|
40
|
+
# Search upward through parent directories
|
|
41
|
+
for parent in current.parents:
|
|
42
|
+
claude_dir = parent / ".claude"
|
|
43
|
+
if claude_dir.exists():
|
|
44
|
+
return claude_dir
|
|
45
|
+
|
|
46
|
+
# Fallback - raise error if not found
|
|
47
|
+
raise FileNotFoundError(
|
|
48
|
+
"No .claude directory found. Please run from a project directory "
|
|
49
|
+
"or specify context_file explicitly."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ContextSectionReader:
|
|
54
|
+
"""
|
|
55
|
+
Read and filter sections from project-context.json for agent-specific loading.
|
|
56
|
+
|
|
57
|
+
Token Optimization:
|
|
58
|
+
- Without filtering: ~328 lines (1,312 tokens)
|
|
59
|
+
- With filtering: ~80-100 lines (320-400 tokens)
|
|
60
|
+
- Savings: ~70% per agent invocation
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Define which sections each agent needs (JSON keys in snake_case)
|
|
64
|
+
# Aligned with v2 scanner sections from context-contracts.json v3
|
|
65
|
+
AGENT_SECTIONS = {
|
|
66
|
+
'gitops-operator': [
|
|
67
|
+
'project_identity',
|
|
68
|
+
'stack',
|
|
69
|
+
'git',
|
|
70
|
+
'environment',
|
|
71
|
+
'infrastructure',
|
|
72
|
+
'orchestration',
|
|
73
|
+
'gitops_configuration',
|
|
74
|
+
'cluster_details',
|
|
75
|
+
'operational_guidelines',
|
|
76
|
+
'application_services',
|
|
77
|
+
],
|
|
78
|
+
'cloud-troubleshooter': [
|
|
79
|
+
'project_identity',
|
|
80
|
+
'stack',
|
|
81
|
+
'git',
|
|
82
|
+
'environment',
|
|
83
|
+
'infrastructure',
|
|
84
|
+
'orchestration',
|
|
85
|
+
'cluster_details',
|
|
86
|
+
'infrastructure_topology',
|
|
87
|
+
'terraform_infrastructure',
|
|
88
|
+
'gitops_configuration',
|
|
89
|
+
'application_services',
|
|
90
|
+
'monitoring_observability',
|
|
91
|
+
],
|
|
92
|
+
'terraform-architect': [
|
|
93
|
+
'project_identity',
|
|
94
|
+
'stack',
|
|
95
|
+
'git',
|
|
96
|
+
'environment',
|
|
97
|
+
'infrastructure',
|
|
98
|
+
'orchestration',
|
|
99
|
+
'terraform_infrastructure',
|
|
100
|
+
'infrastructure_topology',
|
|
101
|
+
'operational_guidelines',
|
|
102
|
+
'cluster_details',
|
|
103
|
+
'application_services',
|
|
104
|
+
],
|
|
105
|
+
'devops-developer': [
|
|
106
|
+
'project_identity',
|
|
107
|
+
'stack',
|
|
108
|
+
'git',
|
|
109
|
+
'environment',
|
|
110
|
+
'infrastructure',
|
|
111
|
+
'application_services',
|
|
112
|
+
'operational_guidelines',
|
|
113
|
+
],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def __init__(self, context_file: Optional[str] = None):
|
|
117
|
+
"""
|
|
118
|
+
Initialize reader with project context file.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
context_file: Path to project-context.json (default: searches for .claude/project-context/project-context.json)
|
|
122
|
+
"""
|
|
123
|
+
if context_file is None:
|
|
124
|
+
# Find the .claude directory by searching upward
|
|
125
|
+
claude_dir = find_claude_dir()
|
|
126
|
+
# Try project-context/ subdirectory first (new location)
|
|
127
|
+
context_file = claude_dir / "project-context" / "project-context.json"
|
|
128
|
+
if not Path(context_file).exists():
|
|
129
|
+
# Fallback to root .claude/ (old location)
|
|
130
|
+
context_file = claude_dir / "project-context.json"
|
|
131
|
+
|
|
132
|
+
self.path = Path(context_file)
|
|
133
|
+
|
|
134
|
+
if not self.path.exists():
|
|
135
|
+
raise FileNotFoundError(f"Context file not found: {self.path}")
|
|
136
|
+
|
|
137
|
+
with open(self.path, 'r', encoding='utf-8') as f:
|
|
138
|
+
self.data = json.load(f)
|
|
139
|
+
|
|
140
|
+
self._parse_sections()
|
|
141
|
+
|
|
142
|
+
def _parse_sections(self) -> None:
|
|
143
|
+
"""Extract sections from JSON data."""
|
|
144
|
+
self.sections: Dict[str, Any] = {}
|
|
145
|
+
|
|
146
|
+
# Extract sections from JSON
|
|
147
|
+
if 'sections' in self.data:
|
|
148
|
+
self.sections = self.data['sections']
|
|
149
|
+
else:
|
|
150
|
+
raise ValueError("Invalid JSON structure: 'sections' key not found")
|
|
151
|
+
|
|
152
|
+
def get_sections(self, section_names: List[str]) -> str:
|
|
153
|
+
"""
|
|
154
|
+
Get specific sections as formatted JSON string.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
section_names: List of section names to retrieve
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Formatted JSON string with requested sections
|
|
161
|
+
"""
|
|
162
|
+
result = {}
|
|
163
|
+
missing = []
|
|
164
|
+
|
|
165
|
+
for name in section_names:
|
|
166
|
+
if name in self.sections:
|
|
167
|
+
result[name] = self.sections[name]
|
|
168
|
+
else:
|
|
169
|
+
missing.append(name)
|
|
170
|
+
|
|
171
|
+
if missing:
|
|
172
|
+
print(f"Warning: Sections not found: {missing}")
|
|
173
|
+
|
|
174
|
+
if not result:
|
|
175
|
+
return json.dumps({
|
|
176
|
+
"error": "No sections found",
|
|
177
|
+
"message": "Requested sections were not available."
|
|
178
|
+
}, indent=2)
|
|
179
|
+
|
|
180
|
+
# Format as JSON for agent consumption
|
|
181
|
+
return json.dumps(result, indent=2, ensure_ascii=False)
|
|
182
|
+
|
|
183
|
+
def get_for_agent(self, agent_name: str) -> str:
|
|
184
|
+
"""
|
|
185
|
+
Get sections needed by specific agent.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
agent_name: Name of the agent (e.g., 'gitops-operator')
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Markdown string with agent-specific context
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
ValueError: If agent_name is not recognized
|
|
195
|
+
"""
|
|
196
|
+
if agent_name not in self.AGENT_SECTIONS:
|
|
197
|
+
available = ', '.join(self.AGENT_SECTIONS.keys())
|
|
198
|
+
raise ValueError(
|
|
199
|
+
f"Unknown agent: {agent_name}. "
|
|
200
|
+
f"Available agents: {available}"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
sections = self.AGENT_SECTIONS[agent_name]
|
|
204
|
+
return self.get_sections(sections)
|
|
205
|
+
|
|
206
|
+
def list_sections(self) -> List[str]:
|
|
207
|
+
"""Get list of all available sections."""
|
|
208
|
+
return list(self.sections.keys())
|
|
209
|
+
|
|
210
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
211
|
+
"""
|
|
212
|
+
Get statistics about the context file.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dictionary with size and token estimates
|
|
216
|
+
"""
|
|
217
|
+
# Calculate total JSON size
|
|
218
|
+
total_json = json.dumps(self.data, ensure_ascii=False)
|
|
219
|
+
total_chars = len(total_json)
|
|
220
|
+
total_tokens = total_chars // 4 # Rough estimate: 4 chars per token
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
'total_chars': total_chars,
|
|
224
|
+
'total_tokens_estimated': total_tokens,
|
|
225
|
+
'total_sections': len(self.sections),
|
|
226
|
+
'sections': {
|
|
227
|
+
name: {
|
|
228
|
+
'chars': len(json.dumps(content, ensure_ascii=False)),
|
|
229
|
+
'tokens_estimated': len(json.dumps(content, ensure_ascii=False)) // 4
|
|
230
|
+
}
|
|
231
|
+
for name, content in self.sections.items()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
def get_agent_stats(self, agent_name: str) -> Dict[str, Any]:
|
|
236
|
+
"""
|
|
237
|
+
Get statistics for a specific agent's context.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
agent_name: Name of the agent
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Dictionary with character and token counts for agent
|
|
244
|
+
"""
|
|
245
|
+
context = self.get_for_agent(agent_name)
|
|
246
|
+
chars = len(context)
|
|
247
|
+
tokens = chars // 4
|
|
248
|
+
|
|
249
|
+
full_stats = self.get_stats()
|
|
250
|
+
savings = {
|
|
251
|
+
'chars': full_stats['total_chars'] - chars,
|
|
252
|
+
'tokens': full_stats['total_tokens_estimated'] - tokens,
|
|
253
|
+
'percentage': round((1 - chars / full_stats['total_chars']) * 100, 1)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
'agent': agent_name,
|
|
258
|
+
'chars_loaded': chars,
|
|
259
|
+
'tokens_estimated': tokens,
|
|
260
|
+
'savings': savings
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def main():
|
|
265
|
+
"""CLI interface for testing and debugging."""
|
|
266
|
+
import sys
|
|
267
|
+
import json
|
|
268
|
+
|
|
269
|
+
reader = ContextSectionReader()
|
|
270
|
+
|
|
271
|
+
if len(sys.argv) < 2:
|
|
272
|
+
print("Context Section Reader")
|
|
273
|
+
print("\nUsage:")
|
|
274
|
+
print(" python context_section_reader.py <command> [args]")
|
|
275
|
+
print("\nCommands:")
|
|
276
|
+
print(" list - List all available sections")
|
|
277
|
+
print(" stats - Show statistics for context file")
|
|
278
|
+
print(" agent <name> - Get context for specific agent")
|
|
279
|
+
print(" agent-stats <name> - Show stats for agent's context")
|
|
280
|
+
print(" sections <name1> <name2> - Get specific sections")
|
|
281
|
+
print("\nAvailable agents:")
|
|
282
|
+
for agent in reader.AGENT_SECTIONS.keys():
|
|
283
|
+
print(f" - {agent}")
|
|
284
|
+
sys.exit(0)
|
|
285
|
+
|
|
286
|
+
command = sys.argv[1]
|
|
287
|
+
|
|
288
|
+
if command == 'list':
|
|
289
|
+
print("Available sections:")
|
|
290
|
+
for section in reader.list_sections():
|
|
291
|
+
print(f" - {section}")
|
|
292
|
+
|
|
293
|
+
elif command == 'stats':
|
|
294
|
+
stats = reader.get_stats()
|
|
295
|
+
print(json.dumps(stats, indent=2))
|
|
296
|
+
|
|
297
|
+
elif command == 'agent':
|
|
298
|
+
if len(sys.argv) < 3:
|
|
299
|
+
print("Error: Agent name required")
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
agent_name = sys.argv[2]
|
|
303
|
+
context = reader.get_for_agent(agent_name)
|
|
304
|
+
print(context)
|
|
305
|
+
|
|
306
|
+
elif command == 'agent-stats':
|
|
307
|
+
if len(sys.argv) < 3:
|
|
308
|
+
print("Error: Agent name required")
|
|
309
|
+
sys.exit(1)
|
|
310
|
+
|
|
311
|
+
agent_name = sys.argv[2]
|
|
312
|
+
stats = reader.get_agent_stats(agent_name)
|
|
313
|
+
print(json.dumps(stats, indent=2))
|
|
314
|
+
|
|
315
|
+
elif command == 'sections':
|
|
316
|
+
if len(sys.argv) < 3:
|
|
317
|
+
print("Error: Section names required")
|
|
318
|
+
sys.exit(1)
|
|
319
|
+
|
|
320
|
+
section_names = sys.argv[2:]
|
|
321
|
+
context = reader.get_sections(section_names)
|
|
322
|
+
print(context)
|
|
323
|
+
|
|
324
|
+
else:
|
|
325
|
+
print(f"Unknown command: {command}")
|
|
326
|
+
sys.exit(1)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
if __name__ == '__main__':
|
|
330
|
+
main()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deep merge utility for project-context.json updates.
|
|
3
|
+
|
|
4
|
+
Merges two dicts recursively following the gaia-ops merge decision tree:
|
|
5
|
+
1. Key missing in current -> ADD
|
|
6
|
+
2. Both values are dicts -> RECURSE (deep merge)
|
|
7
|
+
3. Both values are lists -> UNION (primitives: sorted set union;
|
|
8
|
+
dicts with "name": merge by name;
|
|
9
|
+
other dicts: concatenate + deduplicate)
|
|
10
|
+
4. Both values are scalars -> OVERWRITE (new replaces old)
|
|
11
|
+
5. Type mismatch -> OVERWRITE with warning
|
|
12
|
+
|
|
13
|
+
No-Delete Policy: keys in current but NOT in update are always preserved.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import copy
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def deep_merge(current: dict, update: dict) -> tuple[dict, dict]:
|
|
24
|
+
"""Merge *update* into *current* returning ``(merged, diff)``.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
current:
|
|
29
|
+
The existing data (will NOT be mutated).
|
|
30
|
+
update:
|
|
31
|
+
New data to merge on top of *current*.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
tuple[dict, dict]
|
|
36
|
+
``merged`` – the result of the merge.
|
|
37
|
+
``diff`` – audit trail recording changes (``{key: {old, new}}``).
|
|
38
|
+
"""
|
|
39
|
+
merged = copy.deepcopy(current)
|
|
40
|
+
diff: dict = {}
|
|
41
|
+
|
|
42
|
+
for key, new_value in update.items():
|
|
43
|
+
if key not in merged:
|
|
44
|
+
# Rule 1: ADD missing key
|
|
45
|
+
merged[key] = copy.deepcopy(new_value)
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
old_value = merged[key]
|
|
49
|
+
|
|
50
|
+
# Rule 2: Both dicts -> recurse
|
|
51
|
+
if isinstance(old_value, dict) and isinstance(new_value, dict):
|
|
52
|
+
sub_merged, sub_diff = deep_merge(old_value, new_value)
|
|
53
|
+
merged[key] = sub_merged
|
|
54
|
+
if sub_diff:
|
|
55
|
+
diff[key] = sub_diff
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Rule 3: Both lists -> union strategy
|
|
59
|
+
if isinstance(old_value, list) and isinstance(new_value, list):
|
|
60
|
+
merged_list = _merge_lists(old_value, new_value)
|
|
61
|
+
if merged_list != old_value:
|
|
62
|
+
diff[key] = {"old": old_value, "new": merged_list}
|
|
63
|
+
merged[key] = merged_list
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
# Rule 5: Type mismatch -> overwrite with warning
|
|
67
|
+
if type(old_value) is not type(new_value):
|
|
68
|
+
logger.warning(
|
|
69
|
+
"Type mismatch for key '%s': %s -> %s. New value wins.",
|
|
70
|
+
key,
|
|
71
|
+
type(old_value).__name__,
|
|
72
|
+
type(new_value).__name__,
|
|
73
|
+
)
|
|
74
|
+
diff[key] = {"old": old_value, "new": new_value}
|
|
75
|
+
merged[key] = copy.deepcopy(new_value)
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Rule 4: Both scalars -> overwrite
|
|
79
|
+
if old_value != new_value:
|
|
80
|
+
diff[key] = {"old": old_value, "new": new_value}
|
|
81
|
+
merged[key] = copy.deepcopy(new_value)
|
|
82
|
+
|
|
83
|
+
return merged, diff
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# List merge helpers
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def _merge_lists(current: list, update: list) -> list:
|
|
91
|
+
"""Merge two lists following the union strategy.
|
|
92
|
+
|
|
93
|
+
a) All items are primitives (str, int, float, bool) -> sorted set union.
|
|
94
|
+
b) Items are dicts with a ``"name"`` key -> merge by name, preserve missing.
|
|
95
|
+
c) Otherwise -> concatenate, deduplicate by JSON equality.
|
|
96
|
+
"""
|
|
97
|
+
if _all_primitives(current) and _all_primitives(update):
|
|
98
|
+
return sorted(set(current) | set(update))
|
|
99
|
+
|
|
100
|
+
if _all_dicts_with_name(current) and _all_dicts_with_name(update):
|
|
101
|
+
return _merge_named_dicts(current, update)
|
|
102
|
+
|
|
103
|
+
# Fallback: concatenate + deduplicate by JSON equality
|
|
104
|
+
return _concat_deduplicate(current, update)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _all_primitives(items: list) -> bool:
|
|
108
|
+
"""Return True if every item is a primitive (str, int, float, bool)."""
|
|
109
|
+
return all(isinstance(i, (str, int, float, bool)) for i in items)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _all_dicts_with_name(items: list) -> bool:
|
|
113
|
+
"""Return True if every item is a dict containing a ``"name"`` key."""
|
|
114
|
+
return bool(items) and all(
|
|
115
|
+
isinstance(i, dict) and "name" in i for i in items
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _merge_named_dicts(current: list[dict], update: list[dict]) -> list[dict]:
|
|
120
|
+
"""Merge lists of dicts by their ``"name"`` field.
|
|
121
|
+
|
|
122
|
+
- Matching names: deep-merge the dict fields.
|
|
123
|
+
- Names only in current: preserved (no-delete).
|
|
124
|
+
- Names only in update: appended.
|
|
125
|
+
"""
|
|
126
|
+
result_by_name: dict[str, dict] = {}
|
|
127
|
+
order: list[str] = []
|
|
128
|
+
|
|
129
|
+
# Seed with current entries (preserves order + no-delete)
|
|
130
|
+
for item in current:
|
|
131
|
+
name = item["name"]
|
|
132
|
+
result_by_name[name] = copy.deepcopy(item)
|
|
133
|
+
order.append(name)
|
|
134
|
+
|
|
135
|
+
# Merge / add from update
|
|
136
|
+
for item in update:
|
|
137
|
+
name = item["name"]
|
|
138
|
+
if name in result_by_name:
|
|
139
|
+
merged_item, _ = deep_merge(result_by_name[name], item)
|
|
140
|
+
result_by_name[name] = merged_item
|
|
141
|
+
else:
|
|
142
|
+
result_by_name[name] = copy.deepcopy(item)
|
|
143
|
+
order.append(name)
|
|
144
|
+
|
|
145
|
+
return [result_by_name[n] for n in order]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _concat_deduplicate(current: list, update: list) -> list:
|
|
149
|
+
"""Concatenate two lists, deduplicating by JSON equality."""
|
|
150
|
+
seen: list[str] = []
|
|
151
|
+
result: list = []
|
|
152
|
+
|
|
153
|
+
for item in current + update:
|
|
154
|
+
serialized = json.dumps(item, sort_keys=True)
|
|
155
|
+
if serialized not in seen:
|
|
156
|
+
seen.append(serialized)
|
|
157
|
+
result.append(copy.deepcopy(item))
|
|
158
|
+
|
|
159
|
+
return result
|