@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,624 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rail UI for gaia-scan
|
|
3
|
+
|
|
4
|
+
Clack-style rail output for scan results. Zero prompts. Fully automatic.
|
|
5
|
+
All output goes to stderr. stdout is reserved for JSON only.
|
|
6
|
+
|
|
7
|
+
Classes:
|
|
8
|
+
- RailUI: Clack-style rail output renderer
|
|
9
|
+
|
|
10
|
+
Functions:
|
|
11
|
+
- format_scanner_results: Transform raw scan output into display sections
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# ANSI color helpers
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
def _supports_color() -> bool:
|
|
24
|
+
"""Check if the terminal supports ANSI colors."""
|
|
25
|
+
if os.environ.get("NO_COLOR"):
|
|
26
|
+
return False
|
|
27
|
+
if not hasattr(sys.stderr, "isatty"):
|
|
28
|
+
return False
|
|
29
|
+
return sys.stderr.isatty()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_COLOR = _supports_color()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _c(code: str, text: str) -> str:
|
|
36
|
+
"""Apply ANSI color code if color is supported."""
|
|
37
|
+
return f"\033[{code}m{text}\033[0m" if _COLOR else text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cyan(text: str) -> str:
|
|
41
|
+
return _c("36", text)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _green(text: str) -> str:
|
|
45
|
+
return _c("32", text)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _yellow(text: str) -> str:
|
|
49
|
+
return _c("33", text)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _dim(text: str) -> str:
|
|
53
|
+
return _c("2", text)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _bold(text: str) -> str:
|
|
57
|
+
return _c("1", text)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Rail UI
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
class RailUI:
|
|
65
|
+
"""Clack-style rail output for scan results.
|
|
66
|
+
|
|
67
|
+
All output goes to stderr. The rail character is dimmed.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
version: Scanner version string for the header.
|
|
71
|
+
color: Whether to use ANSI colors (overrides auto-detect).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, version: str, color: Optional[bool] = None):
|
|
75
|
+
self.version = version
|
|
76
|
+
self._color = color if color is not None else _COLOR
|
|
77
|
+
|
|
78
|
+
def _c(self, code: str, text: str) -> str:
|
|
79
|
+
"""Apply ANSI color code if color is enabled for this instance."""
|
|
80
|
+
return f"\033[{code}m{text}\033[0m" if self._color else text
|
|
81
|
+
|
|
82
|
+
def _cyan(self, text: str) -> str:
|
|
83
|
+
return self._c("36", text)
|
|
84
|
+
|
|
85
|
+
def _green(self, text: str) -> str:
|
|
86
|
+
return self._c("32", text)
|
|
87
|
+
|
|
88
|
+
def _yellow(self, text: str) -> str:
|
|
89
|
+
return self._c("33", text)
|
|
90
|
+
|
|
91
|
+
def _dim(self, text: str) -> str:
|
|
92
|
+
return self._c("2", text)
|
|
93
|
+
|
|
94
|
+
def _bold(self, text: str) -> str:
|
|
95
|
+
return self._c("1", text)
|
|
96
|
+
|
|
97
|
+
def _rail(self) -> str:
|
|
98
|
+
"""Return the dimmed rail character."""
|
|
99
|
+
return self._dim("\u2502")
|
|
100
|
+
|
|
101
|
+
def _write(self, text: str) -> None:
|
|
102
|
+
"""Write a line to stderr."""
|
|
103
|
+
print(text, file=sys.stderr)
|
|
104
|
+
|
|
105
|
+
def start(self) -> None:
|
|
106
|
+
"""Print the header: top-left corner + version."""
|
|
107
|
+
self._write(self._cyan(f"\u250c gaia-scan v{self.version}"))
|
|
108
|
+
self._write(self._rail())
|
|
109
|
+
|
|
110
|
+
def scanning(self) -> None:
|
|
111
|
+
"""Print the scanning indicator."""
|
|
112
|
+
self._write(self._cyan(f"\u25d2 Scanning..."))
|
|
113
|
+
self._write(self._rail())
|
|
114
|
+
|
|
115
|
+
def section(self, name: str, lines: List[str]) -> None:
|
|
116
|
+
"""Print a section with its detail lines.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
name: Section title (e.g. "Stack", "Infrastructure").
|
|
120
|
+
lines: Detail lines to display under the section.
|
|
121
|
+
"""
|
|
122
|
+
self._write(f"{self._green('\u25c7')} {self._cyan(name)}")
|
|
123
|
+
for line in lines:
|
|
124
|
+
self._write(f"{self._rail()} {line}")
|
|
125
|
+
self._write(self._rail())
|
|
126
|
+
|
|
127
|
+
def section_compact(self, names: List[str]) -> None:
|
|
128
|
+
"""Print multiple section names on a single line (scan-only mode).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
names: List of section names to join with middle-dot.
|
|
132
|
+
"""
|
|
133
|
+
joined = self._cyan(" \u00b7 ".join(names))
|
|
134
|
+
self._write(f"{self._green('\u25c7')} {joined}")
|
|
135
|
+
self._write(self._rail())
|
|
136
|
+
|
|
137
|
+
def warning(self, count: int, messages: List[str]) -> None:
|
|
138
|
+
"""Print warnings section.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
count: Total number of warnings.
|
|
142
|
+
messages: Warning messages to display.
|
|
143
|
+
"""
|
|
144
|
+
self._write(f"{self._yellow('\u26a0')} {self._yellow(f'Warnings ({count})')}")
|
|
145
|
+
for msg in messages:
|
|
146
|
+
self._write(f"{self._rail()} {msg}")
|
|
147
|
+
self._write(self._rail())
|
|
148
|
+
|
|
149
|
+
def done(self, duration_s: float, suffix: str = "") -> None:
|
|
150
|
+
"""Print the done marker with duration.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
duration_s: Scan duration in seconds.
|
|
154
|
+
suffix: Optional text to append after duration.
|
|
155
|
+
"""
|
|
156
|
+
text = f"\u25c6 Done in {duration_s:.1f}s"
|
|
157
|
+
if suffix:
|
|
158
|
+
text += f" \u00b7 {suffix}"
|
|
159
|
+
self._write(self._green(text))
|
|
160
|
+
self._write(self._rail())
|
|
161
|
+
|
|
162
|
+
def created(self, items: Dict[str, str]) -> None:
|
|
163
|
+
"""Print the 'Created:' summary for fresh installs.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
items: Dict of {name: description} for created items.
|
|
167
|
+
"""
|
|
168
|
+
self._write(f"{self._rail()} Created:")
|
|
169
|
+
for name, desc in items.items():
|
|
170
|
+
self._write(f"{self._rail()} {name:<18s} {self._dim(desc)}")
|
|
171
|
+
self._write(self._rail())
|
|
172
|
+
|
|
173
|
+
def updated(self, sections_updated: int, sections_preserved: int) -> None:
|
|
174
|
+
"""Print the 'Updated/Preserved' summary for rescans.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
sections_updated: Number of scanner-updated sections.
|
|
178
|
+
sections_preserved: Number of agent-enriched preserved sections.
|
|
179
|
+
"""
|
|
180
|
+
self._write(f"{self._rail()} Updated: {sections_updated} sections")
|
|
181
|
+
self._write(f"{self._rail()} Preserved: {sections_preserved} agent-enriched sections")
|
|
182
|
+
self._write(f"{self._rail()} Synced: CLAUDE.md, settings.json")
|
|
183
|
+
self._write(self._rail())
|
|
184
|
+
|
|
185
|
+
def footer(self, message: str) -> None:
|
|
186
|
+
"""Print the footer with closing rail corner.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
message: Footer message text.
|
|
190
|
+
"""
|
|
191
|
+
self._write(f"{self._dim('\u2514')} {message}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Format scanner results for display
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def format_scanner_results(output: Any, project_root: Any = None) -> List[Dict[str, Any]]:
|
|
199
|
+
"""Transform raw scan output into display sections for RailUI.
|
|
200
|
+
|
|
201
|
+
Produces a project-aware context summary with:
|
|
202
|
+
- Project section(s): identity line + infrastructure line
|
|
203
|
+
- Tools section: detected CLI tools
|
|
204
|
+
- Runtime section: language runtimes + OS
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
output: ScanOutput from the orchestrator (has .context, .scanner_results).
|
|
208
|
+
project_root: Path to the project root (used for fallback project name).
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
List of dicts with 'name' and 'lines' keys.
|
|
212
|
+
"""
|
|
213
|
+
from pathlib import Path
|
|
214
|
+
|
|
215
|
+
ctx = output.context
|
|
216
|
+
scan_sections = ctx.get("sections", {})
|
|
217
|
+
root = Path(project_root) if project_root else None
|
|
218
|
+
|
|
219
|
+
sections: List[Dict[str, Any]] = []
|
|
220
|
+
|
|
221
|
+
# --- Project section(s) ---
|
|
222
|
+
project_sections = _build_project_sections(scan_sections, root)
|
|
223
|
+
sections.extend(project_sections)
|
|
224
|
+
|
|
225
|
+
# --- Tools ---
|
|
226
|
+
env = scan_sections.get("environment", {})
|
|
227
|
+
tool_list = env.get("tools", [])
|
|
228
|
+
if tool_list:
|
|
229
|
+
count = len(tool_list)
|
|
230
|
+
names = [t.get("name", "") for t in tool_list if isinstance(t, dict)]
|
|
231
|
+
# Show first 6 tools + ellipsis if more
|
|
232
|
+
if len(names) > 6:
|
|
233
|
+
display = " \u00b7 ".join(names[:6]) + " ..."
|
|
234
|
+
else:
|
|
235
|
+
display = " \u00b7 ".join(names)
|
|
236
|
+
sections.append({"name": f"Tools ({count})", "lines": [display]})
|
|
237
|
+
|
|
238
|
+
# --- Runtime ---
|
|
239
|
+
os_info = env.get("os", {})
|
|
240
|
+
runtimes = env.get("runtimes", [])
|
|
241
|
+
|
|
242
|
+
rt_parts = []
|
|
243
|
+
for rt in runtimes:
|
|
244
|
+
name = _capitalize(rt.get("name", ""))
|
|
245
|
+
ver = rt.get("version", "")
|
|
246
|
+
if name and ver:
|
|
247
|
+
# Use major.minor for cleaner display
|
|
248
|
+
parts = ver.split(".")
|
|
249
|
+
short_ver = ".".join(parts[:2]) if len(parts) >= 2 else parts[0]
|
|
250
|
+
rt_parts.append(f"{name} {short_ver}")
|
|
251
|
+
|
|
252
|
+
wsl = os_info.get("wsl", False)
|
|
253
|
+
if wsl:
|
|
254
|
+
wsl_ver = os_info.get("wsl_version", "")
|
|
255
|
+
rt_parts.append(f"WSL{wsl_ver}")
|
|
256
|
+
else:
|
|
257
|
+
platform = os_info.get("platform", "")
|
|
258
|
+
if platform:
|
|
259
|
+
rt_parts.append(_capitalize(platform))
|
|
260
|
+
|
|
261
|
+
if rt_parts:
|
|
262
|
+
sections.append({"name": "Runtime", "lines": [" \u00b7 ".join(rt_parts)]})
|
|
263
|
+
|
|
264
|
+
return sections
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _build_project_sections(
|
|
268
|
+
scan_sections: Dict[str, Any],
|
|
269
|
+
project_root: Any,
|
|
270
|
+
) -> List[Dict[str, Any]]:
|
|
271
|
+
"""Build project context sections. Returns list for future multi-project support.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
scan_sections: The 'sections' dict from scan output context.
|
|
275
|
+
project_root: Path to the project root (for fallback name).
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
List of section dicts, one per project.
|
|
279
|
+
"""
|
|
280
|
+
projects = []
|
|
281
|
+
projects.append(_build_single_project(scan_sections, project_root))
|
|
282
|
+
return projects
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _build_single_project(
|
|
286
|
+
scan_sections: Dict[str, Any],
|
|
287
|
+
project_root: Any,
|
|
288
|
+
) -> Dict[str, Any]:
|
|
289
|
+
"""Build a single project summary section.
|
|
290
|
+
|
|
291
|
+
Line 1: Project type + service count + git platform
|
|
292
|
+
Line 2: Cloud providers + orchestration + IaC
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
scan_sections: The 'sections' dict from scan output context.
|
|
296
|
+
project_root: Path to the project root (for fallback name).
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Section dict with 'name' and 'lines'.
|
|
300
|
+
"""
|
|
301
|
+
from pathlib import Path
|
|
302
|
+
|
|
303
|
+
# --- Project name ---
|
|
304
|
+
project_identity = scan_sections.get("project_identity", {})
|
|
305
|
+
name = project_identity.get("name", "")
|
|
306
|
+
# Fallback: if name is npm-init default or empty, use directory name
|
|
307
|
+
if not name or name == "my-project":
|
|
308
|
+
if project_root:
|
|
309
|
+
name = Path(project_root).name
|
|
310
|
+
else:
|
|
311
|
+
name = "project"
|
|
312
|
+
|
|
313
|
+
# --- Line 1: Identity ---
|
|
314
|
+
line1_parts = []
|
|
315
|
+
|
|
316
|
+
# Project type
|
|
317
|
+
monorepo = project_identity.get("monorepo", {})
|
|
318
|
+
proj_type = project_identity.get("type", "")
|
|
319
|
+
if monorepo.get("detected") or proj_type == "monorepo":
|
|
320
|
+
line1_parts.append("Monorepo")
|
|
321
|
+
elif proj_type == "library":
|
|
322
|
+
line1_parts.append("Library")
|
|
323
|
+
else:
|
|
324
|
+
line1_parts.append("Single app")
|
|
325
|
+
|
|
326
|
+
# Service count from Dockerfiles (non-worktree)
|
|
327
|
+
infra = scan_sections.get("infrastructure", {})
|
|
328
|
+
service_count = _count_services(infra)
|
|
329
|
+
if service_count > 0:
|
|
330
|
+
if service_count >= 15:
|
|
331
|
+
line1_parts.append(f"{service_count}+ services")
|
|
332
|
+
else:
|
|
333
|
+
line1_parts.append(f"{service_count} services")
|
|
334
|
+
|
|
335
|
+
# Git platform
|
|
336
|
+
git_platform = _detect_git_platform(scan_sections)
|
|
337
|
+
if git_platform:
|
|
338
|
+
line1_parts.append(git_platform)
|
|
339
|
+
|
|
340
|
+
# --- Line 2: Infrastructure ---
|
|
341
|
+
line2_parts = []
|
|
342
|
+
|
|
343
|
+
# Cloud providers
|
|
344
|
+
cloud_providers = infra.get("cloud_providers", [])
|
|
345
|
+
if cloud_providers:
|
|
346
|
+
cloud_names = []
|
|
347
|
+
for cp in cloud_providers:
|
|
348
|
+
cp_name = cp.get("name", "").upper()
|
|
349
|
+
if cp_name:
|
|
350
|
+
cloud_names.append(cp_name)
|
|
351
|
+
if cloud_names:
|
|
352
|
+
line2_parts.append(" + ".join(cloud_names))
|
|
353
|
+
|
|
354
|
+
# Orchestration: Kubernetes + GitOps tool
|
|
355
|
+
orch_summary = _build_orchestration_summary(scan_sections)
|
|
356
|
+
if orch_summary:
|
|
357
|
+
line2_parts.append(orch_summary)
|
|
358
|
+
|
|
359
|
+
# IaC tools
|
|
360
|
+
iac = infra.get("iac", [])
|
|
361
|
+
if iac:
|
|
362
|
+
iac_names = []
|
|
363
|
+
for tool_entry in iac:
|
|
364
|
+
tool_name = _capitalize(tool_entry.get("tool", ""))
|
|
365
|
+
if tool_name and tool_name not in iac_names:
|
|
366
|
+
iac_names.append(tool_name)
|
|
367
|
+
if iac_names:
|
|
368
|
+
line2_parts.append("/".join(iac_names))
|
|
369
|
+
|
|
370
|
+
lines = []
|
|
371
|
+
if line1_parts:
|
|
372
|
+
lines.append(" \u00b7 ".join(line1_parts))
|
|
373
|
+
if line2_parts:
|
|
374
|
+
lines.append(" \u00b7 ".join(line2_parts))
|
|
375
|
+
|
|
376
|
+
return {"name": name, "lines": lines}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _count_services(infra: Dict[str, Any]) -> int:
|
|
380
|
+
"""Count unique services from Docker container files.
|
|
381
|
+
|
|
382
|
+
Counts non-worktree Dockerfiles as a proxy for service count.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
infra: Infrastructure section from scan results.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Number of services detected.
|
|
389
|
+
"""
|
|
390
|
+
containers = infra.get("containers", [])
|
|
391
|
+
for ct in containers:
|
|
392
|
+
if ct.get("tool") == "docker":
|
|
393
|
+
files = ct.get("files", [])
|
|
394
|
+
# Count non-worktree, non-template, non-example Dockerfiles
|
|
395
|
+
count = sum(
|
|
396
|
+
1 for f in files
|
|
397
|
+
if not f.startswith("worktrees/")
|
|
398
|
+
and "template" not in f.lower()
|
|
399
|
+
and not f.endswith(".example")
|
|
400
|
+
)
|
|
401
|
+
return count
|
|
402
|
+
return 0
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _detect_git_platform(scan_sections: Dict[str, Any]) -> Optional[str]:
|
|
406
|
+
"""Detect git platform from scan results.
|
|
407
|
+
|
|
408
|
+
Checks git.platform first, then parses remotes, then falls back to
|
|
409
|
+
tool presence (glab -> GitLab, gh -> GitHub).
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
scan_sections: The 'sections' dict from scan output.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Platform name (e.g. "GitLab", "GitHub") or None.
|
|
416
|
+
"""
|
|
417
|
+
git = scan_sections.get("git", {})
|
|
418
|
+
|
|
419
|
+
# Direct platform detection
|
|
420
|
+
platform = git.get("platform")
|
|
421
|
+
if platform:
|
|
422
|
+
return _capitalize_platform(platform)
|
|
423
|
+
|
|
424
|
+
# Parse remotes for platform hints
|
|
425
|
+
remotes = git.get("remotes", [])
|
|
426
|
+
for remote in remotes:
|
|
427
|
+
remote_platform = remote.get("platform")
|
|
428
|
+
if remote_platform:
|
|
429
|
+
return _capitalize_platform(remote_platform)
|
|
430
|
+
url = remote.get("url", "")
|
|
431
|
+
if "gitlab" in url.lower():
|
|
432
|
+
return "GitLab"
|
|
433
|
+
if "github" in url.lower():
|
|
434
|
+
return "GitHub"
|
|
435
|
+
|
|
436
|
+
# Fallback: check for glab or gh tool presence
|
|
437
|
+
env = scan_sections.get("environment", {})
|
|
438
|
+
tools = env.get("tools", [])
|
|
439
|
+
tool_names = {t.get("name", "") for t in tools if isinstance(t, dict)}
|
|
440
|
+
if "glab" in tool_names:
|
|
441
|
+
return "GitLab"
|
|
442
|
+
if "gh" in tool_names:
|
|
443
|
+
return "GitHub"
|
|
444
|
+
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _capitalize_platform(platform: str) -> str:
|
|
449
|
+
"""Capitalize a git platform name.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
platform: Raw platform string (e.g. "gitlab", "github").
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Human-friendly name (e.g. "GitLab", "GitHub").
|
|
456
|
+
"""
|
|
457
|
+
platform_map = {
|
|
458
|
+
"gitlab": "GitLab",
|
|
459
|
+
"gitlab-ci": "GitLab",
|
|
460
|
+
"github": "GitHub",
|
|
461
|
+
"github-actions": "GitHub",
|
|
462
|
+
"bitbucket": "Bitbucket",
|
|
463
|
+
}
|
|
464
|
+
return platform_map.get(platform.lower(), platform.capitalize())
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _build_orchestration_summary(scan_sections: Dict[str, Any]) -> Optional[str]:
|
|
468
|
+
"""Build orchestration summary string.
|
|
469
|
+
|
|
470
|
+
Produces strings like "Kubernetes (Flux)" or "Kubernetes".
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
scan_sections: The 'sections' dict from scan output.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Orchestration summary string, or None.
|
|
477
|
+
"""
|
|
478
|
+
env = scan_sections.get("environment", {})
|
|
479
|
+
tools = env.get("tools", [])
|
|
480
|
+
tool_names = {t.get("name", "") for t in tools if isinstance(t, dict)}
|
|
481
|
+
|
|
482
|
+
has_k8s = "kubectl" in tool_names
|
|
483
|
+
if not has_k8s:
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
# Detect GitOps tool
|
|
487
|
+
gitops_tool = None
|
|
488
|
+
if "flux" in tool_names or "fluxctl" in tool_names:
|
|
489
|
+
gitops_tool = "Flux"
|
|
490
|
+
|
|
491
|
+
# Check infrastructure for gitops hints (flux files, argocd, etc.)
|
|
492
|
+
infra = scan_sections.get("infrastructure", {})
|
|
493
|
+
ci_cd = infra.get("ci_cd", [])
|
|
494
|
+
for ci in ci_cd:
|
|
495
|
+
if isinstance(ci, dict):
|
|
496
|
+
ci_platform = ci.get("platform", "").lower()
|
|
497
|
+
if "flux" in ci_platform:
|
|
498
|
+
gitops_tool = "Flux"
|
|
499
|
+
elif "argo" in ci_platform or "argocd" in ci_platform:
|
|
500
|
+
gitops_tool = "ArgoCD"
|
|
501
|
+
|
|
502
|
+
# Check orchestration section if present
|
|
503
|
+
orch = scan_sections.get("orchestration", {})
|
|
504
|
+
if isinstance(orch, dict):
|
|
505
|
+
k8s = orch.get("kubernetes", {})
|
|
506
|
+
if isinstance(k8s, dict) and k8s.get("detected"):
|
|
507
|
+
has_k8s = True
|
|
508
|
+
gitops = orch.get("gitops", {})
|
|
509
|
+
if isinstance(gitops, dict) and gitops.get("tool"):
|
|
510
|
+
gitops_tool = gitops["tool"].capitalize()
|
|
511
|
+
|
|
512
|
+
if has_k8s:
|
|
513
|
+
if gitops_tool:
|
|
514
|
+
return f"Kubernetes ({gitops_tool})"
|
|
515
|
+
return "Kubernetes"
|
|
516
|
+
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def collect_warnings(output: Any) -> List[str]:
|
|
521
|
+
"""Collect user-facing warnings from scan output.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
output: ScanOutput from the orchestrator.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
List of warning message strings.
|
|
528
|
+
"""
|
|
529
|
+
warnings = []
|
|
530
|
+
|
|
531
|
+
# Check for no git directory
|
|
532
|
+
git_section = output.context.get("sections", {}).get("git", {})
|
|
533
|
+
remotes = git_section.get("remotes", [])
|
|
534
|
+
if not remotes and not git_section.get("platform"):
|
|
535
|
+
warnings.append("No .git directory found at project root")
|
|
536
|
+
|
|
537
|
+
# Include scanner-level warnings (deduplicated, user-facing only)
|
|
538
|
+
for w in output.warnings:
|
|
539
|
+
# Skip if already covered by a more descriptive version
|
|
540
|
+
if w not in warnings and not any(w in existing for existing in warnings):
|
|
541
|
+
warnings.append(w)
|
|
542
|
+
|
|
543
|
+
return warnings
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def collect_created_summary(project_root: "Path", output: Any) -> Dict[str, str]:
|
|
547
|
+
"""Collect summary of created artifacts for fresh install display.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
project_root: Project root directory.
|
|
551
|
+
output: ScanOutput from the orchestrator.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
Dict of {artifact_name: description}.
|
|
555
|
+
"""
|
|
556
|
+
from pathlib import Path
|
|
557
|
+
|
|
558
|
+
items = {}
|
|
559
|
+
claude_dir = Path(project_root) / ".claude"
|
|
560
|
+
|
|
561
|
+
# Count symlinks
|
|
562
|
+
symlink_count = 0
|
|
563
|
+
if claude_dir.is_dir():
|
|
564
|
+
for entry in claude_dir.iterdir():
|
|
565
|
+
if entry.is_symlink():
|
|
566
|
+
symlink_count += 1
|
|
567
|
+
if symlink_count:
|
|
568
|
+
items[".claude/"] = f"{symlink_count} symlinks"
|
|
569
|
+
|
|
570
|
+
# CLAUDE.md
|
|
571
|
+
claude_md = Path(project_root) / "CLAUDE.md"
|
|
572
|
+
if claude_md.is_file():
|
|
573
|
+
items["CLAUDE.md"] = "orchestrator identity"
|
|
574
|
+
|
|
575
|
+
# settings.json
|
|
576
|
+
settings = claude_dir / "settings.json"
|
|
577
|
+
if settings.is_file():
|
|
578
|
+
items["settings.json"] = "hooks + permissions"
|
|
579
|
+
|
|
580
|
+
# project-context sections
|
|
581
|
+
ctx_path = claude_dir / "project-context" / "project-context.json"
|
|
582
|
+
if ctx_path.is_file():
|
|
583
|
+
try:
|
|
584
|
+
import json
|
|
585
|
+
data = json.loads(ctx_path.read_text())
|
|
586
|
+
section_count = len(data.get("sections", {}))
|
|
587
|
+
items["project-context"] = f"{section_count} sections detected"
|
|
588
|
+
except Exception:
|
|
589
|
+
items["project-context"] = "generated"
|
|
590
|
+
|
|
591
|
+
return items
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# ---------------------------------------------------------------------------
|
|
595
|
+
# Helpers
|
|
596
|
+
# ---------------------------------------------------------------------------
|
|
597
|
+
|
|
598
|
+
def _capitalize(s: str) -> str:
|
|
599
|
+
"""Capitalize first letter, keep rest. Handle known names."""
|
|
600
|
+
name_map = {
|
|
601
|
+
"javascript": "JavaScript",
|
|
602
|
+
"typescript": "TypeScript",
|
|
603
|
+
"python": "Python",
|
|
604
|
+
"go": "Go",
|
|
605
|
+
"java": "Java",
|
|
606
|
+
"rust": "Rust",
|
|
607
|
+
"express": "Express",
|
|
608
|
+
"terraform": "Terraform",
|
|
609
|
+
"terragrunt": "Terragrunt",
|
|
610
|
+
"docker": "Docker",
|
|
611
|
+
"node": "Node",
|
|
612
|
+
"python3": "Python",
|
|
613
|
+
"npm": "npm",
|
|
614
|
+
"pnpm": "pnpm",
|
|
615
|
+
"yarn": "yarn",
|
|
616
|
+
"linux": "Linux",
|
|
617
|
+
"darwin": "macOS",
|
|
618
|
+
"win32": "Windows",
|
|
619
|
+
"terraform_provider": "Terraform",
|
|
620
|
+
"cli_config": "CLI",
|
|
621
|
+
}
|
|
622
|
+
return name_map.get(s, s.capitalize() if s else s)
|
|
623
|
+
|
|
624
|
+
|