@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,760 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Pending Update Store for GAIA-OPS Agent Context Feedback Loop
|
|
4
|
+
|
|
5
|
+
This module manages pending update suggestions to project-context.json from agents.
|
|
6
|
+
It provides deduplication, approval workflow, and automatic application of changes.
|
|
7
|
+
|
|
8
|
+
Architecture:
|
|
9
|
+
- JSONL append-only audit trail for all events
|
|
10
|
+
- JSON index for fast queries and deduplication
|
|
11
|
+
- Content-based hashing for deduplication
|
|
12
|
+
- Atomic file operations for safety
|
|
13
|
+
- Automatic backup before applying changes
|
|
14
|
+
|
|
15
|
+
Storage layout:
|
|
16
|
+
pending-updates/
|
|
17
|
+
├── pending-updates.jsonl # Append-only audit trail
|
|
18
|
+
├── pending-index.json # Mutable index for fast queries
|
|
19
|
+
└── applied/ # Archive of applied updates
|
|
20
|
+
└── update-<id>.json
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import hashlib
|
|
25
|
+
import sys
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Dict, List, Any, Optional, Union
|
|
29
|
+
from dataclasses import dataclass, asdict, field
|
|
30
|
+
from enum import Enum
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DiscoveryCategory(str, Enum):
|
|
34
|
+
"""Categories of discoveries that can be made by agents."""
|
|
35
|
+
NEW_RESOURCE = "new_resource"
|
|
36
|
+
CONFIGURATION_ISSUE = "configuration_issue"
|
|
37
|
+
DRIFT_DETECTED = "drift_detected"
|
|
38
|
+
DEPENDENCY_DISCOVERED = "dependency_discovered"
|
|
39
|
+
TOPOLOGY_CHANGE = "topology_change"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UpdateStatus(str, Enum):
|
|
43
|
+
"""Status of a pending update."""
|
|
44
|
+
PENDING = "pending"
|
|
45
|
+
APPROVED = "approved"
|
|
46
|
+
REJECTED = "rejected"
|
|
47
|
+
APPLIED = "applied"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Mapping of categories to valid target sections
|
|
51
|
+
CATEGORY_TO_SECTIONS = {
|
|
52
|
+
"new_resource": ["application_services", "cluster_details", "infrastructure_topology"],
|
|
53
|
+
"configuration_issue": ["infrastructure", "terraform_infrastructure", "gitops_configuration", "application_services"],
|
|
54
|
+
"drift_detected": ["application_services", "cluster_details", "gitops_configuration", "terraform_infrastructure"],
|
|
55
|
+
"dependency_discovered": ["application_services", "infrastructure_topology"],
|
|
56
|
+
"topology_change": ["infrastructure_topology", "cluster_details"],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class DiscoveryResult:
|
|
62
|
+
"""Input DTO for creating a pending update."""
|
|
63
|
+
category: str
|
|
64
|
+
target_section: str
|
|
65
|
+
proposed_change: dict
|
|
66
|
+
summary: str
|
|
67
|
+
confidence: float
|
|
68
|
+
source_agent: str
|
|
69
|
+
source_task: str = ""
|
|
70
|
+
source_episode_id: str = ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class PendingUpdate:
|
|
75
|
+
"""Represents a pending update to project-context.json."""
|
|
76
|
+
update_id: str
|
|
77
|
+
content_hash: str
|
|
78
|
+
source_agent: str
|
|
79
|
+
source_task: str
|
|
80
|
+
source_episode_id: str
|
|
81
|
+
category: str
|
|
82
|
+
confidence: float
|
|
83
|
+
target_section: str
|
|
84
|
+
proposed_change: dict
|
|
85
|
+
summary: str
|
|
86
|
+
status: str
|
|
87
|
+
created_at: str
|
|
88
|
+
updated_at: str
|
|
89
|
+
seen_count: int
|
|
90
|
+
last_seen_at: str
|
|
91
|
+
seen_by_agents: List[str]
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
94
|
+
"""Convert to dictionary, removing None values."""
|
|
95
|
+
data = asdict(self)
|
|
96
|
+
return {k: v for k, v in data.items() if v is not None}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class PendingUpdateStore:
|
|
100
|
+
"""
|
|
101
|
+
Manages pending updates to project-context.json.
|
|
102
|
+
|
|
103
|
+
This class provides methods to:
|
|
104
|
+
- Create and deduplicate update suggestions
|
|
105
|
+
- Approve/reject updates
|
|
106
|
+
- Apply approved updates with automatic backup
|
|
107
|
+
- Track update statistics
|
|
108
|
+
- Maintain an efficient index for fast retrieval
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, base_path: Optional[Union[str, Path]] = None):
|
|
112
|
+
"""
|
|
113
|
+
Initialize PendingUpdateStore with specified or default path.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
base_path: Base directory for pending updates storage.
|
|
117
|
+
Defaults to .claude/project-context/pending-updates/
|
|
118
|
+
"""
|
|
119
|
+
if base_path:
|
|
120
|
+
self.base_path = Path(base_path)
|
|
121
|
+
else:
|
|
122
|
+
# Default location
|
|
123
|
+
self.base_path = Path(".claude/project-context/pending-updates")
|
|
124
|
+
|
|
125
|
+
self.updates_jsonl = self.base_path / "pending-updates.jsonl"
|
|
126
|
+
self.index_file = self.base_path / "pending-index.json"
|
|
127
|
+
self.applied_dir = self.base_path / "applied"
|
|
128
|
+
|
|
129
|
+
# Auto-create directories
|
|
130
|
+
self._ensure_directories()
|
|
131
|
+
|
|
132
|
+
def _ensure_directories(self):
|
|
133
|
+
"""Create required directories if they don't exist."""
|
|
134
|
+
self.base_path.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
self.applied_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
if not self.index_file.exists():
|
|
138
|
+
self._save_index({
|
|
139
|
+
"version": "1.0.0",
|
|
140
|
+
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
141
|
+
"total_count": 0,
|
|
142
|
+
"pending_count": 0,
|
|
143
|
+
"updates": {},
|
|
144
|
+
"hash_index": {}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
def _save_index(self, index_data: Dict[str, Any]):
|
|
148
|
+
"""Save index to JSON file."""
|
|
149
|
+
with open(self.index_file, 'w') as f:
|
|
150
|
+
json.dump(index_data, f, indent=2)
|
|
151
|
+
|
|
152
|
+
def _load_index(self) -> Dict[str, Any]:
|
|
153
|
+
"""Load index from JSON file."""
|
|
154
|
+
if not self.index_file.exists():
|
|
155
|
+
return {
|
|
156
|
+
"version": "1.0.0",
|
|
157
|
+
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
158
|
+
"total_count": 0,
|
|
159
|
+
"pending_count": 0,
|
|
160
|
+
"updates": {},
|
|
161
|
+
"hash_index": {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
with open(self.index_file, 'r') as f:
|
|
166
|
+
return json.load(f)
|
|
167
|
+
except (json.JSONDecodeError, IOError):
|
|
168
|
+
# Return empty index if file is corrupted
|
|
169
|
+
return {
|
|
170
|
+
"version": "1.0.0",
|
|
171
|
+
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
172
|
+
"total_count": 0,
|
|
173
|
+
"pending_count": 0,
|
|
174
|
+
"updates": {},
|
|
175
|
+
"hash_index": {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def _compute_hash(self, target_section: str, proposed_change: dict) -> str:
|
|
179
|
+
"""
|
|
180
|
+
Compute content hash for deduplication.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
target_section: Target section in project-context.json
|
|
184
|
+
proposed_change: Proposed change dictionary
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
SHA-256 hash (first 12 characters)
|
|
188
|
+
"""
|
|
189
|
+
content = json.dumps({
|
|
190
|
+
"section": target_section,
|
|
191
|
+
"change": proposed_change
|
|
192
|
+
}, sort_keys=True)
|
|
193
|
+
|
|
194
|
+
hash_full = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
|
195
|
+
return hash_full[:12]
|
|
196
|
+
|
|
197
|
+
def _log_event(self, event: Dict[str, Any]):
|
|
198
|
+
"""Append event to JSONL audit trail."""
|
|
199
|
+
with open(self.updates_jsonl, 'a') as f:
|
|
200
|
+
f.write(json.dumps(event) + '\n')
|
|
201
|
+
|
|
202
|
+
def _validate_discovery(self, discovery: DiscoveryResult) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Validate discovery result.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
discovery: Discovery result to validate
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
True if valid, False otherwise
|
|
211
|
+
"""
|
|
212
|
+
if discovery.confidence < 0.7:
|
|
213
|
+
print(f"Error: Confidence {discovery.confidence} is below threshold 0.7", file=sys.stderr)
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
if discovery.category not in CATEGORY_TO_SECTIONS:
|
|
217
|
+
print(f"Error: Invalid category '{discovery.category}'", file=sys.stderr)
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
valid_sections = CATEGORY_TO_SECTIONS[discovery.category]
|
|
221
|
+
if discovery.target_section not in valid_sections:
|
|
222
|
+
print(f"Error: Invalid target section '{discovery.target_section}' for category '{discovery.category}'", file=sys.stderr)
|
|
223
|
+
print(f"Valid sections: {valid_sections}", file=sys.stderr)
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
def create(self, discovery: DiscoveryResult) -> str:
|
|
229
|
+
"""
|
|
230
|
+
Create a new pending update or deduplicate with existing.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
discovery: Discovery result from agent
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Update ID (new or deduplicated)
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
ValueError: If discovery is invalid
|
|
240
|
+
"""
|
|
241
|
+
if not self._validate_discovery(discovery):
|
|
242
|
+
raise ValueError("Invalid discovery result")
|
|
243
|
+
|
|
244
|
+
content_hash = self._compute_hash(discovery.target_section, discovery.proposed_change)
|
|
245
|
+
|
|
246
|
+
index = self._load_index()
|
|
247
|
+
|
|
248
|
+
existing_id = index["hash_index"].get(content_hash)
|
|
249
|
+
|
|
250
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
251
|
+
|
|
252
|
+
if existing_id and existing_id in index["updates"]:
|
|
253
|
+
# Deduplicate - increment seen_count
|
|
254
|
+
existing = index["updates"][existing_id]
|
|
255
|
+
existing["seen_count"] += 1
|
|
256
|
+
existing["last_seen_at"] = now
|
|
257
|
+
existing["updated_at"] = now
|
|
258
|
+
|
|
259
|
+
# Add source agent to seen_by_agents if not already present
|
|
260
|
+
if discovery.source_agent not in existing["seen_by_agents"]:
|
|
261
|
+
existing["seen_by_agents"].append(discovery.source_agent)
|
|
262
|
+
|
|
263
|
+
self._save_index(index)
|
|
264
|
+
|
|
265
|
+
self._log_event({
|
|
266
|
+
"event": "dedup_increment",
|
|
267
|
+
"update_id": existing_id,
|
|
268
|
+
"timestamp": now,
|
|
269
|
+
"seen_count": existing["seen_count"],
|
|
270
|
+
"source_agent": discovery.source_agent
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
print(f"Deduplicated update: {existing_id} (seen_count={existing['seen_count']})", file=sys.stderr)
|
|
274
|
+
return existing_id
|
|
275
|
+
|
|
276
|
+
update_id = f"pu_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{content_hash[:4]}"
|
|
277
|
+
|
|
278
|
+
update = PendingUpdate(
|
|
279
|
+
update_id=update_id,
|
|
280
|
+
content_hash=content_hash,
|
|
281
|
+
source_agent=discovery.source_agent,
|
|
282
|
+
source_task=discovery.source_task,
|
|
283
|
+
source_episode_id=discovery.source_episode_id,
|
|
284
|
+
category=discovery.category,
|
|
285
|
+
confidence=discovery.confidence,
|
|
286
|
+
target_section=discovery.target_section,
|
|
287
|
+
proposed_change=discovery.proposed_change,
|
|
288
|
+
summary=discovery.summary,
|
|
289
|
+
status=UpdateStatus.PENDING.value,
|
|
290
|
+
created_at=now,
|
|
291
|
+
updated_at=now,
|
|
292
|
+
seen_count=1,
|
|
293
|
+
last_seen_at=now,
|
|
294
|
+
seen_by_agents=[discovery.source_agent]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
index["updates"][update_id] = update.to_dict()
|
|
298
|
+
index["hash_index"][content_hash] = update_id
|
|
299
|
+
index["total_count"] += 1
|
|
300
|
+
index["pending_count"] += 1
|
|
301
|
+
index["last_updated"] = now
|
|
302
|
+
|
|
303
|
+
self._save_index(index)
|
|
304
|
+
|
|
305
|
+
self._log_event({
|
|
306
|
+
"event": "created",
|
|
307
|
+
"update_id": update_id,
|
|
308
|
+
"timestamp": now,
|
|
309
|
+
"data": update.to_dict()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
print(f"Created pending update: {update_id}", file=sys.stderr)
|
|
313
|
+
return update_id
|
|
314
|
+
|
|
315
|
+
def get(self, update_id: str) -> Optional[PendingUpdate]:
|
|
316
|
+
"""
|
|
317
|
+
Get a specific pending update by ID.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
update_id: Update ID to retrieve
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
PendingUpdate or None if not found
|
|
324
|
+
"""
|
|
325
|
+
index = self._load_index()
|
|
326
|
+
update_data = index["updates"].get(update_id)
|
|
327
|
+
|
|
328
|
+
if not update_data:
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
return PendingUpdate(**update_data)
|
|
332
|
+
|
|
333
|
+
def list_pending(self) -> List[PendingUpdate]:
|
|
334
|
+
"""
|
|
335
|
+
List all pending updates, ordered by created_at descending.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
List of PendingUpdate objects with status=pending
|
|
339
|
+
"""
|
|
340
|
+
index = self._load_index()
|
|
341
|
+
pending = [
|
|
342
|
+
PendingUpdate(**data)
|
|
343
|
+
for data in index["updates"].values()
|
|
344
|
+
if data["status"] == UpdateStatus.PENDING.value
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
# Sort by created_at descending
|
|
348
|
+
pending.sort(key=lambda x: x.created_at, reverse=True)
|
|
349
|
+
return pending
|
|
350
|
+
|
|
351
|
+
def list_all(self, status: Optional[str] = None) -> List[PendingUpdate]:
|
|
352
|
+
"""
|
|
353
|
+
List all updates with optional status filter.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
status: Optional status filter
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
List of PendingUpdate objects
|
|
360
|
+
"""
|
|
361
|
+
index = self._load_index()
|
|
362
|
+
updates = []
|
|
363
|
+
|
|
364
|
+
for data in index["updates"].values():
|
|
365
|
+
if status is None or data["status"] == status:
|
|
366
|
+
updates.append(PendingUpdate(**data))
|
|
367
|
+
|
|
368
|
+
# Sort by created_at descending
|
|
369
|
+
updates.sort(key=lambda x: x.created_at, reverse=True)
|
|
370
|
+
return updates
|
|
371
|
+
|
|
372
|
+
def approve(self, update_id: str) -> PendingUpdate:
|
|
373
|
+
"""
|
|
374
|
+
Approve a pending update.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
update_id: Update ID to approve
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Updated PendingUpdate
|
|
381
|
+
|
|
382
|
+
Raises:
|
|
383
|
+
ValueError: If update not found or not pending
|
|
384
|
+
"""
|
|
385
|
+
index = self._load_index()
|
|
386
|
+
update_data = index["updates"].get(update_id)
|
|
387
|
+
|
|
388
|
+
if not update_data:
|
|
389
|
+
raise ValueError(f"Update {update_id} not found")
|
|
390
|
+
|
|
391
|
+
if update_data["status"] != UpdateStatus.PENDING.value:
|
|
392
|
+
raise ValueError(f"Update {update_id} is not pending (status={update_data['status']})")
|
|
393
|
+
|
|
394
|
+
old_status = update_data["status"]
|
|
395
|
+
update_data["status"] = UpdateStatus.APPROVED.value
|
|
396
|
+
update_data["updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
397
|
+
|
|
398
|
+
index["pending_count"] -= 1
|
|
399
|
+
index["last_updated"] = update_data["updated_at"]
|
|
400
|
+
|
|
401
|
+
self._save_index(index)
|
|
402
|
+
|
|
403
|
+
self._log_event({
|
|
404
|
+
"event": "status_change",
|
|
405
|
+
"update_id": update_id,
|
|
406
|
+
"timestamp": update_data["updated_at"],
|
|
407
|
+
"old_status": old_status,
|
|
408
|
+
"new_status": UpdateStatus.APPROVED.value
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
print(f"Approved update: {update_id}", file=sys.stderr)
|
|
412
|
+
return PendingUpdate(**update_data)
|
|
413
|
+
|
|
414
|
+
def reject(self, update_id: str) -> PendingUpdate:
|
|
415
|
+
"""
|
|
416
|
+
Reject a pending update.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
update_id: Update ID to reject
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Updated PendingUpdate
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
ValueError: If update not found or not pending
|
|
426
|
+
"""
|
|
427
|
+
index = self._load_index()
|
|
428
|
+
update_data = index["updates"].get(update_id)
|
|
429
|
+
|
|
430
|
+
if not update_data:
|
|
431
|
+
raise ValueError(f"Update {update_id} not found")
|
|
432
|
+
|
|
433
|
+
if update_data["status"] != UpdateStatus.PENDING.value:
|
|
434
|
+
raise ValueError(f"Update {update_id} is not pending (status={update_data['status']})")
|
|
435
|
+
|
|
436
|
+
old_status = update_data["status"]
|
|
437
|
+
update_data["status"] = UpdateStatus.REJECTED.value
|
|
438
|
+
update_data["updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
439
|
+
|
|
440
|
+
index["pending_count"] -= 1
|
|
441
|
+
index["last_updated"] = update_data["updated_at"]
|
|
442
|
+
|
|
443
|
+
self._save_index(index)
|
|
444
|
+
|
|
445
|
+
self._log_event({
|
|
446
|
+
"event": "status_change",
|
|
447
|
+
"update_id": update_id,
|
|
448
|
+
"timestamp": update_data["updated_at"],
|
|
449
|
+
"old_status": old_status,
|
|
450
|
+
"new_status": UpdateStatus.REJECTED.value
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
print(f"Rejected update: {update_id}", file=sys.stderr)
|
|
454
|
+
return PendingUpdate(**update_data)
|
|
455
|
+
|
|
456
|
+
def apply(self, update_id: str, context_path: Optional[Union[str, Path]] = None) -> dict:
|
|
457
|
+
"""
|
|
458
|
+
Apply an approved update to project-context.json.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
update_id: Update ID to apply
|
|
462
|
+
context_path: Path to project-context.json (defaults to standard location)
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Dict with result: {success: bool, update_id: str, target_section: str, backup_path: str}
|
|
466
|
+
|
|
467
|
+
Raises:
|
|
468
|
+
ValueError: If update not found or not approved
|
|
469
|
+
"""
|
|
470
|
+
index = self._load_index()
|
|
471
|
+
update_data = index["updates"].get(update_id)
|
|
472
|
+
|
|
473
|
+
if not update_data:
|
|
474
|
+
raise ValueError(f"Update {update_id} not found")
|
|
475
|
+
|
|
476
|
+
if update_data["status"] != UpdateStatus.APPROVED.value:
|
|
477
|
+
raise ValueError(f"Update {update_id} is not approved (status={update_data['status']})")
|
|
478
|
+
|
|
479
|
+
if context_path:
|
|
480
|
+
context_file = Path(context_path)
|
|
481
|
+
else:
|
|
482
|
+
context_file = Path(".claude/project-context/project-context.json")
|
|
483
|
+
|
|
484
|
+
if not context_file.exists():
|
|
485
|
+
raise ValueError(f"Project context file not found: {context_file}")
|
|
486
|
+
|
|
487
|
+
try:
|
|
488
|
+
with open(context_file, 'r') as f:
|
|
489
|
+
context_data = json.load(f)
|
|
490
|
+
|
|
491
|
+
if "sections" not in context_data:
|
|
492
|
+
raise ValueError("Invalid project-context.json: missing 'sections' key")
|
|
493
|
+
|
|
494
|
+
target_section = update_data["target_section"]
|
|
495
|
+
if target_section not in context_data["sections"]:
|
|
496
|
+
raise ValueError(f"Target section '{target_section}' not found in project-context.json")
|
|
497
|
+
|
|
498
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
499
|
+
backup_path = context_file.parent / f"project-context.backup.{timestamp}.json"
|
|
500
|
+
with open(backup_path, 'w') as f:
|
|
501
|
+
json.dump(context_data, f, indent=2)
|
|
502
|
+
|
|
503
|
+
section_data = context_data["sections"][target_section]
|
|
504
|
+
proposed_change = update_data["proposed_change"]
|
|
505
|
+
|
|
506
|
+
# Simple merge: update keys from proposed_change
|
|
507
|
+
self._merge_dicts(section_data, proposed_change)
|
|
508
|
+
|
|
509
|
+
if "metadata" not in context_data:
|
|
510
|
+
context_data["metadata"] = {}
|
|
511
|
+
context_data["metadata"]["last_updated"] = datetime.now(timezone.utc).isoformat()
|
|
512
|
+
|
|
513
|
+
# Atomic write: write to temp file then rename
|
|
514
|
+
temp_file = context_file.parent / f".{context_file.name}.tmp"
|
|
515
|
+
with open(temp_file, 'w') as f:
|
|
516
|
+
json.dump(context_data, f, indent=2)
|
|
517
|
+
temp_file.rename(context_file)
|
|
518
|
+
|
|
519
|
+
old_status = update_data["status"]
|
|
520
|
+
update_data["status"] = UpdateStatus.APPLIED.value
|
|
521
|
+
update_data["updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
522
|
+
|
|
523
|
+
index["last_updated"] = update_data["updated_at"]
|
|
524
|
+
self._save_index(index)
|
|
525
|
+
|
|
526
|
+
applied_file = self.applied_dir / f"update-{update_id}.json"
|
|
527
|
+
with open(applied_file, 'w') as f:
|
|
528
|
+
json.dump(update_data, f, indent=2)
|
|
529
|
+
|
|
530
|
+
self._log_event({
|
|
531
|
+
"event": "status_change",
|
|
532
|
+
"update_id": update_id,
|
|
533
|
+
"timestamp": update_data["updated_at"],
|
|
534
|
+
"old_status": old_status,
|
|
535
|
+
"new_status": UpdateStatus.APPLIED.value
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
print(f"Applied update {update_id} to {target_section}", file=sys.stderr)
|
|
539
|
+
print(f"Backup saved: {backup_path}", file=sys.stderr)
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
"success": True,
|
|
543
|
+
"update_id": update_id,
|
|
544
|
+
"target_section": target_section,
|
|
545
|
+
"backup_path": str(backup_path)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
except Exception as e:
|
|
549
|
+
print(f"Error applying update {update_id}: {e}", file=sys.stderr)
|
|
550
|
+
raise
|
|
551
|
+
|
|
552
|
+
def _merge_dicts(self, target: dict, source: dict):
|
|
553
|
+
"""
|
|
554
|
+
Recursively merge source dict into target dict.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
target: Target dictionary to merge into
|
|
558
|
+
source: Source dictionary to merge from
|
|
559
|
+
"""
|
|
560
|
+
for key, value in source.items():
|
|
561
|
+
if isinstance(value, dict) and key in target and isinstance(target[key], dict):
|
|
562
|
+
# Recursive merge for nested dicts
|
|
563
|
+
self._merge_dicts(target[key], value)
|
|
564
|
+
else:
|
|
565
|
+
# Overwrite or add key
|
|
566
|
+
target[key] = value
|
|
567
|
+
|
|
568
|
+
def get_statistics(self) -> dict:
|
|
569
|
+
"""
|
|
570
|
+
Get statistics about pending updates.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Dict with counts by status, category, and agent
|
|
574
|
+
"""
|
|
575
|
+
index = self._load_index()
|
|
576
|
+
|
|
577
|
+
stats = {
|
|
578
|
+
"total_count": index["total_count"],
|
|
579
|
+
"pending_count": index["pending_count"],
|
|
580
|
+
"by_status": {},
|
|
581
|
+
"by_category": {},
|
|
582
|
+
"by_agent": {}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
for update_data in index["updates"].values():
|
|
586
|
+
status = update_data["status"]
|
|
587
|
+
category = update_data["category"]
|
|
588
|
+
agent = update_data["source_agent"]
|
|
589
|
+
|
|
590
|
+
stats["by_status"][status] = stats["by_status"].get(status, 0) + 1
|
|
591
|
+
stats["by_category"][category] = stats["by_category"].get(category, 0) + 1
|
|
592
|
+
stats["by_agent"][agent] = stats["by_agent"].get(agent, 0) + 1
|
|
593
|
+
|
|
594
|
+
return stats
|
|
595
|
+
|
|
596
|
+
def get_pending_count(self) -> int:
|
|
597
|
+
"""
|
|
598
|
+
Get count of pending updates (fast path from index).
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
Number of pending updates
|
|
602
|
+
"""
|
|
603
|
+
index = self._load_index()
|
|
604
|
+
return index["pending_count"]
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# CLI interface for testing and management
|
|
608
|
+
if __name__ == "__main__":
|
|
609
|
+
import argparse
|
|
610
|
+
|
|
611
|
+
parser = argparse.ArgumentParser(description="Pending Update Store Management")
|
|
612
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
613
|
+
|
|
614
|
+
# Create command
|
|
615
|
+
create_parser = subparsers.add_parser("create", help="Create a new pending update")
|
|
616
|
+
create_parser.add_argument("--category", required=True, choices=list(CATEGORY_TO_SECTIONS.keys()), help="Discovery category")
|
|
617
|
+
create_parser.add_argument("--section", required=True, help="Target section")
|
|
618
|
+
create_parser.add_argument("--change", required=True, help="Proposed change (JSON string)")
|
|
619
|
+
create_parser.add_argument("--summary", required=True, help="Summary of change")
|
|
620
|
+
create_parser.add_argument("--confidence", type=float, default=0.8, help="Confidence score")
|
|
621
|
+
create_parser.add_argument("--agent", required=True, help="Source agent")
|
|
622
|
+
create_parser.add_argument("--task", default="", help="Source task")
|
|
623
|
+
|
|
624
|
+
# List command
|
|
625
|
+
list_parser = subparsers.add_parser("list", help="List pending updates")
|
|
626
|
+
list_parser.add_argument("--status", choices=["pending", "approved", "rejected", "applied"], help="Filter by status")
|
|
627
|
+
|
|
628
|
+
# Get command
|
|
629
|
+
get_parser = subparsers.add_parser("get", help="Get a specific update")
|
|
630
|
+
get_parser.add_argument("update_id", help="Update ID")
|
|
631
|
+
|
|
632
|
+
# Approve command
|
|
633
|
+
approve_parser = subparsers.add_parser("approve", help="Approve a pending update")
|
|
634
|
+
approve_parser.add_argument("update_id", help="Update ID")
|
|
635
|
+
|
|
636
|
+
# Reject command
|
|
637
|
+
reject_parser = subparsers.add_parser("reject", help="Reject a pending update")
|
|
638
|
+
reject_parser.add_argument("update_id", help="Update ID")
|
|
639
|
+
|
|
640
|
+
# Apply command
|
|
641
|
+
apply_parser = subparsers.add_parser("apply", help="Apply an approved update")
|
|
642
|
+
apply_parser.add_argument("update_id", help="Update ID")
|
|
643
|
+
apply_parser.add_argument("--context", help="Path to project-context.json")
|
|
644
|
+
|
|
645
|
+
# Stats command
|
|
646
|
+
stats_parser = subparsers.add_parser("stats", help="Show statistics")
|
|
647
|
+
|
|
648
|
+
args = parser.parse_args()
|
|
649
|
+
|
|
650
|
+
store = PendingUpdateStore()
|
|
651
|
+
|
|
652
|
+
if args.command == "create":
|
|
653
|
+
try:
|
|
654
|
+
proposed_change = json.loads(args.change)
|
|
655
|
+
discovery = DiscoveryResult(
|
|
656
|
+
category=args.category,
|
|
657
|
+
target_section=args.section,
|
|
658
|
+
proposed_change=proposed_change,
|
|
659
|
+
summary=args.summary,
|
|
660
|
+
confidence=args.confidence,
|
|
661
|
+
source_agent=args.agent,
|
|
662
|
+
source_task=args.task
|
|
663
|
+
)
|
|
664
|
+
update_id = store.create(discovery)
|
|
665
|
+
print(f"Created update: {update_id}")
|
|
666
|
+
except Exception as e:
|
|
667
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
668
|
+
sys.exit(1)
|
|
669
|
+
|
|
670
|
+
elif args.command == "list":
|
|
671
|
+
if args.status:
|
|
672
|
+
updates = store.list_all(status=args.status)
|
|
673
|
+
else:
|
|
674
|
+
updates = store.list_pending()
|
|
675
|
+
|
|
676
|
+
if not updates:
|
|
677
|
+
print("No updates found")
|
|
678
|
+
else:
|
|
679
|
+
print(f"\nFound {len(updates)} update(s):\n")
|
|
680
|
+
for update in updates:
|
|
681
|
+
print(f"ID: {update.update_id}")
|
|
682
|
+
print(f" Status: {update.status}")
|
|
683
|
+
print(f" Category: {update.category}")
|
|
684
|
+
print(f" Section: {update.target_section}")
|
|
685
|
+
print(f" Agent: {update.source_agent}")
|
|
686
|
+
print(f" Confidence: {update.confidence}")
|
|
687
|
+
print(f" Seen: {update.seen_count} time(s)")
|
|
688
|
+
print(f" Summary: {update.summary}")
|
|
689
|
+
print(f" Created: {update.created_at}")
|
|
690
|
+
print()
|
|
691
|
+
|
|
692
|
+
elif args.command == "get":
|
|
693
|
+
update = store.get(args.update_id)
|
|
694
|
+
if not update:
|
|
695
|
+
print(f"Update {args.update_id} not found", file=sys.stderr)
|
|
696
|
+
sys.exit(1)
|
|
697
|
+
|
|
698
|
+
print(f"\nUpdate: {update.update_id}")
|
|
699
|
+
print(f" Status: {update.status}")
|
|
700
|
+
print(f" Category: {update.category}")
|
|
701
|
+
print(f" Section: {update.target_section}")
|
|
702
|
+
print(f" Agent: {update.source_agent}")
|
|
703
|
+
print(f" Task: {update.source_task}")
|
|
704
|
+
print(f" Confidence: {update.confidence}")
|
|
705
|
+
print(f" Seen: {update.seen_count} time(s) by {update.seen_by_agents}")
|
|
706
|
+
print(f" Summary: {update.summary}")
|
|
707
|
+
print(f" Created: {update.created_at}")
|
|
708
|
+
print(f" Updated: {update.updated_at}")
|
|
709
|
+
print(f"\n Proposed change:")
|
|
710
|
+
print(f" {json.dumps(update.proposed_change, indent=4)}")
|
|
711
|
+
|
|
712
|
+
elif args.command == "approve":
|
|
713
|
+
try:
|
|
714
|
+
update = store.approve(args.update_id)
|
|
715
|
+
print(f"Approved update: {update.update_id}")
|
|
716
|
+
except ValueError as e:
|
|
717
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
718
|
+
sys.exit(1)
|
|
719
|
+
|
|
720
|
+
elif args.command == "reject":
|
|
721
|
+
try:
|
|
722
|
+
update = store.reject(args.update_id)
|
|
723
|
+
print(f"Rejected update: {update.update_id}")
|
|
724
|
+
except ValueError as e:
|
|
725
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
726
|
+
sys.exit(1)
|
|
727
|
+
|
|
728
|
+
elif args.command == "apply":
|
|
729
|
+
try:
|
|
730
|
+
result = store.apply(args.update_id, context_path=args.context)
|
|
731
|
+
print(f"Successfully applied update: {result['update_id']}")
|
|
732
|
+
print(f" Section: {result['target_section']}")
|
|
733
|
+
print(f" Backup: {result['backup_path']}")
|
|
734
|
+
except Exception as e:
|
|
735
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
736
|
+
sys.exit(1)
|
|
737
|
+
|
|
738
|
+
elif args.command == "stats":
|
|
739
|
+
stats = store.get_statistics()
|
|
740
|
+
print("\nPending Update Statistics:")
|
|
741
|
+
print(f" Total updates: {stats['total_count']}")
|
|
742
|
+
print(f" Pending: {stats['pending_count']}")
|
|
743
|
+
|
|
744
|
+
if stats["by_status"]:
|
|
745
|
+
print("\n By status:")
|
|
746
|
+
for status, count in stats["by_status"].items():
|
|
747
|
+
print(f" {status}: {count}")
|
|
748
|
+
|
|
749
|
+
if stats["by_category"]:
|
|
750
|
+
print("\n By category:")
|
|
751
|
+
for category, count in stats["by_category"].items():
|
|
752
|
+
print(f" {category}: {count}")
|
|
753
|
+
|
|
754
|
+
if stats["by_agent"]:
|
|
755
|
+
print("\n By agent:")
|
|
756
|
+
for agent, count in stats["by_agent"].items():
|
|
757
|
+
print(f" {agent}: {count}")
|
|
758
|
+
|
|
759
|
+
else:
|
|
760
|
+
parser.print_help()
|