@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,570 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Scanner
|
|
3
|
+
|
|
4
|
+
Detects git platform, remotes, default branch, branch strategy, and monorepo
|
|
5
|
+
workspace configuration from the project's .git directory and manifest files.
|
|
6
|
+
|
|
7
|
+
Returns the `git` section per data-model.md section 2.5.
|
|
8
|
+
|
|
9
|
+
Pure Function Contract:
|
|
10
|
+
- No file writes
|
|
11
|
+
- No state modification
|
|
12
|
+
- No network calls
|
|
13
|
+
- Only reads: filesystem reads (`.git/config`, `.git/HEAD`, manifest files)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import configparser
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from tools.scan.scanners.base import BaseScanner, ScanResult
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Known platform domain patterns
|
|
29
|
+
_PLATFORM_PATTERNS: List[Tuple[str, str]] = [
|
|
30
|
+
("github.com", "github"),
|
|
31
|
+
("gitlab.com", "gitlab"),
|
|
32
|
+
("bitbucket.org", "bitbucket"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Monorepo indicator files and their workspace config names
|
|
36
|
+
_MONOREPO_INDICATORS: List[Tuple[str, str]] = [
|
|
37
|
+
("turbo.json", "turborepo"),
|
|
38
|
+
("nx.json", "nx"),
|
|
39
|
+
("lerna.json", "lerna"),
|
|
40
|
+
("pnpm-workspace.yaml", "pnpm"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _detect_platform_from_url(url: str) -> Optional[str]:
|
|
45
|
+
"""Detect git hosting platform from a remote URL.
|
|
46
|
+
|
|
47
|
+
Supports SSH (git@host:...) and HTTPS (https://host/...) URLs.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
url: Git remote URL string.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Platform name ('github', 'gitlab', 'bitbucket', 'self-hosted') or None.
|
|
54
|
+
"""
|
|
55
|
+
if not url:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
url_lower = url.lower()
|
|
59
|
+
|
|
60
|
+
for domain, platform in _PLATFORM_PATTERNS:
|
|
61
|
+
if domain in url_lower:
|
|
62
|
+
return platform
|
|
63
|
+
|
|
64
|
+
# If there is a host but it doesn't match known platforms, it's self-hosted.
|
|
65
|
+
# Check for patterns like git@host: or https://host/
|
|
66
|
+
if re.search(r"(://|@)", url):
|
|
67
|
+
return "self-hosted"
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse_git_config(git_dir: Path) -> Dict[str, Any]:
|
|
73
|
+
"""Parse .git/config to extract remotes.
|
|
74
|
+
|
|
75
|
+
Uses configparser which handles the INI-like git config format.
|
|
76
|
+
Falls back gracefully on parse errors.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
git_dir: Path to the .git directory.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dict with 'remotes' list of {name, url, platform}.
|
|
83
|
+
"""
|
|
84
|
+
config_path = git_dir / "config"
|
|
85
|
+
remotes: List[Dict[str, Any]] = []
|
|
86
|
+
|
|
87
|
+
if not config_path.is_file():
|
|
88
|
+
return {"remotes": remotes}
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
parser = configparser.ConfigParser(strict=False)
|
|
92
|
+
parser.read(str(config_path))
|
|
93
|
+
|
|
94
|
+
for section in parser.sections():
|
|
95
|
+
# Git config remote sections look like: [remote "origin"]
|
|
96
|
+
if section.startswith('remote "') and section.endswith('"'):
|
|
97
|
+
remote_name = section[8:-1] # Strip 'remote "' and '"'
|
|
98
|
+
url = parser.get(section, "url", fallback="")
|
|
99
|
+
platform = _detect_platform_from_url(url)
|
|
100
|
+
remotes.append({
|
|
101
|
+
"name": remote_name,
|
|
102
|
+
"url": url,
|
|
103
|
+
"platform": platform,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
except (configparser.Error, OSError) as exc:
|
|
107
|
+
logger.debug("Failed to parse git config at %s: %s", config_path, exc)
|
|
108
|
+
|
|
109
|
+
return {"remotes": remotes}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _detect_default_branch(git_dir: Path) -> Optional[str]:
|
|
113
|
+
"""Detect the default branch from .git/HEAD.
|
|
114
|
+
|
|
115
|
+
The HEAD file contains either:
|
|
116
|
+
- 'ref: refs/heads/<branch>' for a branch reference
|
|
117
|
+
- A commit hash for detached HEAD
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
git_dir: Path to the .git directory.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Branch name string or None if HEAD is detached or unreadable.
|
|
124
|
+
"""
|
|
125
|
+
head_path = git_dir / "HEAD"
|
|
126
|
+
|
|
127
|
+
if not head_path.is_file():
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
content = head_path.read_text().strip()
|
|
132
|
+
if content.startswith("ref: refs/heads/"):
|
|
133
|
+
return content[len("ref: refs/heads/"):]
|
|
134
|
+
except OSError as exc:
|
|
135
|
+
logger.debug("Failed to read HEAD at %s: %s", head_path, exc)
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _detect_branch_strategy(git_dir: Path) -> Dict[str, Any]:
|
|
141
|
+
"""Detect branch strategy from branch name patterns in refs.
|
|
142
|
+
|
|
143
|
+
Looks at refs/heads/ (local branches) and refs/remotes/ (remote tracking).
|
|
144
|
+
|
|
145
|
+
Strategy detection:
|
|
146
|
+
- gitflow: has develop + release/* or hotfix/* branches
|
|
147
|
+
- trunk-based: only main/master, no long-lived feature branches
|
|
148
|
+
- github-flow: main/master + feature/* or short-lived branches
|
|
149
|
+
- unknown: cannot determine
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
git_dir: Path to the .git directory.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dict with 'detected', 'pattern', and 'indicators'.
|
|
156
|
+
"""
|
|
157
|
+
result: Dict[str, Any] = {
|
|
158
|
+
"detected": False,
|
|
159
|
+
"pattern": None,
|
|
160
|
+
"indicators": [],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
branches: List[str] = []
|
|
164
|
+
|
|
165
|
+
# Collect local branches
|
|
166
|
+
refs_heads = git_dir / "refs" / "heads"
|
|
167
|
+
if refs_heads.is_dir():
|
|
168
|
+
branches.extend(_collect_branch_names(refs_heads, ""))
|
|
169
|
+
|
|
170
|
+
# Collect remote branches
|
|
171
|
+
refs_remotes = git_dir / "refs" / "remotes"
|
|
172
|
+
if refs_remotes.is_dir():
|
|
173
|
+
for remote_dir in refs_remotes.iterdir():
|
|
174
|
+
if remote_dir.is_dir():
|
|
175
|
+
branches.extend(
|
|
176
|
+
_collect_branch_names(remote_dir, "")
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Also check packed-refs for branches not yet unpacked
|
|
180
|
+
packed_refs = git_dir / "packed-refs"
|
|
181
|
+
if packed_refs.is_file():
|
|
182
|
+
try:
|
|
183
|
+
for line in packed_refs.read_text().splitlines():
|
|
184
|
+
line = line.strip()
|
|
185
|
+
if line.startswith("#") or not line:
|
|
186
|
+
continue
|
|
187
|
+
parts = line.split()
|
|
188
|
+
if len(parts) >= 2:
|
|
189
|
+
ref = parts[1]
|
|
190
|
+
if ref.startswith("refs/heads/"):
|
|
191
|
+
branches.append(ref[len("refs/heads/"):])
|
|
192
|
+
elif ref.startswith("refs/remotes/"):
|
|
193
|
+
# Strip remote name prefix (e.g., origin/)
|
|
194
|
+
remainder = ref[len("refs/remotes/"):]
|
|
195
|
+
slash_idx = remainder.find("/")
|
|
196
|
+
if slash_idx >= 0:
|
|
197
|
+
branches.append(remainder[slash_idx + 1:])
|
|
198
|
+
except OSError:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
if not branches:
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
# Deduplicate
|
|
205
|
+
branch_set = set(branches)
|
|
206
|
+
indicators: List[str] = []
|
|
207
|
+
|
|
208
|
+
has_develop = "develop" in branch_set or "development" in branch_set
|
|
209
|
+
has_release = any(b.startswith("release/") or b.startswith("release-") for b in branch_set)
|
|
210
|
+
has_hotfix = any(b.startswith("hotfix/") or b.startswith("hotfix-") for b in branch_set)
|
|
211
|
+
has_feature = any(b.startswith("feature/") or b.startswith("feature-") for b in branch_set)
|
|
212
|
+
has_main = "main" in branch_set or "master" in branch_set
|
|
213
|
+
|
|
214
|
+
# Gitflow: develop + (release/* or hotfix/*)
|
|
215
|
+
if has_develop and (has_release or has_hotfix):
|
|
216
|
+
result["detected"] = True
|
|
217
|
+
result["pattern"] = "gitflow"
|
|
218
|
+
if has_develop:
|
|
219
|
+
indicators.append("develop branch present")
|
|
220
|
+
if has_release:
|
|
221
|
+
indicators.append("release/* branches present")
|
|
222
|
+
if has_hotfix:
|
|
223
|
+
indicators.append("hotfix/* branches present")
|
|
224
|
+
if has_feature:
|
|
225
|
+
indicators.append("feature/* branches present")
|
|
226
|
+
result["indicators"] = indicators
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
# Trunk-based: only main/master, few or no long-lived branches
|
|
230
|
+
# Heuristic: main exists, no develop, no release/*, total branches <= 3
|
|
231
|
+
if has_main and not has_develop and not has_release and len(branch_set) <= 3:
|
|
232
|
+
result["detected"] = True
|
|
233
|
+
result["pattern"] = "trunk-based"
|
|
234
|
+
indicators.append("main/master only")
|
|
235
|
+
indicators.append(f"{len(branch_set)} total branches")
|
|
236
|
+
result["indicators"] = indicators
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
# GitHub-flow: main + feature branches, no develop
|
|
240
|
+
if has_main and not has_develop and (has_feature or len(branch_set) > 3):
|
|
241
|
+
result["detected"] = True
|
|
242
|
+
result["pattern"] = "github-flow"
|
|
243
|
+
indicators.append("main/master present")
|
|
244
|
+
if has_feature:
|
|
245
|
+
indicators.append("feature/* branches present")
|
|
246
|
+
indicators.append(f"{len(branch_set)} total branches")
|
|
247
|
+
result["indicators"] = indicators
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
# Could not determine a clear pattern
|
|
251
|
+
if has_main:
|
|
252
|
+
result["detected"] = False
|
|
253
|
+
result["pattern"] = "unknown"
|
|
254
|
+
result["indicators"] = [f"{len(branch_set)} branches, pattern unclear"]
|
|
255
|
+
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _collect_branch_names(directory: Path, prefix: str) -> List[str]:
|
|
260
|
+
"""Recursively collect branch names from refs directory.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
directory: Path to scan for ref files.
|
|
264
|
+
prefix: Current path prefix for nested refs (e.g., 'feature/').
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of branch name strings.
|
|
268
|
+
"""
|
|
269
|
+
names: List[str] = []
|
|
270
|
+
try:
|
|
271
|
+
for entry in directory.iterdir():
|
|
272
|
+
name = f"{prefix}{entry.name}" if prefix else entry.name
|
|
273
|
+
if entry.is_file():
|
|
274
|
+
# Skip HEAD files in remote dirs
|
|
275
|
+
if entry.name != "HEAD":
|
|
276
|
+
names.append(name)
|
|
277
|
+
elif entry.is_dir():
|
|
278
|
+
names.extend(_collect_branch_names(entry, f"{name}/"))
|
|
279
|
+
except OSError:
|
|
280
|
+
pass
|
|
281
|
+
return names
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _detect_monorepo(root: Path) -> Dict[str, Any]:
|
|
285
|
+
"""Detect monorepo workspace configuration.
|
|
286
|
+
|
|
287
|
+
Checks for:
|
|
288
|
+
- npm workspaces: 'workspaces' field in package.json
|
|
289
|
+
- pnpm workspaces: pnpm-workspace.yaml
|
|
290
|
+
- Turborepo: turbo.json
|
|
291
|
+
- Nx: nx.json
|
|
292
|
+
- Lerna: lerna.json
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
root: Project root path.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Dict with 'workspace_config' (str or None).
|
|
299
|
+
"""
|
|
300
|
+
# Check indicator files first (turbo, nx, lerna, pnpm)
|
|
301
|
+
for filename, config_name in _MONOREPO_INDICATORS:
|
|
302
|
+
if (root / filename).is_file():
|
|
303
|
+
return {"workspace_config": config_name}
|
|
304
|
+
|
|
305
|
+
# Check npm workspaces in package.json
|
|
306
|
+
package_json = root / "package.json"
|
|
307
|
+
if package_json.is_file():
|
|
308
|
+
try:
|
|
309
|
+
data = json.loads(package_json.read_text())
|
|
310
|
+
if "workspaces" in data:
|
|
311
|
+
return {"workspace_config": "npm"}
|
|
312
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
313
|
+
logger.debug("Failed to read package.json for workspaces: %s", exc)
|
|
314
|
+
|
|
315
|
+
return {"workspace_config": None}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _determine_primary_platform(
|
|
319
|
+
remotes: List[Dict[str, Any]],
|
|
320
|
+
) -> Optional[str]:
|
|
321
|
+
"""Determine the primary platform from the list of remotes.
|
|
322
|
+
|
|
323
|
+
Priority: origin remote first, then first remote with a known platform.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
remotes: List of remote dicts with 'name', 'url', 'platform'.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Platform string or None.
|
|
330
|
+
"""
|
|
331
|
+
if not remotes:
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
# Prefer origin
|
|
335
|
+
for remote in remotes:
|
|
336
|
+
if remote.get("name") == "origin" and remote.get("platform"):
|
|
337
|
+
return remote["platform"]
|
|
338
|
+
|
|
339
|
+
# Fall back to first remote with a known platform
|
|
340
|
+
for remote in remotes:
|
|
341
|
+
if remote.get("platform"):
|
|
342
|
+
return remote["platform"]
|
|
343
|
+
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class GitScanner(BaseScanner):
|
|
348
|
+
"""Scanner for git repository configuration.
|
|
349
|
+
|
|
350
|
+
Detects platform, remotes, default branch, branch strategy, and
|
|
351
|
+
monorepo workspace configuration. Returns the `git` section per
|
|
352
|
+
data-model.md section 2.5.
|
|
353
|
+
|
|
354
|
+
Always returns a git section even when no .git directory exists
|
|
355
|
+
(platform=null, remotes=[]).
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def SCANNER_NAME(self) -> str:
|
|
360
|
+
return "git"
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def SCANNER_VERSION(self) -> str:
|
|
364
|
+
return "1.0.0"
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def OWNED_SECTIONS(self) -> List[str]:
|
|
368
|
+
return ["git"]
|
|
369
|
+
|
|
370
|
+
def scan(self, root: Path) -> ScanResult:
|
|
371
|
+
"""Scan the project for git configuration.
|
|
372
|
+
|
|
373
|
+
In multi-repo mode (workspace_info.is_multi_repo), scans ALL
|
|
374
|
+
subdirectories with .git and produces a 'repos' array. In
|
|
375
|
+
single-repo mode, behaves as before.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
root: Absolute path to the project root directory.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
ScanResult with the 'git' section populated.
|
|
382
|
+
"""
|
|
383
|
+
start_ms = time.monotonic() * 1000
|
|
384
|
+
warnings: List[str] = []
|
|
385
|
+
|
|
386
|
+
# Multi-repo mode: scan all repo subdirectories
|
|
387
|
+
if self.workspace_info and self.workspace_info.is_multi_repo:
|
|
388
|
+
section = self._scan_multi_repo(root, warnings)
|
|
389
|
+
elapsed = (time.monotonic() * 1000) - start_ms
|
|
390
|
+
return self.make_result(
|
|
391
|
+
sections={"git": section},
|
|
392
|
+
warnings=warnings,
|
|
393
|
+
duration_ms=elapsed,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Single-repo mode (original behavior)
|
|
397
|
+
git_dir = root / ".git"
|
|
398
|
+
git_root = root
|
|
399
|
+
|
|
400
|
+
if not git_dir.is_dir():
|
|
401
|
+
# Look in immediate subdirectories for .git
|
|
402
|
+
git_dir, git_root = self._find_git_in_subdirs(root)
|
|
403
|
+
|
|
404
|
+
if git_dir is None:
|
|
405
|
+
# No .git directory found at root or in subdirectories
|
|
406
|
+
# Note: monorepo detection is owned by StackScanner, not duplicated here
|
|
407
|
+
section: Dict[str, Any] = {
|
|
408
|
+
"platform": None,
|
|
409
|
+
"remotes": [],
|
|
410
|
+
"default_branch": None,
|
|
411
|
+
"branch_strategy": {
|
|
412
|
+
"detected": False,
|
|
413
|
+
"pattern": None,
|
|
414
|
+
"indicators": [],
|
|
415
|
+
},
|
|
416
|
+
"monorepo": {"workspace_config": None},
|
|
417
|
+
}
|
|
418
|
+
elapsed = (time.monotonic() * 1000) - start_ms
|
|
419
|
+
return self.make_result(
|
|
420
|
+
sections={"git": section},
|
|
421
|
+
warnings=["No .git directory found"],
|
|
422
|
+
duration_ms=elapsed,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Track if git was found in a subdirectory
|
|
426
|
+
if git_root != root:
|
|
427
|
+
warnings.append(
|
|
428
|
+
f".git found in subdirectory: {git_root.name}/"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Parse remotes from .git/config
|
|
432
|
+
git_config = _parse_git_config(git_dir)
|
|
433
|
+
remotes = git_config["remotes"]
|
|
434
|
+
|
|
435
|
+
# Determine primary platform
|
|
436
|
+
platform = _determine_primary_platform(remotes)
|
|
437
|
+
|
|
438
|
+
# Detect default branch from HEAD
|
|
439
|
+
default_branch = _detect_default_branch(git_dir)
|
|
440
|
+
|
|
441
|
+
# Detect branch strategy
|
|
442
|
+
branch_strategy = _detect_branch_strategy(git_dir)
|
|
443
|
+
|
|
444
|
+
# Note: monorepo detection is owned by StackScanner, not duplicated here
|
|
445
|
+
section: Dict[str, Any] = {
|
|
446
|
+
"platform": platform,
|
|
447
|
+
"remotes": remotes,
|
|
448
|
+
"default_branch": default_branch,
|
|
449
|
+
"branch_strategy": branch_strategy,
|
|
450
|
+
"monorepo": {"workspace_config": None},
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# Include git_root when it differs from the scan root
|
|
454
|
+
if git_root != root:
|
|
455
|
+
section["git_root"] = str(git_root.relative_to(root))
|
|
456
|
+
|
|
457
|
+
elapsed = (time.monotonic() * 1000) - start_ms
|
|
458
|
+
return self.make_result(
|
|
459
|
+
sections={"git": section},
|
|
460
|
+
warnings=warnings,
|
|
461
|
+
duration_ms=elapsed,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
def _scan_multi_repo(
|
|
465
|
+
self, root: Path, warnings: List[str]
|
|
466
|
+
) -> Dict[str, Any]:
|
|
467
|
+
"""Scan all repos in a multi-repo workspace.
|
|
468
|
+
|
|
469
|
+
Produces a section with 'repos' array where each entry has:
|
|
470
|
+
name, path, remote_url, platform, default_branch.
|
|
471
|
+
|
|
472
|
+
Also determines the primary platform from the first repo's origin.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
root: Workspace root path.
|
|
476
|
+
warnings: Warning accumulator.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Git section dict with 'repos' array and aggregate fields.
|
|
480
|
+
"""
|
|
481
|
+
repos: List[Dict[str, Any]] = []
|
|
482
|
+
primary_platform: Optional[str] = None
|
|
483
|
+
|
|
484
|
+
for repo_dir in self.workspace_info.repo_dirs:
|
|
485
|
+
git_dir = repo_dir / ".git"
|
|
486
|
+
if not git_dir.is_dir():
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
git_config = _parse_git_config(git_dir)
|
|
490
|
+
remotes = git_config["remotes"]
|
|
491
|
+
platform = _determine_primary_platform(remotes)
|
|
492
|
+
default_branch = _detect_default_branch(git_dir)
|
|
493
|
+
|
|
494
|
+
# Get origin remote URL
|
|
495
|
+
origin_url = None
|
|
496
|
+
for remote in remotes:
|
|
497
|
+
if remote.get("name") == "origin":
|
|
498
|
+
origin_url = remote.get("url")
|
|
499
|
+
break
|
|
500
|
+
if origin_url is None and remotes:
|
|
501
|
+
origin_url = remotes[0].get("url")
|
|
502
|
+
|
|
503
|
+
repo_entry: Dict[str, Any] = {
|
|
504
|
+
"name": repo_dir.name,
|
|
505
|
+
"path": str(repo_dir.relative_to(root)),
|
|
506
|
+
"remote_url": origin_url,
|
|
507
|
+
"platform": platform,
|
|
508
|
+
"default_branch": default_branch,
|
|
509
|
+
}
|
|
510
|
+
repos.append(repo_entry)
|
|
511
|
+
|
|
512
|
+
if primary_platform is None and platform:
|
|
513
|
+
primary_platform = platform
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
"platform": primary_platform,
|
|
517
|
+
"workspace_type": "multi-repo",
|
|
518
|
+
"repos": repos,
|
|
519
|
+
"branch_strategy": {
|
|
520
|
+
"detected": False,
|
|
521
|
+
"pattern": None,
|
|
522
|
+
"indicators": ["multi-repo workspace — per-repo strategies vary"],
|
|
523
|
+
},
|
|
524
|
+
"monorepo": {"workspace_config": None},
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@staticmethod
|
|
528
|
+
def _find_git_in_subdirs(
|
|
529
|
+
root: Path,
|
|
530
|
+
) -> Tuple[Optional[Path], Path]:
|
|
531
|
+
"""Look for .git in immediate subdirectories.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
root: Scan root directory.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Tuple of (git_dir, git_root) if found, or (None, root) if not.
|
|
538
|
+
"""
|
|
539
|
+
try:
|
|
540
|
+
for entry in sorted(root.iterdir()):
|
|
541
|
+
if not entry.is_dir() or entry.name.startswith("."):
|
|
542
|
+
continue
|
|
543
|
+
if entry.name in ("node_modules", "vendor", "__pycache__"):
|
|
544
|
+
continue
|
|
545
|
+
candidate = entry / ".git"
|
|
546
|
+
if candidate.is_dir():
|
|
547
|
+
return candidate, entry
|
|
548
|
+
except OSError:
|
|
549
|
+
pass
|
|
550
|
+
return None, root
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# Module-level convenience for T009 task verify command compatibility
|
|
554
|
+
SCANNER_NAME = GitScanner.SCANNER_NAME.fget(GitScanner()) # type: ignore[union-attr]
|
|
555
|
+
SCANNER_VERSION = GitScanner.SCANNER_VERSION.fget(GitScanner()) # type: ignore[union-attr]
|
|
556
|
+
OWNED_SECTIONS = GitScanner.OWNED_SECTIONS.fget(GitScanner()) # type: ignore[union-attr]
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def scan(root: Path) -> Dict[str, Any]:
|
|
560
|
+
"""Module-level scan function for backward compatibility with T009 verify.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
root: Absolute path to the project root directory.
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
Dict mapping section names to section data.
|
|
567
|
+
"""
|
|
568
|
+
scanner = GitScanner()
|
|
569
|
+
result = scanner.scan(root)
|
|
570
|
+
return result.sections
|