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