@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.
Files changed (371) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +12 -3
  3. package/ARCHITECTURE.md +9 -8
  4. package/CHANGELOG.md +34 -0
  5. package/README.md +43 -11
  6. package/agents/terraform-architect.md +1 -1
  7. package/bin/README.md +2 -2
  8. package/bin/gaia-doctor.js +18 -5
  9. package/bin/gaia-history.js +0 -1
  10. package/bin/gaia-metrics.js +2 -2
  11. package/bin/gaia-scan.py +23 -1
  12. package/bin/gaia-update.js +346 -54
  13. package/bin/pre-publish-validate.js +33 -10
  14. package/commands/gaia.md +37 -0
  15. package/config/README.md +3 -9
  16. package/config/context-contracts.json +47 -15
  17. package/config/surface-routing.json +9 -1
  18. package/dist/gaia-ops/.claude-plugin/plugin.json +22 -0
  19. package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
  20. package/dist/gaia-ops/agents/devops-developer.md +57 -0
  21. package/dist/gaia-ops/agents/gaia-system.md +58 -0
  22. package/dist/gaia-ops/agents/gitops-operator.md +60 -0
  23. package/dist/gaia-ops/agents/speckit-planner.md +71 -0
  24. package/dist/gaia-ops/agents/terraform-architect.md +60 -0
  25. package/dist/gaia-ops/commands/gaia.md +37 -0
  26. package/dist/gaia-ops/config/README.md +58 -0
  27. package/dist/gaia-ops/config/cloud/aws.json +140 -0
  28. package/dist/gaia-ops/config/cloud/gcp.json +145 -0
  29. package/dist/gaia-ops/config/context-contracts.json +131 -0
  30. package/dist/gaia-ops/config/git_standards.json +72 -0
  31. package/dist/gaia-ops/config/surface-routing.json +197 -0
  32. package/dist/gaia-ops/config/universal-rules.json +10 -0
  33. package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
  34. package/dist/gaia-ops/hooks/adapters/base.py +219 -0
  35. package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
  36. package/dist/gaia-ops/hooks/adapters/claude_code.py +1477 -0
  37. package/dist/gaia-ops/hooks/adapters/types.py +194 -0
  38. package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
  39. package/dist/gaia-ops/hooks/hooks.json +126 -0
  40. package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
  41. package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
  42. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
  43. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
  44. package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +124 -0
  45. package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
  46. package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
  47. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
  48. package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
  49. package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
  50. package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
  51. package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
  52. package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +576 -0
  53. package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
  54. package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
  55. package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
  56. package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +215 -0
  57. package/dist/gaia-ops/hooks/modules/context/context_cache.py +129 -0
  58. package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
  59. package/dist/gaia-ops/hooks/modules/context/context_injector.py +427 -0
  60. package/dist/gaia-ops/hooks/modules/context/context_writer.py +518 -0
  61. package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
  62. package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
  63. package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
  64. package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
  65. package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
  66. package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +558 -0
  67. package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
  68. package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
  69. package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
  70. package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
  71. package/dist/gaia-ops/hooks/modules/identity/__init__.py +0 -0
  72. package/dist/gaia-ops/hooks/modules/identity/identity_provider.py +21 -0
  73. package/dist/gaia-ops/hooks/modules/identity/ops_identity.py +34 -0
  74. package/dist/gaia-ops/hooks/modules/identity/security_identity.py +10 -0
  75. package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
  76. package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +227 -0
  77. package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
  78. package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +128 -0
  79. package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
  80. package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
  81. package/dist/gaia-ops/hooks/modules/security/__init__.py +89 -0
  82. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
  83. package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
  84. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +912 -0
  85. package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
  86. package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +153 -0
  87. package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +584 -0
  88. package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +86 -0
  89. package/dist/gaia-ops/hooks/modules/security/command_semantics.py +130 -0
  90. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
  91. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +850 -0
  92. package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
  93. package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
  94. package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
  95. package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
  96. package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +158 -0
  97. package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
  98. package/dist/gaia-ops/hooks/modules/tools/__init__.py +25 -0
  99. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +708 -0
  100. package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +181 -0
  101. package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
  102. package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
  103. package/dist/gaia-ops/hooks/modules/tools/task_validator.py +283 -0
  104. package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
  105. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
  106. package/dist/gaia-ops/hooks/post_compact.py +43 -0
  107. package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
  108. package/dist/gaia-ops/hooks/pre_tool_use.py +383 -0
  109. package/dist/gaia-ops/hooks/session_start.py +69 -0
  110. package/dist/gaia-ops/hooks/stop_hook.py +69 -0
  111. package/dist/gaia-ops/hooks/subagent_start.py +71 -0
  112. package/dist/gaia-ops/hooks/subagent_stop.py +288 -0
  113. package/dist/gaia-ops/hooks/task_completed.py +70 -0
  114. package/dist/gaia-ops/hooks/user_prompt_submit.py +177 -0
  115. package/dist/gaia-ops/settings.json +72 -0
  116. package/dist/gaia-ops/skills/README.md +109 -0
  117. package/dist/gaia-ops/skills/agent-protocol/SKILL.md +105 -0
  118. package/dist/gaia-ops/skills/agent-protocol/examples.md +170 -0
  119. package/dist/gaia-ops/skills/agent-response/SKILL.md +53 -0
  120. package/dist/gaia-ops/skills/approval/SKILL.md +85 -0
  121. package/dist/gaia-ops/skills/approval/examples.md +140 -0
  122. package/dist/gaia-ops/skills/approval/reference.md +57 -0
  123. package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
  124. package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
  125. package/dist/gaia-ops/skills/context-updater/SKILL.md +76 -0
  126. package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
  127. package/dist/gaia-ops/skills/developer-patterns/SKILL.md +93 -0
  128. package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
  129. package/dist/gaia-ops/skills/execution/SKILL.md +66 -0
  130. package/dist/gaia-ops/skills/fast-queries/SKILL.md +47 -0
  131. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +92 -0
  132. package/dist/gaia-ops/skills/gaia-patterns/reference.md +22 -0
  133. package/dist/gaia-ops/skills/git-conventions/SKILL.md +48 -0
  134. package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +73 -0
  135. package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
  136. package/dist/gaia-ops/skills/investigation/SKILL.md +77 -0
  137. package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +64 -0
  138. package/dist/gaia-ops/skills/reference.md +134 -0
  139. package/dist/gaia-ops/skills/security-tiers/SKILL.md +61 -0
  140. package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
  141. package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
  142. package/dist/gaia-ops/skills/skill-creation/SKILL.md +119 -0
  143. package/dist/gaia-ops/skills/specification/SKILL.md +186 -0
  144. package/dist/gaia-ops/skills/speckit-workflow/SKILL.md +165 -0
  145. package/dist/gaia-ops/skills/speckit-workflow/reference.md +117 -0
  146. package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +63 -0
  147. package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
  148. package/dist/gaia-ops/speckit/README.md +516 -0
  149. package/dist/gaia-ops/speckit/scripts/.gitkeep +0 -0
  150. package/dist/gaia-ops/speckit/templates/adr-template.md +118 -0
  151. package/dist/gaia-ops/speckit/templates/agent-file-template.md +23 -0
  152. package/dist/gaia-ops/speckit/templates/plan-template.md +227 -0
  153. package/dist/gaia-ops/speckit/templates/spec-template.md +140 -0
  154. package/dist/gaia-ops/speckit/templates/tasks-template.md +257 -0
  155. package/dist/gaia-ops/tools/context/README.md +132 -0
  156. package/dist/gaia-ops/tools/context/__init__.py +42 -0
  157. package/dist/gaia-ops/tools/context/_paths.py +20 -0
  158. package/dist/gaia-ops/tools/context/context_provider.py +476 -0
  159. package/dist/gaia-ops/tools/context/context_section_reader.py +330 -0
  160. package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
  161. package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
  162. package/dist/gaia-ops/tools/context/surface_router.py +278 -0
  163. package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
  164. package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
  165. package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  166. package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  167. package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  168. package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  169. package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
  170. package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  171. package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
  172. package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
  173. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
  174. package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
  175. package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
  176. package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
  177. package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +262 -0
  178. package/dist/gaia-ops/tools/memory/README.md +0 -0
  179. package/dist/gaia-ops/tools/memory/__init__.py +20 -0
  180. package/dist/gaia-ops/tools/memory/episodic.py +1196 -0
  181. package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
  182. package/dist/gaia-ops/tools/review/__init__.py +1 -0
  183. package/dist/gaia-ops/tools/review/review_engine.py +157 -0
  184. package/dist/gaia-ops/tools/scan/__init__.py +35 -0
  185. package/dist/gaia-ops/tools/scan/config.py +247 -0
  186. package/dist/gaia-ops/tools/scan/merge.py +212 -0
  187. package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
  188. package/dist/gaia-ops/tools/scan/registry.py +127 -0
  189. package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
  190. package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
  191. package/dist/gaia-ops/tools/scan/scanners/environment.py +324 -0
  192. package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
  193. package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
  194. package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
  195. package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
  196. package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
  197. package/dist/gaia-ops/tools/scan/setup.py +753 -0
  198. package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
  199. package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
  200. package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
  201. package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
  202. package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
  203. package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
  204. package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
  205. package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
  206. package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
  207. package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
  208. package/dist/gaia-ops/tools/scan/ui.py +624 -0
  209. package/dist/gaia-ops/tools/scan/verify.py +266 -0
  210. package/dist/gaia-ops/tools/scan/walk.py +118 -0
  211. package/dist/gaia-ops/tools/scan/workspace.py +85 -0
  212. package/dist/gaia-ops/tools/validation/README.md +244 -0
  213. package/dist/gaia-ops/tools/validation/__init__.py +17 -0
  214. package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
  215. package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
  216. package/dist/gaia-security/.claude-plugin/plugin.json +22 -0
  217. package/dist/gaia-security/config/universal-rules.json +10 -0
  218. package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
  219. package/dist/gaia-security/hooks/adapters/base.py +219 -0
  220. package/dist/gaia-security/hooks/adapters/channel.py +17 -0
  221. package/dist/gaia-security/hooks/adapters/claude_code.py +1477 -0
  222. package/dist/gaia-security/hooks/adapters/types.py +194 -0
  223. package/dist/gaia-security/hooks/adapters/utils.py +25 -0
  224. package/dist/gaia-security/hooks/hooks.json +57 -0
  225. package/dist/gaia-security/hooks/modules/__init__.py +15 -0
  226. package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
  227. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
  228. package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
  229. package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +124 -0
  230. package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
  231. package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
  232. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
  233. package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
  234. package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
  235. package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
  236. package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
  237. package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +576 -0
  238. package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
  239. package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
  240. package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
  241. package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +215 -0
  242. package/dist/gaia-security/hooks/modules/context/context_cache.py +129 -0
  243. package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
  244. package/dist/gaia-security/hooks/modules/context/context_injector.py +427 -0
  245. package/dist/gaia-security/hooks/modules/context/context_writer.py +518 -0
  246. package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
  247. package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
  248. package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
  249. package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
  250. package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
  251. package/dist/gaia-security/hooks/modules/core/plugin_setup.py +558 -0
  252. package/dist/gaia-security/hooks/modules/core/state.py +179 -0
  253. package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
  254. package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
  255. package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
  256. package/dist/gaia-security/hooks/modules/identity/__init__.py +0 -0
  257. package/dist/gaia-security/hooks/modules/identity/identity_provider.py +21 -0
  258. package/dist/gaia-security/hooks/modules/identity/ops_identity.py +34 -0
  259. package/dist/gaia-security/hooks/modules/identity/security_identity.py +10 -0
  260. package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
  261. package/dist/gaia-security/hooks/modules/memory/episode_writer.py +227 -0
  262. package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
  263. package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +128 -0
  264. package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
  265. package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
  266. package/dist/gaia-security/hooks/modules/security/__init__.py +89 -0
  267. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
  268. package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
  269. package/dist/gaia-security/hooks/modules/security/approval_grants.py +912 -0
  270. package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
  271. package/dist/gaia-security/hooks/modules/security/approval_scopes.py +153 -0
  272. package/dist/gaia-security/hooks/modules/security/blocked_commands.py +584 -0
  273. package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +86 -0
  274. package/dist/gaia-security/hooks/modules/security/command_semantics.py +130 -0
  275. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
  276. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +850 -0
  277. package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
  278. package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
  279. package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
  280. package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
  281. package/dist/gaia-security/hooks/modules/session/session_event_injector.py +158 -0
  282. package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
  283. package/dist/gaia-security/hooks/modules/tools/__init__.py +25 -0
  284. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +708 -0
  285. package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +181 -0
  286. package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
  287. package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
  288. package/dist/gaia-security/hooks/modules/tools/task_validator.py +283 -0
  289. package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
  290. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
  291. package/dist/gaia-security/hooks/post_tool_use.py +54 -0
  292. package/dist/gaia-security/hooks/pre_tool_use.py +383 -0
  293. package/dist/gaia-security/hooks/session_start.py +69 -0
  294. package/dist/gaia-security/hooks/stop_hook.py +69 -0
  295. package/dist/gaia-security/hooks/user_prompt_submit.py +177 -0
  296. package/dist/gaia-security/settings.json +58 -0
  297. package/git-hooks/commit-msg +41 -0
  298. package/hooks/README.md +8 -6
  299. package/hooks/adapters/channel.py +0 -25
  300. package/hooks/adapters/claude_code.py +364 -125
  301. package/hooks/elicitation_result.py +132 -0
  302. package/hooks/hooks.json +10 -1
  303. package/hooks/modules/README.md +3 -2
  304. package/hooks/modules/agents/contract_validator.py +3 -51
  305. package/hooks/modules/agents/response_contract.py +4 -8
  306. package/hooks/modules/agents/transcript_reader.py +4 -5
  307. package/hooks/modules/audit/__init__.py +4 -6
  308. package/hooks/modules/audit/event_detector.py +0 -2
  309. package/hooks/modules/audit/metrics.py +108 -187
  310. package/hooks/modules/audit/workflow_auditor.py +0 -4
  311. package/hooks/modules/audit/workflow_recorder.py +0 -5
  312. package/hooks/modules/context/compact_context_builder.py +1 -0
  313. package/hooks/modules/context/context_cache.py +129 -0
  314. package/hooks/modules/context/context_injector.py +18 -40
  315. package/hooks/modules/context/context_writer.py +1 -25
  316. package/hooks/modules/context/contracts_loader.py +7 -10
  317. package/hooks/modules/core/hook_entry.py +1 -0
  318. package/hooks/modules/core/paths.py +12 -13
  319. package/hooks/modules/core/plugin_mode.py +74 -4
  320. package/hooks/modules/core/plugin_setup.py +395 -23
  321. package/hooks/modules/events/__init__.py +1 -0
  322. package/hooks/modules/events/event_writer.py +210 -0
  323. package/hooks/modules/identity/ops_identity.py +18 -27
  324. package/hooks/modules/memory/episode_writer.py +1 -6
  325. package/hooks/modules/orchestrator/__init__.py +1 -0
  326. package/hooks/modules/orchestrator/delegate_mode.py +128 -0
  327. package/hooks/modules/security/__init__.py +2 -4
  328. package/hooks/modules/security/approval_constants.py +5 -1
  329. package/hooks/modules/security/approval_grants.py +189 -6
  330. package/hooks/modules/security/approval_messages.py +9 -21
  331. package/hooks/modules/security/blocked_commands.py +98 -34
  332. package/hooks/modules/security/command_semantics.py +0 -4
  333. package/hooks/modules/security/gitops_validator.py +1 -11
  334. package/hooks/modules/security/mutative_verbs.py +179 -38
  335. package/hooks/modules/security/tiers.py +1 -19
  336. package/hooks/modules/session/session_event_injector.py +1 -25
  337. package/hooks/modules/tools/bash_validator.py +310 -94
  338. package/hooks/modules/tools/shell_parser.py +0 -1
  339. package/hooks/modules/tools/task_validator.py +9 -29
  340. package/hooks/post_tool_use.py +0 -72
  341. package/hooks/pre_tool_use.py +42 -102
  342. package/hooks/session_start.py +4 -2
  343. package/hooks/subagent_start.py +6 -2
  344. package/hooks/subagent_stop.py +1 -13
  345. package/hooks/user_prompt_submit.py +119 -37
  346. package/index.js +1 -1
  347. package/package.json +5 -3
  348. package/skills/README.md +3 -5
  349. package/skills/agent-protocol/SKILL.md +17 -16
  350. package/skills/agent-protocol/examples.md +6 -6
  351. package/skills/agent-response/SKILL.md +11 -14
  352. package/skills/approval/SKILL.md +28 -13
  353. package/skills/approval/reference.md +2 -2
  354. package/skills/execution/SKILL.md +1 -1
  355. package/skills/gaia-patterns/SKILL.md +2 -3
  356. package/skills/orchestrator-approval/SKILL.md +22 -50
  357. package/skills/security-tiers/SKILL.md +1 -1
  358. package/templates/README.md +9 -9
  359. package/templates/managed-settings.template.json +43 -0
  360. package/tools/gaia_simulator/runner.py +34 -1
  361. package/tools/scan/orchestrator.py +13 -0
  362. package/tools/scan/scanners/base.py +8 -0
  363. package/tools/scan/scanners/git.py +78 -0
  364. package/tools/scan/scanners/infrastructure.py +65 -0
  365. package/tools/scan/scanners/stack.py +110 -0
  366. package/tools/scan/setup.py +120 -13
  367. package/tools/scan/workspace.py +85 -0
  368. package/config/context-contracts.aws.json +0 -42
  369. package/config/context-contracts.gcp.json +0 -39
  370. package/skills/project-dispatch/SKILL.md +0 -34
  371. package/templates/settings.template.json +0 -226
@@ -0,0 +1,875 @@
1
+ """
2
+ Infrastructure Scanner
3
+
4
+ Detects cloud providers, IaC tools, container tooling, CI/CD platforms,
5
+ application services, and infrastructure-related directory paths. Only
6
+ produces the 'infrastructure' section when at least one indicator is found;
7
+ returns empty dict for projects with no infrastructure files.
8
+
9
+ Schema: data-model.md section 2.7
10
+ Contract: contracts/scanner-interface.md
11
+ """
12
+
13
+ import logging
14
+ import os
15
+ import re
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional, Set
19
+
20
+ from tools.scan.scanners.base import BaseScanner, ScanResult
21
+ from tools.scan.walk import walk_project, walk_project_named
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Terraform provider patterns
27
+ # ---------------------------------------------------------------------------
28
+ _TF_PROVIDER_PATTERNS: Dict[str, str] = {
29
+ "gcp": r'provider\s+"google"',
30
+ "aws": r'provider\s+"aws"',
31
+ "azure": r'provider\s+"azurerm"',
32
+ }
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Cloud-related environment variable prefixes (non-secret only)
36
+ # ---------------------------------------------------------------------------
37
+ _CLOUD_ENV_VARS: Dict[str, List[str]] = {
38
+ "gcp": [
39
+ "GOOGLE_CLOUD_PROJECT",
40
+ "GCLOUD_PROJECT",
41
+ "GCP_PROJECT",
42
+ "CLOUDSDK_CORE_PROJECT",
43
+ ],
44
+ "aws": [
45
+ "AWS_DEFAULT_REGION",
46
+ "AWS_REGION",
47
+ ],
48
+ "azure": [
49
+ "AZURE_SUBSCRIPTION_ID",
50
+ "ARM_SUBSCRIPTION_ID",
51
+ ],
52
+ }
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # IaC tool markers
56
+ # ---------------------------------------------------------------------------
57
+ _IAC_MARKERS: List[Dict[str, str]] = [
58
+ {"tool": "terraform", "rglob": "*.tf"},
59
+ {"tool": "terragrunt", "rglob": "terragrunt.hcl"},
60
+ {"tool": "pulumi", "rglob": "Pulumi.yaml"},
61
+ {"tool": "cdk", "rglob": "cdk.json"},
62
+ ]
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Container markers
66
+ # ---------------------------------------------------------------------------
67
+ _CONTAINER_GLOBS: List[Dict[str, Any]] = [
68
+ {"tool": "docker", "rglob_patterns": ["Dockerfile", "Dockerfile.*"]},
69
+ {"tool": "docker-compose", "rglob_patterns": ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]},
70
+ ]
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # CI/CD platform markers
74
+ # ---------------------------------------------------------------------------
75
+ _CICD_MARKERS: List[Dict[str, Any]] = [
76
+ {"platform": "github-actions", "type": "dir", "path": ".github/workflows"},
77
+ {"platform": "gitlab-ci", "type": "file", "path": ".gitlab-ci.yml"},
78
+ {"platform": "jenkins", "type": "file", "path": "Jenkinsfile"},
79
+ {"platform": "circleci", "type": "dir", "path": ".circleci"},
80
+ {"platform": "bitbucket-pipelines", "type": "file", "path": "bitbucket-pipelines.yml"},
81
+ {"platform": "cloud-build", "type": "file", "path": "cloudbuild.yaml"},
82
+ {"platform": "cloud-build", "type": "file", "path": "cloudbuild.json"},
83
+ ]
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Known infrastructure directory names
87
+ # ---------------------------------------------------------------------------
88
+ _INFRA_DIR_NAMES: Dict[str, List[str]] = {
89
+ "gitops": ["gitops", "flux", "argocd", "deploy", "k8s"],
90
+ "terraform": ["terraform", "terragrunt", "infra", "infrastructure"],
91
+ "app_services": ["app_services", "app-services", "services", "apps"],
92
+ }
93
+
94
+
95
+ class InfrastructureScanner(BaseScanner):
96
+ """Detects cloud providers, IaC, containers, CI/CD, and infra paths.
97
+
98
+ Pure function contract:
99
+ - No file writes
100
+ - No state modification
101
+ - No network calls
102
+ - Only reads: filesystem paths, file contents, environment variables
103
+ """
104
+
105
+ @property
106
+ def SCANNER_NAME(self) -> str:
107
+ return "infrastructure"
108
+
109
+ @property
110
+ def SCANNER_VERSION(self) -> str:
111
+ return "1.0.0"
112
+
113
+ @property
114
+ def OWNED_SECTIONS(self) -> List[str]:
115
+ return ["infrastructure", "application_services"]
116
+
117
+ def scan(self, root: Path) -> ScanResult:
118
+ """Scan for infrastructure indicators.
119
+
120
+ In multi-repo mode, scans each repo subdirectory and tags IaC
121
+ entries with their containing repo name. In single-repo mode,
122
+ behaves as before.
123
+
124
+ Args:
125
+ root: Absolute path to the project root directory.
126
+
127
+ Returns:
128
+ ScanResult with 'infrastructure' section if indicators found,
129
+ or empty sections dict if none detected.
130
+ """
131
+ start = time.monotonic()
132
+ warnings: List[str] = []
133
+
134
+ try:
135
+ cloud_providers = self._detect_cloud_providers(root, warnings)
136
+ iac = self._detect_iac(root, warnings)
137
+ containers = self._detect_containers(root, warnings)
138
+ ci_cd = self._detect_cicd(root, warnings)
139
+ paths = self._detect_paths(root, warnings)
140
+ app_services = self._detect_application_services(root, warnings)
141
+
142
+ # Multi-repo mode: also scan each repo subdirectory and tag results
143
+ if self.workspace_info and self.workspace_info.is_multi_repo:
144
+ self._enrich_multi_repo(
145
+ root, iac, containers, ci_cd, warnings
146
+ )
147
+
148
+ # Only produce section when at least one indicator is found
149
+ has_indicators = (
150
+ cloud_providers
151
+ or iac
152
+ or containers
153
+ or ci_cd
154
+ or paths.get("gitops") is not None
155
+ or paths.get("terraform") is not None
156
+ or paths.get("app_services") is not None
157
+ )
158
+
159
+ if not has_indicators and not app_services:
160
+ duration_ms = (time.monotonic() - start) * 1000
161
+ return self.make_result(sections={}, warnings=warnings, duration_ms=duration_ms)
162
+
163
+ sections: Dict[str, Any] = {}
164
+
165
+ if has_indicators:
166
+ sections["infrastructure"] = {
167
+ "cloud_providers": cloud_providers,
168
+ "iac": iac,
169
+ "containers": containers,
170
+ "ci_cd": ci_cd,
171
+ "paths": paths,
172
+ }
173
+
174
+ if app_services:
175
+ sections["application_services"] = {
176
+ "services": app_services,
177
+ "base_path": self._common_base_path(
178
+ [s["path"] for s in app_services]
179
+ ),
180
+ }
181
+
182
+ duration_ms = (time.monotonic() - start) * 1000
183
+ return self.make_result(sections=sections, warnings=warnings, duration_ms=duration_ms)
184
+
185
+ except Exception as exc:
186
+ logger.warning("Infrastructure scanner failed: %s", exc)
187
+ duration_ms = (time.monotonic() - start) * 1000
188
+ return self.make_result(sections={}, warnings=[str(exc)], duration_ms=duration_ms)
189
+
190
+ def _enrich_multi_repo(
191
+ self,
192
+ root: Path,
193
+ iac: List[Dict[str, Any]],
194
+ containers: List[Dict[str, Any]],
195
+ ci_cd: List[Dict[str, Any]],
196
+ warnings: List[str],
197
+ ) -> None:
198
+ """Tag IaC, container, and CI/CD entries with their containing repo.
199
+
200
+ For each entry whose base_path or config_path starts with a known
201
+ repo directory, adds a 'repo' field with the repo name. This helps
202
+ agents understand which repo owns each infrastructure component.
203
+
204
+ Args:
205
+ root: Workspace root path.
206
+ iac: IaC entries list (mutated in place).
207
+ containers: Container entries list (mutated in place).
208
+ ci_cd: CI/CD entries list (mutated in place).
209
+ warnings: Warning accumulator.
210
+ """
211
+ repo_names = {
212
+ str(rd.relative_to(root)): rd.name
213
+ for rd in self.workspace_info.repo_dirs
214
+ }
215
+
216
+ def _tag_repo(entry: Dict[str, Any], path_key: str) -> None:
217
+ """Add 'repo' field if path matches a known repo directory."""
218
+ path_val = entry.get(path_key, "")
219
+ if not path_val:
220
+ return
221
+ for repo_path, repo_name in repo_names.items():
222
+ if path_val == repo_path or path_val.startswith(repo_path + "/"):
223
+ entry["repo"] = repo_name
224
+ return
225
+
226
+ for entry in iac:
227
+ _tag_repo(entry, "base_path")
228
+
229
+ for entry in containers:
230
+ # Containers use 'files' list; tag based on first file's directory
231
+ files = entry.get("files", [])
232
+ if files:
233
+ first_file = files[0]
234
+ for repo_path, repo_name in repo_names.items():
235
+ if first_file.startswith(repo_path + "/") or first_file.startswith(repo_path):
236
+ entry["repo"] = repo_name
237
+ break
238
+
239
+ for entry in ci_cd:
240
+ _tag_repo(entry, "config_path")
241
+
242
+ # ------------------------------------------------------------------
243
+ # Cloud provider detection
244
+ # ------------------------------------------------------------------
245
+
246
+ def _detect_cloud_providers(
247
+ self, root: Path, warnings: List[str]
248
+ ) -> List[Dict[str, Any]]:
249
+ """Detect cloud providers from Terraform, CLI configs, and env vars."""
250
+ providers: Dict[str, Dict[str, Any]] = {}
251
+
252
+ # 1. Terraform provider blocks
253
+ self._detect_providers_from_terraform(root, providers, warnings)
254
+
255
+ # 2. CLI configs
256
+ self._detect_providers_from_cli_configs(providers)
257
+
258
+ # 3. Environment variables (non-secret only)
259
+ self._detect_providers_from_env_vars(providers)
260
+
261
+ return list(providers.values())
262
+
263
+ def _detect_providers_from_terraform(
264
+ self,
265
+ root: Path,
266
+ providers: Dict[str, Dict[str, Any]],
267
+ warnings: List[str],
268
+ ) -> None:
269
+ """Scan .tf files for provider blocks."""
270
+ for tf_file in walk_project(root, [".tf"]):
271
+ try:
272
+ content = tf_file.read_text(encoding="utf-8", errors="replace")
273
+ for cloud_name, pattern in _TF_PROVIDER_PATTERNS.items():
274
+ if re.search(pattern, content):
275
+ if cloud_name not in providers:
276
+ providers[cloud_name] = {
277
+ "name": cloud_name,
278
+ "detected_by": "terraform_provider",
279
+ }
280
+ except OSError as exc:
281
+ warnings.append(f"Could not read {tf_file}: {exc}")
282
+
283
+ def _detect_providers_from_cli_configs(
284
+ self, providers: Dict[str, Dict[str, Any]]
285
+ ) -> None:
286
+ """Check for cloud CLI config files."""
287
+ home = Path.home()
288
+
289
+ # GCP: gcloud CLI config
290
+ gcloud_config = home / ".config" / "gcloud" / "properties"
291
+ if gcloud_config.is_file() and "gcp" not in providers:
292
+ providers["gcp"] = {
293
+ "name": "gcp",
294
+ "detected_by": "cli_config",
295
+ }
296
+
297
+ # AWS: aws CLI config
298
+ aws_config = home / ".aws" / "config"
299
+ if aws_config.is_file() and "aws" not in providers:
300
+ providers["aws"] = {
301
+ "name": "aws",
302
+ "detected_by": "cli_config",
303
+ }
304
+
305
+ # Azure: az CLI config
306
+ azure_config = home / ".azure" / "azureProfile.json"
307
+ if azure_config.is_file() and "azure" not in providers:
308
+ providers["azure"] = {
309
+ "name": "azure",
310
+ "detected_by": "cli_config",
311
+ }
312
+
313
+ def _detect_providers_from_env_vars(
314
+ self, providers: Dict[str, Dict[str, Any]]
315
+ ) -> None:
316
+ """Detect cloud providers from non-secret environment variables."""
317
+ for cloud_name, env_vars in _CLOUD_ENV_VARS.items():
318
+ if cloud_name in providers:
319
+ continue
320
+ for var in env_vars:
321
+ if os.environ.get(var):
322
+ providers[cloud_name] = {
323
+ "name": cloud_name,
324
+ "detected_by": "env_var",
325
+ }
326
+ break
327
+
328
+ # ------------------------------------------------------------------
329
+ # IaC tool detection
330
+ # ------------------------------------------------------------------
331
+
332
+ def _detect_iac(
333
+ self, root: Path, warnings: List[str]
334
+ ) -> List[Dict[str, Any]]:
335
+ """Detect IaC tools from file presence.
336
+
337
+ Groups detected files into distinct IaC roots. For example, if both
338
+ ``terraform/`` (shared modules) and ``features/infra/`` contain .tf
339
+ files, they become separate entries. Validates that all reported file
340
+ paths actually exist on disk (Fix 3: ghost references).
341
+ """
342
+ results: List[Dict[str, Any]] = []
343
+
344
+ for marker in _IAC_MARKERS:
345
+ try:
346
+ rglob_pattern = marker["rglob"]
347
+ if rglob_pattern.startswith("*."):
348
+ ext = rglob_pattern[1:]
349
+ found_files = sorted(walk_project(root, [ext]))
350
+ else:
351
+ found_files = sorted(walk_project_named(root, [rglob_pattern]))
352
+
353
+ if not found_files:
354
+ continue
355
+
356
+ # Fix 3: filter out files whose paths no longer exist
357
+ found_files = [f for f in found_files if f.exists()]
358
+
359
+ if not found_files:
360
+ continue
361
+
362
+ relative_files = [
363
+ str(f.relative_to(root)) for f in found_files
364
+ ]
365
+
366
+ # Group files into distinct IaC roots. A "root" is the
367
+ # top-level directory under `root` that contains the file
368
+ # (depth-1 or depth-2 for monorepo subdirs).
369
+ iac_roots = self._group_iac_roots(root, found_files)
370
+
371
+ for iac_root_path, root_files in sorted(iac_roots.items()):
372
+ root_relative_files = [
373
+ str(f.relative_to(root)) for f in root_files[:10]
374
+ ]
375
+ results.append(
376
+ {
377
+ "tool": marker["tool"],
378
+ "base_path": iac_root_path,
379
+ "detected_files": root_relative_files,
380
+ }
381
+ )
382
+ except OSError as exc:
383
+ warnings.append(f"IaC detection error for {marker['tool']}: {exc}")
384
+
385
+ return results
386
+
387
+ @staticmethod
388
+ def _group_iac_roots(
389
+ root: Path, files: List[Path]
390
+ ) -> Dict[str, List[Path]]:
391
+ """Group IaC files by their top-level infrastructure root directory.
392
+
393
+ Identifies distinct IaC roots by looking for well-known directory
394
+ names (``terraform/``, ``infra/``) in the file path. Files that
395
+ share a common IaC root are grouped together.
396
+ """
397
+ # Known IaC root directory names
398
+ iac_dir_names = {"terraform", "terragrunt", "infra", "infrastructure", "iac"}
399
+
400
+ groups: Dict[str, List[Path]] = {}
401
+
402
+ for f in files:
403
+ rel = f.relative_to(root)
404
+ parts = rel.parts
405
+
406
+ # Find the deepest IaC-root-named directory in the path
407
+ iac_root = None
408
+ for i, part in enumerate(parts[:-1]): # exclude filename
409
+ if part.lower() in iac_dir_names:
410
+ # Use path up to and including this directory
411
+ iac_root = str(Path(*parts[: i + 1]))
412
+ break
413
+
414
+ if iac_root is None:
415
+ # No known IaC directory found; use common base path logic
416
+ iac_root = str(rel.parent) if len(parts) > 1 else "."
417
+
418
+ if iac_root not in groups:
419
+ groups[iac_root] = []
420
+ groups[iac_root].append(f)
421
+
422
+ return groups
423
+
424
+ # ------------------------------------------------------------------
425
+ # Container detection
426
+ # ------------------------------------------------------------------
427
+
428
+ def _detect_containers(
429
+ self, root: Path, warnings: List[str]
430
+ ) -> List[Dict[str, Any]]:
431
+ """Detect container tooling from file presence.
432
+
433
+ Validates that all reported paths actually exist (Fix 3: ghost refs).
434
+ """
435
+ results: List[Dict[str, Any]] = []
436
+
437
+ for container_def in _CONTAINER_GLOBS:
438
+ found: List[str] = []
439
+ try:
440
+ # Separate exact names from prefix patterns (e.g., "Dockerfile.*")
441
+ exact_names = []
442
+ prefixes = []
443
+ for pattern in container_def["rglob_patterns"]:
444
+ if "*" in pattern:
445
+ # "Dockerfile.*" -> prefix "Dockerfile."
446
+ prefixes.append(pattern.replace("*", ""))
447
+ else:
448
+ exact_names.append(pattern)
449
+
450
+ for match in walk_project_named(root, exact_names):
451
+ if match.exists():
452
+ found.append(str(match.relative_to(root)))
453
+
454
+ # Handle prefix patterns (e.g., "Dockerfile.*") via walk
455
+ if prefixes:
456
+ from tools.scan.walk import walk_project_prefix
457
+ for match in walk_project_prefix(root, prefixes):
458
+ if match.exists():
459
+ found.append(str(match.relative_to(root)))
460
+ except OSError as exc:
461
+ warnings.append(
462
+ f"Container detection error for {container_def['tool']}: {exc}"
463
+ )
464
+
465
+ if found:
466
+ results.append(
467
+ {
468
+ "tool": container_def["tool"],
469
+ "files": sorted(set(found)),
470
+ }
471
+ )
472
+
473
+ return results
474
+
475
+ # ------------------------------------------------------------------
476
+ # CI/CD detection
477
+ # ------------------------------------------------------------------
478
+
479
+ def _detect_cicd(
480
+ self, root: Path, warnings: List[str]
481
+ ) -> List[Dict[str, Any]]:
482
+ """Detect CI/CD platforms from config files/directories.
483
+
484
+ Checks root-level marker files first, then scans subdirectories
485
+ for CI/CD configuration files and manifests (e.g., gitlab-runner
486
+ kustomize files).
487
+ """
488
+ results: List[Dict[str, Any]] = []
489
+ detected_platforms: set = set()
490
+
491
+ # Check root-level markers
492
+ for marker in _CICD_MARKERS:
493
+ target = root / marker["path"]
494
+ detected = False
495
+
496
+ if marker["type"] == "dir":
497
+ detected = target.is_dir()
498
+ elif marker["type"] == "file":
499
+ detected = target.is_file()
500
+
501
+ if detected:
502
+ entry: Dict[str, Any] = {
503
+ "platform": marker["platform"],
504
+ "config_path": marker["path"],
505
+ }
506
+ # Enrich GitLab CI entries with related files and stages
507
+ if marker["platform"] == "gitlab-ci":
508
+ self._enrich_gitlab_ci(root, entry, warnings)
509
+ results.append(entry)
510
+ detected_platforms.add(marker["platform"])
511
+
512
+ # Check subdirectories for CI/CD config files (handles monorepo
513
+ # and nested project structures)
514
+ if not results:
515
+ self._detect_cicd_in_subdirs(root, results, detected_platforms, warnings)
516
+
517
+ # Detect CI/CD from manifest content (e.g., gitlab-runner in
518
+ # kustomize manifests)
519
+ if "gitlab-ci" not in detected_platforms:
520
+ self._detect_cicd_from_manifests(root, results, detected_platforms, warnings)
521
+
522
+ return results
523
+
524
+ def _detect_cicd_in_subdirs(
525
+ self,
526
+ root: Path,
527
+ results: List[Dict[str, Any]],
528
+ detected_platforms: set,
529
+ warnings: List[str],
530
+ ) -> None:
531
+ """Check immediate subdirectories for CI/CD config files."""
532
+ try:
533
+ for entry in sorted(root.iterdir()):
534
+ if not entry.is_dir() or entry.name.startswith("."):
535
+ continue
536
+ if entry.name in ("node_modules", "vendor", "__pycache__"):
537
+ continue
538
+ for marker in _CICD_MARKERS:
539
+ if marker["platform"] in detected_platforms:
540
+ continue
541
+ target = entry / marker["path"]
542
+ detected = False
543
+ if marker["type"] == "dir":
544
+ detected = target.is_dir()
545
+ elif marker["type"] == "file":
546
+ detected = target.is_file()
547
+ if detected:
548
+ rel_path = str(target.relative_to(root))
549
+ cicd_entry: Dict[str, Any] = {
550
+ "platform": marker["platform"],
551
+ "config_path": rel_path,
552
+ }
553
+ # Enrich GitLab CI entries found in subdirectories
554
+ if marker["platform"] == "gitlab-ci":
555
+ self._enrich_gitlab_ci(entry, cicd_entry, warnings)
556
+ results.append(cicd_entry)
557
+ detected_platforms.add(marker["platform"])
558
+ except OSError as exc:
559
+ warnings.append(f"CI/CD subdirectory scan error: {exc}")
560
+
561
+ def _detect_cicd_from_manifests(
562
+ self,
563
+ root: Path,
564
+ results: List[Dict[str, Any]],
565
+ detected_platforms: set,
566
+ warnings: List[str],
567
+ ) -> None:
568
+ """Detect CI/CD platforms from Kubernetes manifest content.
569
+
570
+ Looks for CI/CD-related resources like gitlab-runner deployments
571
+ in kustomize/Kubernetes manifests.
572
+ """
573
+ # CI/CD-related directory or file name patterns
574
+ cicd_manifest_indicators = {
575
+ "gitlab-runner": "gitlab-ci",
576
+ "github-actions-runner": "github-actions",
577
+ "jenkins": "jenkins",
578
+ }
579
+
580
+ try:
581
+ for dirpath, dirnames, filenames in os.walk(str(root)):
582
+ dirnames[:] = [
583
+ d for d in dirnames
584
+ if d not in ("node_modules", ".git", "__pycache__",
585
+ ".terraform", "vendor", "dist", "build",
586
+ ".venv", "venv")
587
+ and not d.startswith(".")
588
+ ]
589
+ dir_name = os.path.basename(dirpath).lower()
590
+ for indicator, platform in cicd_manifest_indicators.items():
591
+ if platform in detected_platforms:
592
+ continue
593
+ if indicator in dir_name:
594
+ rel_path = str(Path(dirpath).relative_to(root))
595
+ results.append(
596
+ {
597
+ "platform": platform,
598
+ "config_path": rel_path,
599
+ }
600
+ )
601
+ detected_platforms.add(platform)
602
+ except OSError as exc:
603
+ warnings.append(f"CI/CD manifest scan error: {exc}")
604
+
605
+ # ------------------------------------------------------------------
606
+ # GitLab CI enrichment
607
+ # ------------------------------------------------------------------
608
+
609
+ def _enrich_gitlab_ci(
610
+ self,
611
+ root: Path,
612
+ entry: Dict[str, Any],
613
+ warnings: List[str],
614
+ ) -> None:
615
+ """Enrich a GitLab CI entry with related files and stage names.
616
+
617
+ Looks for:
618
+ - .gitlab/ci/ directory (reusable CI components/templates)
619
+ - Additional CI yml files (e.g., .gitlab-ci-builder.yml)
620
+ - .ci-local/ directory (local CI testing)
621
+ - Stage names extracted from the main .gitlab-ci.yml
622
+ """
623
+ related_files: List[str] = []
624
+
625
+ try:
626
+ # Check for .gitlab/ci/ directory
627
+ gitlab_ci_dir = root / ".gitlab" / "ci"
628
+ if gitlab_ci_dir.is_dir():
629
+ related_files.append(".gitlab/ci/")
630
+
631
+ # Check for additional CI yml files at root
632
+ for candidate in sorted(root.iterdir()):
633
+ if not candidate.is_file():
634
+ continue
635
+ name = candidate.name
636
+ if (
637
+ name != ".gitlab-ci.yml"
638
+ and name.startswith(".gitlab-ci")
639
+ and name.endswith((".yml", ".yaml"))
640
+ ):
641
+ related_files.append(name)
642
+
643
+ # Check for .ci-local/ directory
644
+ ci_local_dir = root / ".ci-local"
645
+ if ci_local_dir.is_dir():
646
+ related_files.append(".ci-local/")
647
+
648
+ except OSError as exc:
649
+ warnings.append(f"GitLab CI enrichment error (related files): {exc}")
650
+
651
+ if related_files:
652
+ entry["related_files"] = related_files
653
+
654
+ # Extract stage names from the main .gitlab-ci.yml
655
+ try:
656
+ ci_file = root / ".gitlab-ci.yml"
657
+ if ci_file.is_file():
658
+ content = ci_file.read_text(encoding="utf-8", errors="replace")
659
+ stages = self._extract_yaml_stages(content)
660
+ if stages:
661
+ entry["stages"] = stages
662
+ except OSError as exc:
663
+ warnings.append(f"GitLab CI enrichment error (stages): {exc}")
664
+
665
+ @staticmethod
666
+ def _extract_yaml_stages(content: str) -> List[str]:
667
+ """Extract stage names from a YAML string using simple line parsing.
668
+
669
+ Looks for a top-level ``stages:`` key followed by ``- name`` lines.
670
+ Handles the common format without requiring a YAML library.
671
+ """
672
+ stages: List[str] = []
673
+ in_stages = False
674
+
675
+ for line in content.splitlines():
676
+ stripped = line.strip()
677
+
678
+ # Detect the start of the stages block (must be top-level, no leading spaces)
679
+ if line.startswith("stages:") and not line[0].isspace():
680
+ in_stages = True
681
+ continue
682
+
683
+ if in_stages:
684
+ # A list item under stages
685
+ if stripped.startswith("- "):
686
+ stage_name = stripped[2:].strip().strip("'\"")
687
+ if stage_name:
688
+ stages.append(stage_name)
689
+ elif stripped == "" or stripped.startswith("#"):
690
+ # Blank lines and comments are OK inside the block
691
+ continue
692
+ else:
693
+ # Any other non-list content ends the stages block
694
+ break
695
+
696
+ return stages
697
+
698
+ # ------------------------------------------------------------------
699
+ # Infrastructure path detection
700
+ # ------------------------------------------------------------------
701
+
702
+ def _detect_paths(
703
+ self, root: Path, warnings: List[str]
704
+ ) -> Dict[str, Optional[str]]:
705
+ """Detect infrastructure-related directories.
706
+
707
+ Searches depth=1 and depth=2 to handle monorepo structures where
708
+ infra directories live inside a workspace subdirectory (e.g.,
709
+ ``qxo-monorepo/terraform/``).
710
+ """
711
+ detected: Dict[str, Optional[str]] = {
712
+ "gitops": None,
713
+ "terraform": None,
714
+ "app_services": None,
715
+ }
716
+
717
+ skip = {"node_modules", ".git", "__pycache__", ".terraform", "vendor",
718
+ "dist", "build", ".venv", "venv"}
719
+
720
+ try:
721
+ # Depth=1: immediate subdirectories of root
722
+ subdirs = [
723
+ d for d in root.iterdir()
724
+ if d.is_dir() and not d.name.startswith(".") and d.name not in skip
725
+ ]
726
+ except OSError as exc:
727
+ warnings.append(f"Path detection error: {exc}")
728
+ return detected
729
+
730
+ for subdir in subdirs:
731
+ dir_name = subdir.name.lower()
732
+ for path_key, candidates in _INFRA_DIR_NAMES.items():
733
+ if detected[path_key] is None and dir_name in candidates:
734
+ detected[path_key] = str(subdir.relative_to(root))
735
+
736
+ # Depth=2: check inside each depth-1 subdirectory for infra dirs
737
+ # This handles monorepo layouts like qxo-monorepo/terraform/
738
+ for subdir in subdirs:
739
+ try:
740
+ for child in subdir.iterdir():
741
+ if not child.is_dir() or child.name.startswith("."):
742
+ continue
743
+ if child.name in skip:
744
+ continue
745
+ child_name = child.name.lower()
746
+ for path_key, candidates in _INFRA_DIR_NAMES.items():
747
+ if detected[path_key] is None and child_name in candidates:
748
+ detected[path_key] = str(child.relative_to(root))
749
+ except OSError:
750
+ continue
751
+
752
+ return detected
753
+
754
+ # ------------------------------------------------------------------
755
+ # Application service detection
756
+ # ------------------------------------------------------------------
757
+
758
+ def _detect_application_services(
759
+ self, root: Path, warnings: List[str]
760
+ ) -> List[Dict[str, Any]]:
761
+ """Detect microservices from directory conventions.
762
+
763
+ Scans for directories containing ``service-runtime/`` subdirs or
764
+ ``Dockerfile`` files, following common monorepo patterns like
765
+ ``features/*-feature/*-service/service-runtime/``.
766
+
767
+ Returns a list of service descriptors (scanner-owned fields only).
768
+ """
769
+ services: List[Dict[str, Any]] = []
770
+ seen_names: Set[str] = set()
771
+
772
+ skip = {"node_modules", ".git", "__pycache__", ".terraform",
773
+ ".terragrunt-cache", "vendor", "dist", "build",
774
+ ".venv", "venv", "charts", "infra"}
775
+
776
+ # Search up to depth=4 for service-runtime dirs and Dockerfiles
777
+ # that indicate a service boundary
778
+ self._find_services_recursive(
779
+ root, root, services, seen_names, skip, warnings, depth=0, max_depth=4
780
+ )
781
+
782
+ return services
783
+
784
+ def _find_services_recursive(
785
+ self,
786
+ root: Path,
787
+ current: Path,
788
+ services: List[Dict[str, Any]],
789
+ seen_names: Set[str],
790
+ skip: Set[str],
791
+ warnings: List[str],
792
+ depth: int,
793
+ max_depth: int,
794
+ ) -> None:
795
+ """Recursively find service directories."""
796
+ if depth >= max_depth:
797
+ return
798
+
799
+ try:
800
+ entries = sorted(current.iterdir())
801
+ except OSError:
802
+ return
803
+
804
+ for entry in entries:
805
+ if not entry.is_dir() or entry.name.startswith("."):
806
+ continue
807
+ if entry.name in skip:
808
+ continue
809
+
810
+ # Check if this directory looks like a service
811
+ has_service_runtime = (entry / "service-runtime").is_dir()
812
+ has_dockerfile = (entry / "Dockerfile").is_file()
813
+ has_docker_compose = (
814
+ (entry / "docker-compose.yml").is_file()
815
+ or (entry / "docker-compose.yaml").is_file()
816
+ )
817
+
818
+ if has_service_runtime or has_dockerfile:
819
+ service_name = entry.name
820
+ if service_name not in seen_names:
821
+ seen_names.add(service_name)
822
+ services.append({
823
+ "name": service_name,
824
+ "path": str(entry.relative_to(root)),
825
+ "has_dockerfile": has_dockerfile,
826
+ "has_docker_compose": has_docker_compose,
827
+ "has_service_runtime": has_service_runtime,
828
+ })
829
+
830
+ # Continue recursing into subdirectories
831
+ self._find_services_recursive(
832
+ root, entry, services, seen_names, skip, warnings,
833
+ depth + 1, max_depth,
834
+ )
835
+
836
+ # ------------------------------------------------------------------
837
+ # Helpers
838
+ # ------------------------------------------------------------------
839
+
840
+ @staticmethod
841
+ def _common_base_path(paths: List[str]) -> str:
842
+ """Find the common base directory from a list of relative paths.
843
+
844
+ Returns '.' if paths are in the root directory.
845
+ """
846
+ if not paths:
847
+ return "."
848
+
849
+ parts_list = [Path(p).parent.parts for p in paths]
850
+ if not parts_list:
851
+ return "."
852
+
853
+ common: List[str] = []
854
+ for segments in zip(*parts_list):
855
+ if len(set(segments)) == 1:
856
+ common.append(segments[0])
857
+ else:
858
+ break
859
+
860
+ return str(Path(*common)) if common else "."
861
+
862
+
863
+ # Module-level convenience for verify commands
864
+ def scan(root: Path) -> Dict[str, Any]:
865
+ """Module-level convenience function for infrastructure scanning.
866
+
867
+ Args:
868
+ root: Absolute path to the project root directory.
869
+
870
+ Returns:
871
+ Dict mapping section names to section data.
872
+ """
873
+ scanner = InfrastructureScanner()
874
+ result = scanner.scan(root)
875
+ return result.sections