@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,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health Check / Verification Functions for gaia-scan
|
|
3
|
+
|
|
4
|
+
Provides post-install verification checks to confirm that the
|
|
5
|
+
gaia-ops installation is healthy. Used after fresh install (Mode 1)
|
|
6
|
+
and after rescan+sync (Mode 2).
|
|
7
|
+
|
|
8
|
+
Functions:
|
|
9
|
+
- run_verification: run all checks, return summary
|
|
10
|
+
- check_symlinks: verify symlinks exist and are valid
|
|
11
|
+
- check_claude_md: legacy check (CLAUDE.md no longer generated)
|
|
12
|
+
- check_settings_json: verify valid JSON
|
|
13
|
+
- check_project_context: verify exists and has sections
|
|
14
|
+
- check_python: verify python3 available
|
|
15
|
+
- check_hooks: verify pre_tool_use.py exists
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import List, Optional
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class CheckResult:
|
|
31
|
+
"""Result of a single health check.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
name: Check name for display.
|
|
35
|
+
ok: Whether the check passed.
|
|
36
|
+
detail: Human-readable detail string.
|
|
37
|
+
fix: Optional fix suggestion if check failed.
|
|
38
|
+
"""
|
|
39
|
+
name: str
|
|
40
|
+
ok: bool
|
|
41
|
+
detail: str = ""
|
|
42
|
+
fix: Optional[str] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_symlinks(project_root: Path) -> CheckResult:
|
|
46
|
+
"""Verify that all expected symlinks exist in .claude/.
|
|
47
|
+
|
|
48
|
+
Checks for: agents, tools, hooks, commands, templates, config,
|
|
49
|
+
speckit, skills, CHANGELOG.md (9 total).
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
project_root: Project root directory.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
CheckResult with count of valid symlinks.
|
|
56
|
+
"""
|
|
57
|
+
names = [
|
|
58
|
+
"agents", "tools", "hooks", "commands",
|
|
59
|
+
"templates", "config", "speckit", "skills",
|
|
60
|
+
"CHANGELOG.md",
|
|
61
|
+
]
|
|
62
|
+
valid = 0
|
|
63
|
+
for name in names:
|
|
64
|
+
link_path = project_root / ".claude" / name
|
|
65
|
+
if link_path.exists() or link_path.is_symlink():
|
|
66
|
+
valid += 1
|
|
67
|
+
|
|
68
|
+
return CheckResult(
|
|
69
|
+
name="Symlinks",
|
|
70
|
+
ok=valid == len(names),
|
|
71
|
+
detail=f"{valid}/{len(names)} valid",
|
|
72
|
+
fix="Run gaia-scan to recreate symlinks" if valid < len(names) else None,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def check_claude_md(project_root: Path) -> CheckResult:
|
|
77
|
+
"""Check for CLAUDE.md presence. No longer required -- identity injected by hook.
|
|
78
|
+
|
|
79
|
+
Kept for backward compatibility with callers that expect this check.
|
|
80
|
+
"""
|
|
81
|
+
path = project_root / "CLAUDE.md"
|
|
82
|
+
if path.is_file():
|
|
83
|
+
return CheckResult(
|
|
84
|
+
name="CLAUDE.md",
|
|
85
|
+
ok=True,
|
|
86
|
+
detail="Present (legacy -- identity now injected by hook)",
|
|
87
|
+
)
|
|
88
|
+
return CheckResult(
|
|
89
|
+
name="CLAUDE.md",
|
|
90
|
+
ok=True,
|
|
91
|
+
detail="Not present (identity injected by UserPromptSubmit hook)",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def check_settings_json(project_root: Path) -> CheckResult:
|
|
96
|
+
"""Verify that .claude/settings.json exists and is valid JSON.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
project_root: Project root directory.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
CheckResult.
|
|
103
|
+
"""
|
|
104
|
+
path = project_root / ".claude" / "settings.json"
|
|
105
|
+
if not path.is_file():
|
|
106
|
+
return CheckResult(
|
|
107
|
+
name="settings.json",
|
|
108
|
+
ok=False,
|
|
109
|
+
detail="Missing",
|
|
110
|
+
fix="Run gaia-scan",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
json.loads(path.read_text())
|
|
115
|
+
return CheckResult(name="settings.json", ok=True, detail="Valid JSON")
|
|
116
|
+
except (json.JSONDecodeError, OSError):
|
|
117
|
+
return CheckResult(
|
|
118
|
+
name="settings.json",
|
|
119
|
+
ok=False,
|
|
120
|
+
detail="Invalid JSON",
|
|
121
|
+
fix="Delete and run gaia-scan",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_project_context(project_root: Path) -> CheckResult:
|
|
126
|
+
"""Verify that project-context.json exists and has sections.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
project_root: Project root directory.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
CheckResult with section count.
|
|
133
|
+
"""
|
|
134
|
+
path = project_root / ".claude" / "project-context" / "project-context.json"
|
|
135
|
+
if not path.is_file():
|
|
136
|
+
return CheckResult(
|
|
137
|
+
name="project-context",
|
|
138
|
+
ok=False,
|
|
139
|
+
detail="Missing",
|
|
140
|
+
fix="Run gaia-scan",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
data = json.loads(path.read_text())
|
|
145
|
+
sections = len(data.get("sections", {}))
|
|
146
|
+
return CheckResult(
|
|
147
|
+
name="project-context",
|
|
148
|
+
ok=sections >= 3,
|
|
149
|
+
detail=f"{sections} sections",
|
|
150
|
+
fix="Run gaia-scan to regenerate" if sections < 3 else None,
|
|
151
|
+
)
|
|
152
|
+
except (json.JSONDecodeError, OSError):
|
|
153
|
+
return CheckResult(
|
|
154
|
+
name="project-context",
|
|
155
|
+
ok=False,
|
|
156
|
+
detail="Invalid JSON",
|
|
157
|
+
fix="Run gaia-scan to regenerate",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def check_python() -> CheckResult:
|
|
162
|
+
"""Verify that python3 is available.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
CheckResult with Python version.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
result = subprocess.run(
|
|
169
|
+
["python3", "--version"],
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
timeout=5,
|
|
173
|
+
)
|
|
174
|
+
if result.returncode == 0:
|
|
175
|
+
version = result.stdout.strip()
|
|
176
|
+
return CheckResult(name="Python", ok=True, detail=version)
|
|
177
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
return CheckResult(
|
|
181
|
+
name="Python",
|
|
182
|
+
ok=False,
|
|
183
|
+
detail="Not found",
|
|
184
|
+
fix="Install Python 3.9+",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def check_hooks(project_root: Path) -> CheckResult:
|
|
189
|
+
"""Verify that pre_tool_use.py hook exists.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
project_root: Project root directory.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
CheckResult.
|
|
196
|
+
"""
|
|
197
|
+
hook_path = project_root / ".claude" / "hooks" / "pre_tool_use.py"
|
|
198
|
+
if hook_path.exists():
|
|
199
|
+
return CheckResult(name="Hooks", ok=True, detail="pre_tool_use.py found")
|
|
200
|
+
|
|
201
|
+
return CheckResult(
|
|
202
|
+
name="Hooks",
|
|
203
|
+
ok=False,
|
|
204
|
+
detail="pre_tool_use.py missing",
|
|
205
|
+
fix="Run gaia-scan to recreate symlinks",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def run_verification(project_root: Path) -> List[CheckResult]:
|
|
210
|
+
"""Run all post-install verification checks.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
project_root: Project root directory.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
List of CheckResult objects (all checks always run).
|
|
217
|
+
"""
|
|
218
|
+
checks = [
|
|
219
|
+
check_symlinks(project_root),
|
|
220
|
+
check_claude_md(project_root),
|
|
221
|
+
check_settings_json(project_root),
|
|
222
|
+
check_project_context(project_root),
|
|
223
|
+
check_python(),
|
|
224
|
+
check_hooks(project_root),
|
|
225
|
+
]
|
|
226
|
+
return checks
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def print_verification(results: List[CheckResult]) -> bool:
|
|
230
|
+
"""Print verification results in a human-readable format.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
results: List of CheckResult objects.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if all checks passed.
|
|
237
|
+
"""
|
|
238
|
+
supports_color = hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
|
|
239
|
+
|
|
240
|
+
def _green(t: str) -> str:
|
|
241
|
+
return f"\033[32m{t}\033[0m" if supports_color else t
|
|
242
|
+
|
|
243
|
+
def _yellow(t: str) -> str:
|
|
244
|
+
return f"\033[33m{t}\033[0m" if supports_color else t
|
|
245
|
+
|
|
246
|
+
def _red(t: str) -> str:
|
|
247
|
+
return f"\033[31m{t}\033[0m" if supports_color else t
|
|
248
|
+
|
|
249
|
+
def _gray(t: str) -> str:
|
|
250
|
+
return f"\033[90m{t}\033[0m" if supports_color else t
|
|
251
|
+
|
|
252
|
+
print("\n Verifying installation...\n", file=sys.stderr)
|
|
253
|
+
|
|
254
|
+
all_passed = True
|
|
255
|
+
for r in results:
|
|
256
|
+
padded = r.name.ljust(18)
|
|
257
|
+
if r.ok:
|
|
258
|
+
print(_green(f" ✓ {padded} {r.detail}"), file=sys.stderr)
|
|
259
|
+
else:
|
|
260
|
+
print(_yellow(f" ⚠ {padded} {r.detail}"), file=sys.stderr)
|
|
261
|
+
if r.fix:
|
|
262
|
+
print(_gray(f" Fix: {r.fix}"), file=sys.stderr)
|
|
263
|
+
all_passed = False
|
|
264
|
+
|
|
265
|
+
print("", file=sys.stderr)
|
|
266
|
+
return all_passed
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filtered os.walk utility for scan performance.
|
|
3
|
+
|
|
4
|
+
Replaces pathlib.rglob with a single filtered os.walk pass that prunes
|
|
5
|
+
expensive directories (node_modules, .git, .terraform, etc.) before descending.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import FrozenSet, Iterator, Sequence
|
|
11
|
+
|
|
12
|
+
# Directories to skip during scanning -- shared across all scanners
|
|
13
|
+
SKIP_DIRS: FrozenSet[str] = frozenset({
|
|
14
|
+
"node_modules",
|
|
15
|
+
".git",
|
|
16
|
+
"__pycache__",
|
|
17
|
+
".terraform",
|
|
18
|
+
".terragrunt-cache",
|
|
19
|
+
"vendor",
|
|
20
|
+
"dist",
|
|
21
|
+
"build",
|
|
22
|
+
".venv",
|
|
23
|
+
"venv",
|
|
24
|
+
".cache",
|
|
25
|
+
".npm",
|
|
26
|
+
".next",
|
|
27
|
+
".nuxt",
|
|
28
|
+
"target",
|
|
29
|
+
".pytest_cache",
|
|
30
|
+
".mypy_cache",
|
|
31
|
+
".ruff_cache",
|
|
32
|
+
".tox",
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def walk_project(root: Path, extensions: Sequence[str]) -> Iterator[Path]:
|
|
37
|
+
"""Walk project tree yielding files matching given extensions.
|
|
38
|
+
|
|
39
|
+
Prunes SKIP_DIRS and hidden directories (starting with '.') to avoid
|
|
40
|
+
descending into expensive subtrees like node_modules or .terraform.
|
|
41
|
+
|
|
42
|
+
Much faster than pathlib.rglob on large monorepos because rglob
|
|
43
|
+
traverses every directory before filtering.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
root: Absolute path to start walking from.
|
|
47
|
+
extensions: Sequence of file extensions to match (e.g., [".tf", ".hcl"]).
|
|
48
|
+
|
|
49
|
+
Yields:
|
|
50
|
+
Path objects for matching files.
|
|
51
|
+
"""
|
|
52
|
+
root_str = str(root)
|
|
53
|
+
ext_set = set(extensions)
|
|
54
|
+
|
|
55
|
+
for dirpath, dirnames, filenames in os.walk(root_str):
|
|
56
|
+
# Prune in-place to prevent os.walk from descending
|
|
57
|
+
dirnames[:] = [
|
|
58
|
+
d for d in dirnames
|
|
59
|
+
if d not in SKIP_DIRS and not d.startswith(".")
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for f in filenames:
|
|
63
|
+
# Check extension by finding the last dot
|
|
64
|
+
dot_idx = f.rfind(".")
|
|
65
|
+
if dot_idx >= 0 and f[dot_idx:] in ext_set:
|
|
66
|
+
yield Path(dirpath) / f
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def walk_project_prefix(root: Path, prefixes: Sequence[str]) -> Iterator[Path]:
|
|
70
|
+
"""Walk project tree yielding files whose names start with given prefixes.
|
|
71
|
+
|
|
72
|
+
Useful for patterns like 'Dockerfile.*' where the prefix is 'Dockerfile.'.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
root: Absolute path to start walking from.
|
|
76
|
+
prefixes: Sequence of filename prefixes to match.
|
|
77
|
+
|
|
78
|
+
Yields:
|
|
79
|
+
Path objects for matching files.
|
|
80
|
+
"""
|
|
81
|
+
root_str = str(root)
|
|
82
|
+
|
|
83
|
+
for dirpath, dirnames, filenames in os.walk(root_str):
|
|
84
|
+
dirnames[:] = [
|
|
85
|
+
d for d in dirnames
|
|
86
|
+
if d not in SKIP_DIRS and not d.startswith(".")
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
for f in filenames:
|
|
90
|
+
if any(f.startswith(p) for p in prefixes):
|
|
91
|
+
yield Path(dirpath) / f
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def walk_project_named(root: Path, filenames: Sequence[str]) -> Iterator[Path]:
|
|
95
|
+
"""Walk project tree yielding files matching exact filenames.
|
|
96
|
+
|
|
97
|
+
Same pruning as walk_project but matches on exact filename rather than
|
|
98
|
+
extension. Useful for files like 'Dockerfile', 'Chart.yaml', etc.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
root: Absolute path to start walking from.
|
|
102
|
+
filenames: Sequence of exact filenames to match.
|
|
103
|
+
|
|
104
|
+
Yields:
|
|
105
|
+
Path objects for matching files.
|
|
106
|
+
"""
|
|
107
|
+
root_str = str(root)
|
|
108
|
+
name_set = set(filenames)
|
|
109
|
+
|
|
110
|
+
for dirpath, dirnames, fnames in os.walk(root_str):
|
|
111
|
+
dirnames[:] = [
|
|
112
|
+
d for d in dirnames
|
|
113
|
+
if d not in SKIP_DIRS and not d.startswith(".")
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
for f in fnames:
|
|
117
|
+
if f in name_set:
|
|
118
|
+
yield Path(dirpath) / f
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workspace Type Detection
|
|
3
|
+
|
|
4
|
+
Detects whether the scan root is a single-repo, monorepo, or multi-repo workspace.
|
|
5
|
+
Called by the orchestrator before individual scanners run, and importable by scanners
|
|
6
|
+
that need workspace-aware behavior.
|
|
7
|
+
|
|
8
|
+
Detection logic:
|
|
9
|
+
- If root has .git -> single-repo or monorepo (determined by stack scanner)
|
|
10
|
+
- If root has NO .git but 2+ immediate subdirectories have .git -> multi-repo-workspace
|
|
11
|
+
- Otherwise -> single-repo (default)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Directories to always skip during workspace scanning
|
|
22
|
+
_SKIP_DIRS = frozenset({
|
|
23
|
+
"node_modules", "__pycache__", ".tox", ".venv",
|
|
24
|
+
"venv", "dist", "build", ".next", ".nuxt", "target",
|
|
25
|
+
".pytest_cache", ".mypy_cache", ".ruff_cache", "vendor",
|
|
26
|
+
".terraform", ".terragrunt-cache",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class WorkspaceInfo:
|
|
32
|
+
"""Result of workspace type detection.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
workspace_type: One of 'single-repo', 'monorepo', 'multi-repo-workspace'.
|
|
36
|
+
repo_dirs: For multi-repo, list of subdirectory Paths that contain .git.
|
|
37
|
+
Empty for single-repo/monorepo.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
workspace_type: str = "single-repo"
|
|
41
|
+
repo_dirs: List[Path] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_multi_repo(self) -> bool:
|
|
45
|
+
return self.workspace_type == "multi-repo-workspace"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def detect_workspace_type(root: Path) -> WorkspaceInfo:
|
|
49
|
+
"""Detect the workspace type for the given root directory.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
root: Absolute path to the project root directory.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
WorkspaceInfo with the detected workspace type and repo directories.
|
|
56
|
+
"""
|
|
57
|
+
# If root itself has .git, it's a normal repo (single or monorepo)
|
|
58
|
+
if (root / ".git").is_dir():
|
|
59
|
+
return WorkspaceInfo(workspace_type="single-repo")
|
|
60
|
+
|
|
61
|
+
# Check immediate subdirectories for .git
|
|
62
|
+
git_subdirs: List[Path] = []
|
|
63
|
+
try:
|
|
64
|
+
for entry in sorted(root.iterdir()):
|
|
65
|
+
if not entry.is_dir():
|
|
66
|
+
continue
|
|
67
|
+
if entry.name.startswith(".") or entry.name in _SKIP_DIRS:
|
|
68
|
+
continue
|
|
69
|
+
if (entry / ".git").is_dir():
|
|
70
|
+
git_subdirs.append(entry)
|
|
71
|
+
except (PermissionError, OSError) as exc:
|
|
72
|
+
logger.debug("Failed to scan subdirectories of %s: %s", root, exc)
|
|
73
|
+
|
|
74
|
+
if len(git_subdirs) >= 2:
|
|
75
|
+
logger.info(
|
|
76
|
+
"Multi-repo workspace detected: %d repos in %s",
|
|
77
|
+
len(git_subdirs),
|
|
78
|
+
root,
|
|
79
|
+
)
|
|
80
|
+
return WorkspaceInfo(
|
|
81
|
+
workspace_type="multi-repo-workspace",
|
|
82
|
+
repo_dirs=git_subdirs,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return WorkspaceInfo(workspace_type="single-repo")
|