@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,600 @@
1
+ """
2
+ Orchestration Scanner
3
+
4
+ Detects Kubernetes, GitOps, Helm, Kustomize, and service mesh indicators
5
+ from project filesystem. Only produces output when orchestration tooling
6
+ is detected -- returns empty dict for projects with no orchestration files.
7
+
8
+ Contract: specs/002-gaia-scan/data-model.md section 2.8
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from tools.scan.scanners.base import BaseScanner, ScanResult
18
+ from tools.scan.walk import walk_project, walk_project_named
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Kubernetes manifest kinds that indicate orchestration
23
+ _K8S_KINDS = frozenset({
24
+ "Deployment",
25
+ "Service",
26
+ "StatefulSet",
27
+ "DaemonSet",
28
+ "Job",
29
+ "CronJob",
30
+ "Ingress",
31
+ "ConfigMap",
32
+ "Secret",
33
+ "HelmRelease",
34
+ "Kustomization",
35
+ "Pod",
36
+ "ReplicaSet",
37
+ "Namespace",
38
+ "ServiceAccount",
39
+ "ClusterRole",
40
+ "ClusterRoleBinding",
41
+ "Role",
42
+ "RoleBinding",
43
+ "PersistentVolumeClaim",
44
+ "PersistentVolume",
45
+ "NetworkPolicy",
46
+ })
47
+
48
+ # GitOps API groups
49
+ _FLUX_API_GROUPS = frozenset({
50
+ "toolkit.fluxcd.io",
51
+ "source.toolkit.fluxcd.io",
52
+ "kustomize.toolkit.fluxcd.io",
53
+ "helm.toolkit.fluxcd.io",
54
+ "notification.toolkit.fluxcd.io",
55
+ "image.toolkit.fluxcd.io",
56
+ })
57
+
58
+ _ARGOCD_API_GROUPS = frozenset({
59
+ "argoproj.io",
60
+ })
61
+
62
+ # Flux directory conventions
63
+ _FLUX_DIR_CONVENTIONS = frozenset({
64
+ "clusters",
65
+ "infrastructure",
66
+ "apps",
67
+ })
68
+
69
+ # Service mesh annotation prefixes
70
+ _ISTIO_INDICATORS = frozenset({
71
+ "sidecar.istio.io",
72
+ "istio.io",
73
+ "networking.istio.io",
74
+ "security.istio.io",
75
+ })
76
+
77
+ _LINKERD_INDICATORS = frozenset({
78
+ "linkerd.io",
79
+ "viz.linkerd.io",
80
+ "config.linkerd.io",
81
+ })
82
+
83
+ _CONSUL_INDICATORS = frozenset({
84
+ "consul.hashicorp.com",
85
+ "connect-inject",
86
+ })
87
+
88
+ # Maximum number of YAML files to scan to stay within performance budget
89
+ _MAX_YAML_FILES = 500
90
+
91
+ # Maximum file size (bytes) to read for YAML scanning
92
+ _MAX_YAML_SIZE = 256 * 1024 # 256 KB
93
+
94
+
95
+ class OrchestrationScanner(BaseScanner):
96
+ """Scanner for Kubernetes, GitOps, Helm, Kustomize, and service mesh.
97
+
98
+ Only creates the 'orchestration' section when indicators are found.
99
+ Returns empty dict for projects with no orchestration tooling.
100
+
101
+ Pure Function Contract:
102
+ - No file writes
103
+ - No state modification
104
+ - No network calls
105
+ - No command execution
106
+ - Only filesystem reads
107
+ """
108
+
109
+ @property
110
+ def SCANNER_NAME(self) -> str:
111
+ return "orchestration"
112
+
113
+ @property
114
+ def SCANNER_VERSION(self) -> str:
115
+ return "1.0.0"
116
+
117
+ @property
118
+ def OWNED_SECTIONS(self) -> List[str]:
119
+ return ["orchestration"]
120
+
121
+ def scan(self, root: Path) -> ScanResult:
122
+ """Scan the project for orchestration indicators.
123
+
124
+ Args:
125
+ root: Absolute path to the project root directory.
126
+
127
+ Returns:
128
+ ScanResult with 'orchestration' section if indicators found,
129
+ or ScanResult with empty sections if nothing detected.
130
+ """
131
+ start_ms = time.monotonic() * 1000
132
+ warnings: List[str] = []
133
+
134
+ try:
135
+ kubernetes = self._detect_kubernetes(root, warnings)
136
+ gitops = self._detect_gitops(root, warnings)
137
+ helm = self._detect_helm(root, warnings)
138
+ kustomize = self._detect_kustomize(root, warnings)
139
+ service_mesh = self._detect_service_mesh(root, warnings)
140
+
141
+ # Only produce section when at least one indicator is found
142
+ has_indicators = (
143
+ kubernetes["detected"]
144
+ or gitops["tool"] is not None
145
+ or helm["detected"]
146
+ or kustomize["detected"]
147
+ or service_mesh["tool"] is not None
148
+ )
149
+
150
+ if not has_indicators:
151
+ elapsed = (time.monotonic() * 1000) - start_ms
152
+ return self.make_result(
153
+ sections={},
154
+ warnings=warnings,
155
+ duration_ms=elapsed,
156
+ )
157
+
158
+ orchestration_data: Dict[str, Any] = {
159
+ "kubernetes": kubernetes,
160
+ "gitops": gitops,
161
+ "helm": helm,
162
+ "kustomize": kustomize,
163
+ "service_mesh": service_mesh,
164
+ }
165
+
166
+ elapsed = (time.monotonic() * 1000) - start_ms
167
+ return self.make_result(
168
+ sections={"orchestration": orchestration_data},
169
+ warnings=warnings,
170
+ duration_ms=elapsed,
171
+ )
172
+
173
+ except Exception as exc:
174
+ elapsed = (time.monotonic() * 1000) - start_ms
175
+ logger.warning("Orchestration scanner failed: %s", exc)
176
+ return self.make_result(
177
+ sections={},
178
+ warnings=[f"Orchestration scanner error: {exc}"],
179
+ duration_ms=elapsed,
180
+ )
181
+
182
+ # ------------------------------------------------------------------ #
183
+ # Kubernetes Detection
184
+ # ------------------------------------------------------------------ #
185
+
186
+ def _detect_kubernetes(
187
+ self, root: Path, warnings: List[str]
188
+ ) -> Dict[str, Any]:
189
+ """Detect Kubernetes indicators from manifests, kubeconfig, etc."""
190
+ indicators: List[str] = []
191
+ manifest_patterns: List[str] = []
192
+
193
+ # Check YAML manifests for Kubernetes kinds
194
+ yaml_kinds = self._scan_yaml_for_kinds(root, warnings)
195
+ if yaml_kinds:
196
+ indicators.append("kubernetes manifests found")
197
+ manifest_patterns.extend(sorted(yaml_kinds))
198
+
199
+ # Check kubeconfig
200
+ kubeconfig_path = self._find_kubeconfig()
201
+ if kubeconfig_path:
202
+ indicators.append(f"kubeconfig: {kubeconfig_path}")
203
+
204
+ return {
205
+ "detected": len(indicators) > 0,
206
+ "indicators": indicators,
207
+ "manifest_patterns": manifest_patterns,
208
+ }
209
+
210
+ def _scan_yaml_for_kinds(
211
+ self, root: Path, warnings: List[str]
212
+ ) -> List[str]:
213
+ """Scan YAML files for Kubernetes resource kinds."""
214
+ kinds_found: set = set()
215
+ yaml_files = self._find_yaml_files(root)
216
+
217
+ for yaml_path in yaml_files:
218
+ try:
219
+ content = self._safe_read(yaml_path)
220
+ if content is None:
221
+ continue
222
+
223
+ for line in content.splitlines():
224
+ stripped = line.strip()
225
+ if stripped.startswith("kind:"):
226
+ kind_value = stripped[5:].strip().strip('"').strip("'")
227
+ if kind_value in _K8S_KINDS:
228
+ kinds_found.add(kind_value)
229
+ except Exception:
230
+ # Individual file read failures must not abort the scanner
231
+ continue
232
+
233
+ return sorted(kinds_found)
234
+
235
+ def _find_kubeconfig(self) -> Optional[str]:
236
+ """Check for kubeconfig presence via env var or default path."""
237
+ # Check KUBECONFIG env var
238
+ kubeconfig_env = os.environ.get("KUBECONFIG")
239
+ if kubeconfig_env:
240
+ for kc_path in kubeconfig_env.split(os.pathsep):
241
+ if Path(kc_path).is_file():
242
+ return kc_path
243
+
244
+ # Check default location
245
+ default_kc = Path.home() / ".kube" / "config"
246
+ if default_kc.is_file():
247
+ return str(default_kc)
248
+
249
+ return None
250
+
251
+ # ------------------------------------------------------------------ #
252
+ # GitOps Detection
253
+ # ------------------------------------------------------------------ #
254
+
255
+ def _detect_gitops(
256
+ self, root: Path, warnings: List[str]
257
+ ) -> Dict[str, Any]:
258
+ """Detect Flux, ArgoCD, or other GitOps tooling."""
259
+ api_groups: List[str] = []
260
+ tool: Optional[str] = None
261
+ config_path: Optional[str] = None
262
+
263
+ # Scan YAML files for API groups
264
+ flux_groups, argocd_groups = self._scan_yaml_for_api_groups(
265
+ root, warnings
266
+ )
267
+
268
+ if flux_groups:
269
+ tool = "flux"
270
+ api_groups.extend(sorted(flux_groups))
271
+ # Look for Flux config path
272
+ config_path = self._find_flux_config_path(root)
273
+ elif argocd_groups:
274
+ tool = "argocd"
275
+ api_groups.extend(sorted(argocd_groups))
276
+ # Look for ArgoCD config path
277
+ config_path = self._find_argocd_config_path(root)
278
+
279
+ # Check Flux directory conventions if no API groups found
280
+ if tool is None:
281
+ flux_dir = self._check_flux_directory_conventions(root)
282
+ if flux_dir:
283
+ tool = "flux"
284
+ config_path = flux_dir
285
+
286
+ # If tool is detected but config_path is still None, derive from
287
+ # kustomize file paths (e.g., qxo-monorepo/gitops/base/kustomization.yaml)
288
+ if tool is not None and config_path is None:
289
+ config_path = self._derive_config_path_from_kustomize(root)
290
+
291
+ return {
292
+ "tool": tool,
293
+ "api_groups": api_groups,
294
+ "config_path": config_path,
295
+ }
296
+
297
+ def _scan_yaml_for_api_groups(
298
+ self, root: Path, warnings: List[str]
299
+ ) -> tuple:
300
+ """Scan YAML files for Flux and ArgoCD API groups.
301
+
302
+ Returns:
303
+ Tuple of (flux_groups, argocd_groups) sets.
304
+ """
305
+ flux_groups: set = set()
306
+ argocd_groups: set = set()
307
+
308
+ yaml_files = self._find_yaml_files(root)
309
+
310
+ for yaml_path in yaml_files:
311
+ try:
312
+ content = self._safe_read(yaml_path)
313
+ if content is None:
314
+ continue
315
+
316
+ for line in content.splitlines():
317
+ stripped = line.strip()
318
+
319
+ # Check apiVersion lines for Flux
320
+ for fg in _FLUX_API_GROUPS:
321
+ if fg in stripped:
322
+ flux_groups.add(fg)
323
+
324
+ # Check apiVersion lines for ArgoCD
325
+ for ag in _ARGOCD_API_GROUPS:
326
+ if ag in stripped:
327
+ argocd_groups.add(ag)
328
+ except Exception:
329
+ continue
330
+
331
+ return flux_groups, argocd_groups
332
+
333
+ def _find_flux_config_path(self, root: Path) -> Optional[str]:
334
+ """Find the Flux configuration root directory.
335
+
336
+ Checks root-level directories first, then searches subdirectories
337
+ for gitops-related directory names.
338
+ """
339
+ candidates = ["clusters", "flux-system", "gitops"]
340
+
341
+ # Check root-level directories
342
+ for candidate in candidates:
343
+ candidate_path = root / candidate
344
+ if candidate_path.is_dir():
345
+ return candidate
346
+
347
+ # Check one level deeper (e.g., monorepo-root/gitops/)
348
+ try:
349
+ for entry in sorted(root.iterdir()):
350
+ if not entry.is_dir() or entry.name.startswith("."):
351
+ continue
352
+ if entry.name in ("node_modules", "vendor", "__pycache__"):
353
+ continue
354
+ for candidate in candidates:
355
+ nested = entry / candidate
356
+ if nested.is_dir():
357
+ return str(nested.relative_to(root))
358
+ except OSError:
359
+ pass
360
+
361
+ return None
362
+
363
+ def _find_argocd_config_path(self, root: Path) -> Optional[str]:
364
+ """Find the ArgoCD configuration root directory."""
365
+ candidates = ["argocd", "argo-cd", "applications"]
366
+ for candidate in candidates:
367
+ candidate_path = root / candidate
368
+ if candidate_path.is_dir():
369
+ return candidate
370
+ return None
371
+
372
+ def _check_flux_directory_conventions(
373
+ self, root: Path
374
+ ) -> Optional[str]:
375
+ """Check for Flux directory conventions (clusters/, infrastructure/, apps/)."""
376
+ matched_dirs = []
377
+ for dirname in _FLUX_DIR_CONVENTIONS:
378
+ if (root / dirname).is_dir():
379
+ matched_dirs.append(dirname)
380
+
381
+ # Require at least 2 of the 3 convention directories
382
+ if len(matched_dirs) >= 2:
383
+ return "clusters" if (root / "clusters").is_dir() else matched_dirs[0]
384
+
385
+ return None
386
+
387
+ def _derive_config_path_from_kustomize(self, root: Path) -> Optional[str]:
388
+ """Derive gitops config_path from detected kustomize file locations.
389
+
390
+ Finds kustomization.yaml files and returns the highest common
391
+ gitops-related ancestor directory.
392
+ """
393
+ gitops_dirs: List[str] = []
394
+
395
+ try:
396
+ for kust_file in walk_project_named(
397
+ root, ["kustomization.yaml", "kustomization.yml", "Kustomization"]
398
+ ):
399
+ rel = kust_file.relative_to(root)
400
+ # Walk up the path parts looking for a gitops-related directory name
401
+ parts = rel.parent.parts
402
+ for i, part in enumerate(parts):
403
+ if part.lower() in ("gitops", "flux", "argocd", "deploy", "k8s"):
404
+ gitops_path = str(Path(*parts[: i + 1]))
405
+ if gitops_path not in gitops_dirs:
406
+ gitops_dirs.append(gitops_path)
407
+ break
408
+ except Exception:
409
+ pass
410
+
411
+ if gitops_dirs:
412
+ return gitops_dirs[0]
413
+ return None
414
+
415
+ # ------------------------------------------------------------------ #
416
+ # Helm Detection
417
+ # ------------------------------------------------------------------ #
418
+
419
+ def _detect_helm(
420
+ self, root: Path, warnings: List[str]
421
+ ) -> Dict[str, Any]:
422
+ """Detect Helm charts from Chart.yaml files."""
423
+ charts: List[str] = []
424
+
425
+ try:
426
+ for chart_yaml in walk_project_named(root, ["Chart.yaml"]):
427
+ rel_path = str(chart_yaml.relative_to(root))
428
+ charts.append(rel_path)
429
+ except Exception:
430
+ warnings.append("Failed to scan for Helm Chart.yaml files")
431
+
432
+ return {
433
+ "detected": len(charts) > 0,
434
+ "charts": sorted(charts),
435
+ }
436
+
437
+ # ------------------------------------------------------------------ #
438
+ # Kustomize Detection
439
+ # ------------------------------------------------------------------ #
440
+
441
+ def _detect_kustomize(
442
+ self, root: Path, warnings: List[str]
443
+ ) -> Dict[str, Any]:
444
+ """Detect Kustomize from kustomization.yaml files."""
445
+ files: List[str] = []
446
+
447
+ try:
448
+ for kust_file in walk_project_named(root, ["kustomization.yaml", "kustomization.yml", "Kustomization"]):
449
+ rel_path = str(kust_file.relative_to(root))
450
+ if rel_path not in files:
451
+ files.append(rel_path)
452
+ except Exception:
453
+ warnings.append("Failed to scan for kustomization.yaml files")
454
+
455
+ return {
456
+ "detected": len(files) > 0,
457
+ "files": sorted(files),
458
+ }
459
+
460
+ # ------------------------------------------------------------------ #
461
+ # Service Mesh Detection
462
+ # ------------------------------------------------------------------ #
463
+
464
+ def _detect_service_mesh(
465
+ self, root: Path, warnings: List[str]
466
+ ) -> Dict[str, Any]:
467
+ """Detect Istio, Linkerd, or Consul Connect from annotations."""
468
+ tool: Optional[str] = None
469
+ indicators: List[str] = []
470
+
471
+ istio_found = False
472
+ linkerd_found = False
473
+ consul_found = False
474
+
475
+ yaml_files = self._find_yaml_files(root)
476
+
477
+ for yaml_path in yaml_files:
478
+ try:
479
+ content = self._safe_read(yaml_path)
480
+ if content is None:
481
+ continue
482
+
483
+ for line in content.splitlines():
484
+ stripped = line.strip()
485
+
486
+ # Istio
487
+ for prefix in _ISTIO_INDICATORS:
488
+ if prefix in stripped:
489
+ istio_found = True
490
+ indicator = f"istio: {prefix}"
491
+ if indicator not in indicators:
492
+ indicators.append(indicator)
493
+
494
+ # Linkerd
495
+ for prefix in _LINKERD_INDICATORS:
496
+ if prefix in stripped:
497
+ linkerd_found = True
498
+ indicator = f"linkerd: {prefix}"
499
+ if indicator not in indicators:
500
+ indicators.append(indicator)
501
+
502
+ # Consul Connect
503
+ for prefix in _CONSUL_INDICATORS:
504
+ if prefix in stripped:
505
+ consul_found = True
506
+ indicator = f"consul: {prefix}"
507
+ if indicator not in indicators:
508
+ indicators.append(indicator)
509
+ except Exception:
510
+ continue
511
+
512
+ # Determine primary tool (first detected wins)
513
+ if istio_found:
514
+ tool = "istio"
515
+ elif linkerd_found:
516
+ tool = "linkerd"
517
+ elif consul_found:
518
+ tool = "consul"
519
+
520
+ return {
521
+ "tool": tool,
522
+ "indicators": sorted(indicators),
523
+ }
524
+
525
+ # ------------------------------------------------------------------ #
526
+ # Utility Methods
527
+ # ------------------------------------------------------------------ #
528
+
529
+ def _find_yaml_files(self, root: Path) -> List[Path]:
530
+ """Find YAML files in the project, respecting scan limits.
531
+
532
+ Uses walk_project for filtered os.walk (skips node_modules, .git, etc.)
533
+ instead of rglob which traverses all directories.
534
+
535
+ Caches results on the instance for reuse across detection methods
536
+ within the same scan() call.
537
+ """
538
+ cache_attr = "_yaml_files_cache"
539
+ cache_root_attr = "_yaml_files_root"
540
+
541
+ cached_root = getattr(self, cache_root_attr, None)
542
+ if cached_root == root:
543
+ cached = getattr(self, cache_attr, None)
544
+ if cached is not None:
545
+ return cached
546
+
547
+ yaml_files: List[Path] = []
548
+ count = 0
549
+
550
+ try:
551
+ for p in walk_project(root, [".yaml", ".yml"]):
552
+ yaml_files.append(p)
553
+ count += 1
554
+ if count >= _MAX_YAML_FILES:
555
+ break
556
+ except Exception:
557
+ pass
558
+
559
+ # Cache on instance
560
+ object.__setattr__(self, cache_attr, yaml_files)
561
+ object.__setattr__(self, cache_root_attr, root)
562
+
563
+ return yaml_files
564
+
565
+ def _safe_read(self, path: Path) -> Optional[str]:
566
+ """Read a file safely, returning None on failure or if too large."""
567
+ try:
568
+ if not path.is_file():
569
+ return None
570
+ if path.stat().st_size > _MAX_YAML_SIZE:
571
+ return None
572
+ return path.read_text(encoding="utf-8", errors="replace")
573
+ except (OSError, UnicodeDecodeError):
574
+ return None
575
+
576
+ @staticmethod
577
+ def _should_skip_path(path: Path) -> bool:
578
+ """Check if a path should be skipped (hidden dirs, vendor, node_modules)."""
579
+ parts = path.parts
580
+ for part in parts:
581
+ if part.startswith(".") and part not in (".", ".."):
582
+ return True
583
+ if part in ("node_modules", "vendor", "__pycache__", ".git"):
584
+ return True
585
+ return False
586
+
587
+
588
+ # Module-level convenience function (matches task verify pattern)
589
+ def scan(root: Path) -> Dict[str, Any]:
590
+ """Convenience function: instantiate OrchestrationScanner and run scan.
591
+
592
+ Args:
593
+ root: Absolute path to the project root directory.
594
+
595
+ Returns:
596
+ Dict mapping section names to section data (from ScanResult.sections).
597
+ """
598
+ scanner = OrchestrationScanner()
599
+ result = scanner.scan(root)
600
+ return result.sections