@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,549 @@
1
+ """
2
+ Scan Orchestrator
3
+
4
+ Runs all registered scanners in parallel, collects results, combines them
5
+ with existing project-context.json using the merge rules, updates metadata,
6
+ and performs an atomic write.
7
+
8
+ Pipeline:
9
+ 1. Load existing project-context.json (if present)
10
+ 2. Run all scanners in parallel (ThreadPoolExecutor)
11
+ 3. Collect and combine scanner sections (handling environment sub-keys)
12
+ 4. Merge with existing context (section ownership model)
13
+ 5. Update metadata (last_updated, last_scan, scanner_version)
14
+ 6. Atomic write to project-context.json
15
+ 7. Return ScanOutput
16
+
17
+ Contract: specs/002-gaia-scan/data-model.md section 4
18
+ specs/002-gaia-scan/contracts/merge-behavior.md
19
+ """
20
+
21
+ import json
22
+ import logging
23
+ import os
24
+ import time
25
+ from concurrent.futures import ThreadPoolExecutor, as_completed
26
+ from dataclasses import dataclass, field
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+ from typing import Any, Dict, List, Optional
30
+
31
+ from tools.scan import __version__ as scanner_package_version
32
+ from tools.scan.config import CONTRACT_CONFIG_PATH, ScanConfig
33
+ from tools.scan.merge import (
34
+ AGENT_ENRICHED_SECTIONS,
35
+ collect_scanner_sections,
36
+ merge_context,
37
+ )
38
+ from tools.scan.registry import ScannerRegistry
39
+ from tools.scan.scanners.base import BaseScanner, ScanResult
40
+ from tools.scan.workspace import WorkspaceInfo, detect_workspace_type
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class ScanOutput:
47
+ """Aggregated output from all scanners.
48
+
49
+ Attributes:
50
+ context: Full merged project-context data (top-level with metadata,
51
+ paths, and sections).
52
+ sections_updated: Section names that were updated by scanners.
53
+ sections_preserved: Agent-enriched sections left untouched.
54
+ warnings: Aggregated warnings from all scanners.
55
+ errors: Aggregated errors from all scanners.
56
+ duration_ms: Total scan time in milliseconds.
57
+ scanner_results: Per-scanner ScanResult mapping.
58
+ """
59
+
60
+ context: Dict[str, Any] = field(default_factory=dict)
61
+ sections_updated: List[str] = field(default_factory=list)
62
+ sections_preserved: List[str] = field(default_factory=list)
63
+ warnings: List[str] = field(default_factory=list)
64
+ errors: List[str] = field(default_factory=list)
65
+ duration_ms: float = 0.0
66
+ scanner_results: Dict[str, ScanResult] = field(default_factory=dict)
67
+
68
+
69
+ class ScanOrchestrator:
70
+ """Orchestrates parallel scanner execution with fault isolation.
71
+
72
+ Runs all scanners from a ScannerRegistry, collects their results,
73
+ merges sections with existing context, applies backward compatibility,
74
+ and returns a ScanOutput. Individual scanner failures are caught and
75
+ reported without aborting the scan.
76
+
77
+ Args:
78
+ registry: ScannerRegistry with discovered scanners.
79
+ config: ScanConfig with orchestration settings.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ registry: Optional[ScannerRegistry] = None,
85
+ config: Optional[ScanConfig] = None,
86
+ ) -> None:
87
+ self.registry = registry or ScannerRegistry()
88
+ self.config = config or ScanConfig()
89
+
90
+ def _run_scanner(
91
+ self,
92
+ scanner: BaseScanner,
93
+ project_root: Path,
94
+ ) -> ScanResult:
95
+ """Run a single scanner with fault isolation.
96
+
97
+ Args:
98
+ scanner: Scanner instance to execute.
99
+ project_root: Project root path.
100
+
101
+ Returns:
102
+ ScanResult from the scanner, or an error result on failure.
103
+ """
104
+ start_ms = time.monotonic() * 1000
105
+ try:
106
+ result = scanner.scan(project_root)
107
+ return result
108
+ except Exception as exc:
109
+ elapsed_ms = (time.monotonic() * 1000) - start_ms
110
+ error_msg = (
111
+ f"Scanner '{scanner.SCANNER_NAME}' failed: "
112
+ f"{type(exc).__name__}: {exc}"
113
+ )
114
+ logger.warning(error_msg)
115
+ return ScanResult(
116
+ scanner=scanner.SCANNER_NAME,
117
+ sections={},
118
+ warnings=[error_msg],
119
+ duration_ms=elapsed_ms,
120
+ )
121
+
122
+ def _load_existing_context(self, output_path: Path) -> Dict[str, Any]:
123
+ """Load existing project-context.json if present.
124
+
125
+ Args:
126
+ output_path: Path to project-context.json.
127
+
128
+ Returns:
129
+ Parsed JSON dict, or empty structure if file does not exist.
130
+ """
131
+ if not output_path.is_file():
132
+ return {}
133
+
134
+ try:
135
+ with open(output_path, "r") as f:
136
+ return json.load(f)
137
+ except (json.JSONDecodeError, OSError) as exc:
138
+ logger.warning("Failed to load existing context: %s", exc)
139
+ return {}
140
+
141
+ def _resolve_output_path(self, project_root: Path) -> Path:
142
+ """Resolve the output path for project-context.json.
143
+
144
+ Args:
145
+ project_root: Project root path.
146
+
147
+ Returns:
148
+ Absolute path to project-context.json.
149
+ """
150
+ if self.config.output_path:
151
+ return self.config.output_path
152
+ return project_root / ".claude" / "project-context" / "project-context.json"
153
+
154
+ def _build_metadata(
155
+ self,
156
+ existing_metadata: Dict[str, Any],
157
+ project_root: Path,
158
+ ) -> Dict[str, Any]:
159
+ """Build updated metadata section (Rule 6: always update).
160
+
161
+ Preserves user-set fields (environment, cloud_provider, etc.) while
162
+ updating timestamps and scanner version.
163
+
164
+ Args:
165
+ existing_metadata: Existing metadata from project-context.json.
166
+ project_root: Project root path.
167
+
168
+ Returns:
169
+ Updated metadata dict.
170
+ """
171
+ now_iso = datetime.now(timezone.utc).isoformat()
172
+
173
+ metadata = dict(existing_metadata) if existing_metadata else {}
174
+ metadata["version"] = metadata.get("version", "2.0")
175
+ metadata["last_updated"] = now_iso
176
+
177
+ # Read contract_version from context-contracts.json
178
+ contract_version = self._read_contract_version()
179
+ if contract_version:
180
+ metadata["contract_version"] = contract_version
181
+
182
+ # Ensure scan_config sub-section exists
183
+ scan_config = metadata.get("scan_config", {})
184
+ if not isinstance(scan_config, dict):
185
+ scan_config = {}
186
+ scan_config["last_scan"] = now_iso
187
+ scan_config["scanner_version"] = scanner_package_version
188
+ scan_config["staleness_hours"] = self.config.staleness_hours
189
+ metadata["scan_config"] = scan_config
190
+
191
+ return metadata
192
+
193
+ @staticmethod
194
+ def _read_contract_version() -> Optional[str]:
195
+ """Read the version field from config/context-contracts.json.
196
+
197
+ Returns:
198
+ Version string (e.g. "3.0"), or None if file is missing or unreadable.
199
+ """
200
+ try:
201
+ if CONTRACT_CONFIG_PATH.is_file():
202
+ with open(CONTRACT_CONFIG_PATH, "r") as f:
203
+ data = json.load(f)
204
+ version = data.get("version")
205
+ if isinstance(version, str) and version:
206
+ return version
207
+ except (json.JSONDecodeError, OSError) as exc:
208
+ logger.debug("Failed to read contract version: %s", exc)
209
+ return None
210
+
211
+ def _atomic_write(self, output_path: Path, data: Dict[str, Any]) -> None:
212
+ """Atomically write data to JSON file.
213
+
214
+ Writes to a temp file in the same directory, then renames.
215
+ This prevents corruption from concurrent reads or crashes.
216
+
217
+ Args:
218
+ output_path: Target file path.
219
+ data: Dict to serialize as JSON.
220
+ """
221
+ output_path.parent.mkdir(parents=True, exist_ok=True)
222
+ tmp_path = output_path.with_suffix(".tmp")
223
+
224
+ try:
225
+ with open(tmp_path, "w") as f:
226
+ json.dump(data, f, indent=2, sort_keys=False)
227
+ f.write("\n")
228
+ os.rename(str(tmp_path), str(output_path))
229
+ except OSError as exc:
230
+ logger.error("Atomic write failed: %s", exc)
231
+ # Clean up temp file if rename failed
232
+ if tmp_path.exists():
233
+ try:
234
+ tmp_path.unlink()
235
+ except OSError:
236
+ pass
237
+ raise
238
+
239
+ def run(
240
+ self,
241
+ project_root: Optional[Path] = None,
242
+ write_output: bool = True,
243
+ ) -> ScanOutput:
244
+ """Run all registered scanners and return aggregated output.
245
+
246
+ Full pipeline:
247
+ 1. Load existing project-context.json
248
+ 2. Run scanners in parallel (or sequentially)
249
+ 3. Collect and combine scanner sections
250
+ 4. Merge with existing context using ownership rules
251
+ 5. Update metadata
252
+ 6. Atomic write to project-context.json (if write_output=True)
253
+ 7. Return ScanOutput
254
+
255
+ Args:
256
+ project_root: Project root path. Falls back to config.project_root.
257
+ write_output: Whether to write the result to disk (default True).
258
+
259
+ Returns:
260
+ ScanOutput with merged sections, warnings, errors, and timing.
261
+ """
262
+ root = project_root or self.config.project_root
263
+ start_ms = time.monotonic() * 1000
264
+
265
+ # Step 1: Load existing context
266
+ output_path = self._resolve_output_path(root)
267
+ existing_full = self._load_existing_context(output_path)
268
+ existing_sections = existing_full.get("sections", {})
269
+ existing_metadata = existing_full.get("metadata", {})
270
+
271
+ # Step 1.5: Detect workspace type BEFORE running scanners
272
+ workspace_info = detect_workspace_type(root)
273
+ if workspace_info.is_multi_repo:
274
+ logger.info(
275
+ "Multi-repo workspace: %d repos detected",
276
+ len(workspace_info.repo_dirs),
277
+ )
278
+
279
+ # Step 2: Run all scanners
280
+ scanners = self.registry.get_all()
281
+ if self.config.scanners:
282
+ requested = set(self.config.scanners)
283
+ scanners = [s for s in scanners if s.SCANNER_NAME in requested]
284
+
285
+ # Pass workspace info to each scanner instance
286
+ for scanner in scanners:
287
+ scanner.workspace_info = workspace_info
288
+
289
+ scanner_results: Dict[str, ScanResult] = {}
290
+ all_warnings: List[str] = []
291
+ all_errors: List[str] = []
292
+
293
+ if scanners and self.config.parallel:
294
+ scanner_results, all_warnings, all_errors = self._run_parallel(
295
+ scanners, root
296
+ )
297
+ else:
298
+ scanner_results, all_warnings, all_errors = self._run_sequential(
299
+ scanners, root
300
+ )
301
+
302
+ # Step 3: Collect and combine scanner sections
303
+ scan_sections = collect_scanner_sections(scanner_results)
304
+
305
+ # Step 4: Merge with existing context
306
+ section_owners = self.registry.get_section_owners()
307
+
308
+ # Merge (no backward-compat sections -- consumers read v2 directly)
309
+ merged_sections = merge_context(
310
+ existing=existing_sections,
311
+ scan_sections=scan_sections,
312
+ section_owners=section_owners,
313
+ )
314
+
315
+ # Step 5: Build metadata
316
+ metadata = self._build_metadata(existing_metadata, root)
317
+
318
+ # Determine which sections were updated vs preserved
319
+ sections_updated = sorted(set(scan_sections.keys()))
320
+ sections_preserved = sorted(
321
+ name for name in existing_sections
322
+ if name in AGENT_ENRICHED_SECTIONS
323
+ )
324
+
325
+ # Ensure architecture_overview exists as empty dict so contract
326
+ # references are satisfied (it appears in ALL agent contracts).
327
+ # Other agent-enriched sections are only created when an agent
328
+ # populates them -- no empty {} placeholders.
329
+ if "architecture_overview" not in merged_sections:
330
+ merged_sections["architecture_overview"] = {}
331
+
332
+ # --- Derive infrastructure.paths from scanner data ---
333
+ self._derive_infrastructure_paths(merged_sections)
334
+
335
+ # --- Cross-populate git.monorepo.workspace_config ---
336
+ self._cross_populate_monorepo(merged_sections)
337
+
338
+ # --- Remove empty {} placeholders for agent-enriched and mixed sections ---
339
+ # These sections should only exist when they have actual data.
340
+ # architecture_overview is the exception -- always present (even empty).
341
+ from tools.scan.merge import MIXED_SECTION_SCANNER_FIELDS
342
+ remove_if_empty = (
343
+ AGENT_ENRICHED_SECTIONS
344
+ | frozenset(MIXED_SECTION_SCANNER_FIELDS.keys())
345
+ ) - {"architecture_overview"}
346
+ for section_name in list(merged_sections.keys()):
347
+ if section_name in remove_if_empty:
348
+ if merged_sections[section_name] == {}:
349
+ del merged_sections[section_name]
350
+
351
+ # Build full output document (no top-level paths -- use
352
+ # infrastructure.paths as the single source of truth)
353
+ full_context: Dict[str, Any] = {
354
+ "metadata": metadata,
355
+ "sections": merged_sections,
356
+ }
357
+
358
+ # Step 7: Atomic write
359
+ if write_output:
360
+ self._atomic_write(output_path, full_context)
361
+
362
+ elapsed_ms = (time.monotonic() * 1000) - start_ms
363
+
364
+ return ScanOutput(
365
+ context=full_context,
366
+ sections_updated=sections_updated,
367
+ sections_preserved=sections_preserved,
368
+ warnings=all_warnings,
369
+ errors=all_errors,
370
+ duration_ms=elapsed_ms,
371
+ scanner_results=scanner_results,
372
+ )
373
+
374
+ @staticmethod
375
+ def _derive_infrastructure_paths(
376
+ merged_sections: Dict[str, Any],
377
+ ) -> None:
378
+ """Derive infrastructure.paths shortcuts from detected scanner data.
379
+
380
+ Populates infrastructure.paths.gitops, .terraform, and .app_services
381
+ from orchestration and infrastructure scanner results when the paths
382
+ are not already set.
383
+
384
+ Args:
385
+ merged_sections: Merged sections dict (mutated in place).
386
+ """
387
+ infra = merged_sections.get("infrastructure")
388
+ if not isinstance(infra, dict):
389
+ return
390
+
391
+ paths = infra.setdefault("paths", {})
392
+
393
+ # --- gitops: derive from orchestration.gitops.config_path ---
394
+ if not paths.get("gitops"):
395
+ orch = merged_sections.get("orchestration")
396
+ if isinstance(orch, dict):
397
+ gitops = orch.get("gitops", {})
398
+ if isinstance(gitops, dict) and gitops.get("config_path"):
399
+ paths["gitops"] = gitops["config_path"]
400
+
401
+ # --- terraform: derive from infrastructure.iac entries ---
402
+ if not paths.get("terraform"):
403
+ for iac_entry in infra.get("iac", []):
404
+ if isinstance(iac_entry, dict) and iac_entry.get("tool") in (
405
+ "terraform",
406
+ "terragrunt",
407
+ ):
408
+ base_path = iac_entry.get("base_path")
409
+ if base_path and base_path != ".":
410
+ paths["terraform"] = base_path
411
+ break
412
+
413
+ # --- app_services: derive from Dockerfile paths common parent ---
414
+ if not paths.get("app_services"):
415
+ containers = infra.get("containers", [])
416
+ dockerfile_dirs: list = []
417
+ for container in containers:
418
+ if not isinstance(container, dict):
419
+ continue
420
+ if container.get("tool") != "docker":
421
+ continue
422
+ for fpath in container.get("files", []):
423
+ parent = str(Path(fpath).parent)
424
+ if parent != ".":
425
+ dockerfile_dirs.append(parent)
426
+
427
+ if dockerfile_dirs:
428
+ # Find common parent directory
429
+ from pathlib import PurePosixPath
430
+
431
+ parts_list = [PurePosixPath(d).parts for d in dockerfile_dirs]
432
+ common: list = []
433
+ for segments in zip(*parts_list):
434
+ if len(set(segments)) == 1:
435
+ common.append(segments[0])
436
+ else:
437
+ break
438
+ if common:
439
+ paths["app_services"] = str(PurePosixPath(*common))
440
+
441
+ # Clean up: remove None-valued path entries
442
+ for key in list(paths.keys()):
443
+ if paths[key] is None:
444
+ del paths[key]
445
+
446
+ @staticmethod
447
+ def _cross_populate_monorepo(
448
+ merged_sections: Dict[str, Any],
449
+ ) -> None:
450
+ """Cross-populate git.monorepo.workspace_config from project_identity.
451
+
452
+ When the stack scanner detects a monorepo (project_identity.type ==
453
+ 'monorepo' and project_identity.monorepo has data), propagate the
454
+ workspace_config to git.monorepo so both sections are consistent.
455
+
456
+ Args:
457
+ merged_sections: Merged sections dict (mutated in place).
458
+ """
459
+ identity = merged_sections.get("project_identity")
460
+ git = merged_sections.get("git")
461
+ if not isinstance(identity, dict) or not isinstance(git, dict):
462
+ return
463
+
464
+ monorepo_data = identity.get("monorepo", {})
465
+ if not isinstance(monorepo_data, dict):
466
+ return
467
+
468
+ # If project_identity detected a monorepo, populate git.monorepo
469
+ if monorepo_data.get("detected"):
470
+ git_monorepo = git.setdefault("monorepo", {})
471
+ if isinstance(git_monorepo, dict):
472
+ tool = monorepo_data.get("tool")
473
+ if tool and not git_monorepo.get("workspace_config"):
474
+ git_monorepo["workspace_config"] = tool
475
+
476
+ def _run_parallel(
477
+ self,
478
+ scanners: List[BaseScanner],
479
+ root: Path,
480
+ ) -> tuple:
481
+ """Run scanners in parallel using ThreadPoolExecutor.
482
+
483
+ Args:
484
+ scanners: List of scanner instances to run.
485
+ root: Project root path.
486
+
487
+ Returns:
488
+ Tuple of (scanner_results, all_warnings, all_errors).
489
+ """
490
+ scanner_results: Dict[str, ScanResult] = {}
491
+ all_warnings: List[str] = []
492
+ all_errors: List[str] = []
493
+
494
+ with ThreadPoolExecutor(
495
+ max_workers=min(len(scanners), 8)
496
+ ) as executor:
497
+ future_to_scanner = {
498
+ executor.submit(self._run_scanner, scanner, root): scanner
499
+ for scanner in scanners
500
+ }
501
+ for future in as_completed(future_to_scanner):
502
+ scanner = future_to_scanner[future]
503
+ try:
504
+ result = future.result(
505
+ timeout=self.config.timeout_per_scanner
506
+ )
507
+ except Exception as exc:
508
+ error_msg = (
509
+ f"Scanner '{scanner.SCANNER_NAME}' timed out or "
510
+ f"failed in executor: {type(exc).__name__}: {exc}"
511
+ )
512
+ logger.warning(error_msg)
513
+ result = ScanResult(
514
+ scanner=scanner.SCANNER_NAME,
515
+ sections={},
516
+ warnings=[error_msg],
517
+ duration_ms=0.0,
518
+ )
519
+ all_errors.append(error_msg)
520
+
521
+ scanner_results[scanner.SCANNER_NAME] = result
522
+ all_warnings.extend(result.warnings)
523
+
524
+ return scanner_results, all_warnings, all_errors
525
+
526
+ def _run_sequential(
527
+ self,
528
+ scanners: List[BaseScanner],
529
+ root: Path,
530
+ ) -> tuple:
531
+ """Run scanners sequentially.
532
+
533
+ Args:
534
+ scanners: List of scanner instances to run.
535
+ root: Project root path.
536
+
537
+ Returns:
538
+ Tuple of (scanner_results, all_warnings, all_errors).
539
+ """
540
+ scanner_results: Dict[str, ScanResult] = {}
541
+ all_warnings: List[str] = []
542
+ all_errors: List[str] = []
543
+
544
+ for scanner in scanners:
545
+ result = self._run_scanner(scanner, root)
546
+ scanner_results[scanner.SCANNER_NAME] = result
547
+ all_warnings.extend(result.warnings)
548
+
549
+ return scanner_results, all_warnings, all_errors
@@ -0,0 +1,127 @@
1
+ """
2
+ Scanner Auto-Discovery Registry
3
+
4
+ Auto-discovers scanner modules from tools/scan/scanners/ directory.
5
+ Any .py file that contains a subclass of BaseScanner is registered
6
+ automatically. Validates no section ownership overlap at registration time.
7
+ """
8
+
9
+ import importlib
10
+ import logging
11
+ import pkgutil
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional
14
+
15
+ from tools.scan.scanners.base import BaseScanner
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ScannerRegistry:
21
+ """Registry for auto-discovered scanner modules.
22
+
23
+ Auto-discovers all scanner modules in tools/scan/scanners/ by importing
24
+ them and finding BaseScanner subclasses. Validates section ownership
25
+ uniqueness at registration time.
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ self._scanners: Dict[str, BaseScanner] = {}
30
+ self._section_owners: Dict[str, str] = {}
31
+ self._discover()
32
+
33
+ def _discover(self) -> None:
34
+ """Auto-discover all scanner modules in the scanners package."""
35
+ scanners_dir = Path(__file__).parent / "scanners"
36
+
37
+ if not scanners_dir.is_dir():
38
+ logger.warning("Scanners directory not found: %s", scanners_dir)
39
+ return
40
+
41
+ scanners_package = "tools.scan.scanners"
42
+
43
+ for module_info in pkgutil.iter_modules([str(scanners_dir)]):
44
+ if module_info.name.startswith("_") or module_info.name == "base":
45
+ continue
46
+
47
+ try:
48
+ module = importlib.import_module(
49
+ f"{scanners_package}.{module_info.name}"
50
+ )
51
+
52
+ # Find all BaseScanner subclasses in the module
53
+ for attr_name in dir(module):
54
+ attr = getattr(module, attr_name)
55
+ if (
56
+ isinstance(attr, type)
57
+ and issubclass(attr, BaseScanner)
58
+ and attr is not BaseScanner
59
+ ):
60
+ try:
61
+ scanner_instance = attr()
62
+ self.register(scanner_instance)
63
+ except TypeError:
64
+ # Cannot instantiate (still abstract)
65
+ pass
66
+
67
+ except Exception as exc:
68
+ logger.warning(
69
+ "Failed to load scanner module '%s': %s",
70
+ module_info.name,
71
+ exc,
72
+ )
73
+
74
+ def register(self, scanner: BaseScanner) -> None:
75
+ """Register a scanner, validating section ownership uniqueness.
76
+
77
+ Args:
78
+ scanner: Scanner instance to register.
79
+
80
+ Raises:
81
+ ValueError: If scanner name is duplicate or section ownership overlaps.
82
+ """
83
+ name = scanner.SCANNER_NAME
84
+
85
+ if name in self._scanners:
86
+ raise ValueError(
87
+ f"Duplicate scanner name: '{name}' is already registered"
88
+ )
89
+
90
+ # Check section ownership overlap
91
+ for section in scanner.OWNED_SECTIONS:
92
+ if section in self._section_owners:
93
+ existing_owner = self._section_owners[section]
94
+ raise ValueError(
95
+ f"Section ownership overlap: section '{section}' is owned by "
96
+ f"'{existing_owner}', cannot be claimed by '{name}'"
97
+ )
98
+
99
+ # Register
100
+ self._scanners[name] = scanner
101
+ for section in scanner.OWNED_SECTIONS:
102
+ self._section_owners[section] = name
103
+
104
+ logger.debug("Registered scanner: %s (sections: %s)", name, scanner.OWNED_SECTIONS)
105
+
106
+ def get_all(self) -> List[BaseScanner]:
107
+ """Return all registered scanners."""
108
+ return list(self._scanners.values())
109
+
110
+ def get_by_name(self, name: str) -> Optional[BaseScanner]:
111
+ """Get a scanner by name.
112
+
113
+ Args:
114
+ name: Scanner name to look up.
115
+
116
+ Returns:
117
+ Scanner instance or None if not found.
118
+ """
119
+ return self._scanners.get(name)
120
+
121
+ def get_section_owners(self) -> Dict[str, str]:
122
+ """Return mapping of section name to owning scanner name."""
123
+ return dict(self._section_owners)
124
+
125
+ def list_names(self) -> List[str]:
126
+ """Return list of all registered scanner names."""
127
+ return list(self._scanners.keys())
@@ -0,0 +1,18 @@
1
+ """
2
+ Scanner modules package.
3
+
4
+ Scanner modules are auto-discovered from this directory. Any .py file that
5
+ exports SCANNER_NAME, SCANNER_VERSION, OWNED_SECTIONS, and scan() is
6
+ registered automatically by ScannerRegistry.
7
+ """
8
+
9
+
10
+ def __getattr__(name: str):
11
+ """Lazy import to avoid circular dependency with tools.scan.registry."""
12
+ if name == "ScannerRegistry":
13
+ from tools.scan.registry import ScannerRegistry
14
+ return ScannerRegistry
15
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
16
+
17
+
18
+ __all__ = ["ScannerRegistry"]