@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
@@ -370,6 +370,10 @@ class GitScanner(BaseScanner):
370
370
  def scan(self, root: Path) -> ScanResult:
371
371
  """Scan the project for git configuration.
372
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
+
373
377
  Args:
374
378
  root: Absolute path to the project root directory.
375
379
 
@@ -379,6 +383,17 @@ class GitScanner(BaseScanner):
379
383
  start_ms = time.monotonic() * 1000
380
384
  warnings: List[str] = []
381
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)
382
397
  git_dir = root / ".git"
383
398
  git_root = root
384
399
 
@@ -446,6 +461,69 @@ class GitScanner(BaseScanner):
446
461
  duration_ms=elapsed,
447
462
  )
448
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
+
449
527
  @staticmethod
450
528
  def _find_git_in_subdirs(
451
529
  root: Path,
@@ -77,6 +77,9 @@ _CICD_MARKERS: List[Dict[str, Any]] = [
77
77
  {"platform": "gitlab-ci", "type": "file", "path": ".gitlab-ci.yml"},
78
78
  {"platform": "jenkins", "type": "file", "path": "Jenkinsfile"},
79
79
  {"platform": "circleci", "type": "dir", "path": ".circleci"},
80
+ {"platform": "bitbucket-pipelines", "type": "file", "path": "bitbucket-pipelines.yml"},
81
+ {"platform": "cloud-build", "type": "file", "path": "cloudbuild.yaml"},
82
+ {"platform": "cloud-build", "type": "file", "path": "cloudbuild.json"},
80
83
  ]
81
84
 
82
85
  # ---------------------------------------------------------------------------
@@ -114,6 +117,10 @@ class InfrastructureScanner(BaseScanner):
114
117
  def scan(self, root: Path) -> ScanResult:
115
118
  """Scan for infrastructure indicators.
116
119
 
120
+ In multi-repo mode, scans each repo subdirectory and tags IaC
121
+ entries with their containing repo name. In single-repo mode,
122
+ behaves as before.
123
+
117
124
  Args:
118
125
  root: Absolute path to the project root directory.
119
126
 
@@ -132,6 +139,12 @@ class InfrastructureScanner(BaseScanner):
132
139
  paths = self._detect_paths(root, warnings)
133
140
  app_services = self._detect_application_services(root, warnings)
134
141
 
142
+ # Multi-repo mode: also scan each repo subdirectory and tag results
143
+ if self.workspace_info and self.workspace_info.is_multi_repo:
144
+ self._enrich_multi_repo(
145
+ root, iac, containers, ci_cd, warnings
146
+ )
147
+
135
148
  # Only produce section when at least one indicator is found
136
149
  has_indicators = (
137
150
  cloud_providers
@@ -174,6 +187,58 @@ class InfrastructureScanner(BaseScanner):
174
187
  duration_ms = (time.monotonic() - start) * 1000
175
188
  return self.make_result(sections={}, warnings=[str(exc)], duration_ms=duration_ms)
176
189
 
190
+ def _enrich_multi_repo(
191
+ self,
192
+ root: Path,
193
+ iac: List[Dict[str, Any]],
194
+ containers: List[Dict[str, Any]],
195
+ ci_cd: List[Dict[str, Any]],
196
+ warnings: List[str],
197
+ ) -> None:
198
+ """Tag IaC, container, and CI/CD entries with their containing repo.
199
+
200
+ For each entry whose base_path or config_path starts with a known
201
+ repo directory, adds a 'repo' field with the repo name. This helps
202
+ agents understand which repo owns each infrastructure component.
203
+
204
+ Args:
205
+ root: Workspace root path.
206
+ iac: IaC entries list (mutated in place).
207
+ containers: Container entries list (mutated in place).
208
+ ci_cd: CI/CD entries list (mutated in place).
209
+ warnings: Warning accumulator.
210
+ """
211
+ repo_names = {
212
+ str(rd.relative_to(root)): rd.name
213
+ for rd in self.workspace_info.repo_dirs
214
+ }
215
+
216
+ def _tag_repo(entry: Dict[str, Any], path_key: str) -> None:
217
+ """Add 'repo' field if path matches a known repo directory."""
218
+ path_val = entry.get(path_key, "")
219
+ if not path_val:
220
+ return
221
+ for repo_path, repo_name in repo_names.items():
222
+ if path_val == repo_path or path_val.startswith(repo_path + "/"):
223
+ entry["repo"] = repo_name
224
+ return
225
+
226
+ for entry in iac:
227
+ _tag_repo(entry, "base_path")
228
+
229
+ for entry in containers:
230
+ # Containers use 'files' list; tag based on first file's directory
231
+ files = entry.get("files", [])
232
+ if files:
233
+ first_file = files[0]
234
+ for repo_path, repo_name in repo_names.items():
235
+ if first_file.startswith(repo_path + "/") or first_file.startswith(repo_path):
236
+ entry["repo"] = repo_name
237
+ break
238
+
239
+ for entry in ci_cd:
240
+ _tag_repo(entry, "config_path")
241
+
177
242
  # ------------------------------------------------------------------
178
243
  # Cloud provider detection
179
244
  # ------------------------------------------------------------------
@@ -151,6 +151,14 @@ class StackScanner(BaseScanner):
151
151
  build_tools = self._detect_build_tools(root, warnings)
152
152
  project_identity = self._detect_project_identity(root, languages, warnings)
153
153
 
154
+ # Multi-repo workspace override: if orchestrator detected multi-repo,
155
+ # set project type and add workspace_repos listing
156
+ if self.workspace_info and self.workspace_info.is_multi_repo:
157
+ project_identity["type"] = "multi-repo-workspace"
158
+ project_identity["workspace_repos"] = self._build_workspace_repos(
159
+ root, self.workspace_info.repo_dirs, warnings
160
+ )
161
+
154
162
  sections: Dict[str, Any] = {
155
163
  "project_identity": project_identity,
156
164
  "stack": {
@@ -821,6 +829,108 @@ class StackScanner(BaseScanner):
821
829
 
822
830
  return result
823
831
 
832
+ # ------------------------------------------------------------------
833
+ # Multi-repo workspace helpers
834
+ # ------------------------------------------------------------------
835
+
836
+ def _build_workspace_repos(
837
+ self,
838
+ root: Path,
839
+ repo_dirs: List[Path],
840
+ warnings: List[str],
841
+ ) -> List[Dict[str, Any]]:
842
+ """Build workspace_repos list for multi-repo workspaces.
843
+
844
+ For each subdirectory with .git, extracts name, relative path,
845
+ and primary language from the most prominent manifest file.
846
+
847
+ Args:
848
+ root: Workspace root path.
849
+ repo_dirs: List of subdirectory Paths that contain .git.
850
+ warnings: Warning accumulator.
851
+
852
+ Returns:
853
+ List of repo descriptor dicts.
854
+ """
855
+ repos: List[Dict[str, Any]] = []
856
+
857
+ for repo_dir in repo_dirs:
858
+ repo_entry: Dict[str, Any] = {
859
+ "name": repo_dir.name,
860
+ "path": str(repo_dir.relative_to(root)),
861
+ }
862
+
863
+ # Detect primary language from manifest files
864
+ primary_language = self._detect_primary_language(repo_dir)
865
+ if primary_language:
866
+ repo_entry["primary_language"] = primary_language
867
+
868
+ # Detect role from directory naming conventions
869
+ repo_entry["role"] = self._infer_repo_role(repo_dir, primary_language)
870
+
871
+ repos.append(repo_entry)
872
+
873
+ return repos
874
+
875
+ @staticmethod
876
+ def _detect_primary_language(repo_dir: Path) -> Optional[str]:
877
+ """Detect the primary language of a repo from its manifest files."""
878
+ manifest_checks = [
879
+ ("package.json", "javascript"),
880
+ ("pyproject.toml", "python"),
881
+ ("setup.py", "python"),
882
+ ("requirements.txt", "python"),
883
+ ("go.mod", "go"),
884
+ ("Cargo.toml", "rust"),
885
+ ("pom.xml", "java"),
886
+ ("build.gradle", "java"),
887
+ ("composer.json", "php"),
888
+ ("Gemfile", "ruby"),
889
+ ]
890
+
891
+ for filename, language in manifest_checks:
892
+ if (repo_dir / filename).is_file():
893
+ # Check for TypeScript indicator
894
+ if language == "javascript":
895
+ for f in repo_dir.iterdir():
896
+ if f.is_file() and f.name.startswith("tsconfig") and f.name.endswith(".json"):
897
+ return "typescript"
898
+ return language
899
+
900
+ return None
901
+
902
+ @staticmethod
903
+ def _infer_repo_role(repo_dir: Path, primary_language: Optional[str]) -> str:
904
+ """Infer the role of a repo from its name and contents.
905
+
906
+ Returns one of: gitops, iac, platform, agent, library, application.
907
+ """
908
+ name_lower = repo_dir.name.lower()
909
+
910
+ # GitOps indicators
911
+ if any(kw in name_lower for kw in ("gitops", "flux", "argocd", "deploy")):
912
+ return "gitops"
913
+
914
+ # IaC indicators
915
+ if any(kw in name_lower for kw in ("iac", "infra", "terraform")):
916
+ return "iac"
917
+ # Also check for .tf files at root
918
+ try:
919
+ if any(f.suffix == ".tf" for f in repo_dir.iterdir() if f.is_file()):
920
+ return "iac"
921
+ except OSError:
922
+ pass
923
+
924
+ # Platform indicators
925
+ if any(kw in name_lower for kw in ("platform", "core", "shared", "common")):
926
+ return "platform"
927
+
928
+ # Agent indicators
929
+ if any(kw in name_lower for kw in ("agent", "bot", "assistant")):
930
+ return "agent"
931
+
932
+ return "application"
933
+
824
934
  # ------------------------------------------------------------------
825
935
  # File search helpers
826
936
  # ------------------------------------------------------------------
@@ -8,7 +8,7 @@ functionality that gaia-scan needs when operating on a fresh project
8
8
  Functions:
9
9
  - create_claude_directory: mkdir .claude/ with symlinks and subdirs
10
10
  - copy_claude_md: deprecated no-op (identity now via submit hook)
11
- - copy_settings_json: copy settings.template.json (always replaces)
11
+ - copy_settings_json: create minimal settings.json only if missing (non-invasive)
12
12
  - install_git_hooks: copy commit-msg hook to all git repos
13
13
  - generate_governance: interpolate governance.template.md
14
14
  - ensure_gaia_ops_package: npm install @jaguilar87/gaia-ops
@@ -57,7 +57,7 @@ def _get_template_path(name: str) -> Path:
57
57
  """Get the path to a template file.
58
58
 
59
59
  Args:
60
- name: Template filename (e.g., 'settings.template.json').
60
+ name: Template filename (e.g., 'governance.template.md').
61
61
 
62
62
  Returns:
63
63
  Absolute path to the template file.
@@ -229,7 +229,7 @@ def copy_claude_md(project_root: Path) -> bool:
229
229
  """Deprecated — CLAUDE.md is no longer generated from template.
230
230
 
231
231
  Orchestrator identity is now injected by the UserPromptSubmit hook
232
- via ops_identity.py + on-demand skills (project-dispatch, agent-response).
232
+ via ops_identity.py + deterministic surface routing + on-demand skills (agent-response).
233
233
  This avoids two sources of truth.
234
234
 
235
235
  Kept as no-op for backward compatibility with callers.
@@ -239,33 +239,140 @@ def copy_claude_md(project_root: Path) -> bool:
239
239
 
240
240
 
241
241
  def copy_settings_json(project_root: Path) -> bool:
242
- """Copy settings.template.json to .claude/settings.json.
242
+ """Create a minimal .claude/settings.json only if it does not exist.
243
243
 
244
- Always overwrites -- settings.json is a system template that changes
245
- with each version. The template is the source of truth.
244
+ Non-invasive: never overwrites an existing settings.json. Hooks are
245
+ provided by hooks.json (auto-discovered via the .claude/hooks symlink).
246
+ Env vars and permissions live in settings.local.json.
246
247
 
247
248
  Args:
248
249
  project_root: Project root directory.
249
250
 
250
251
  Returns:
251
- True if file was written successfully.
252
+ True if file exists (created or already present).
252
253
  """
253
- template_path = _get_template_path("settings.template.json")
254
254
  dest_path = project_root / ".claude" / "settings.json"
255
255
 
256
- if not template_path.is_file():
257
- logger.warning("settings.template.json not found at %s", template_path)
258
- return False
256
+ if dest_path.is_file():
257
+ logger.info("settings.json already exists not overwriting")
258
+ return True
259
259
 
260
260
  try:
261
- shutil.copy2(str(template_path), str(dest_path))
262
- logger.info("settings.json updated from template")
261
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
262
+ dest_path.write_text("{}\n")
263
+ logger.info("settings.json created (minimal — hooks from hooks.json, env from settings.local.json)")
263
264
  return True
264
265
  except OSError as exc:
265
266
  logger.error("Failed to write settings.json: %s", exc)
266
267
  return False
267
268
 
268
269
 
270
+ def merge_hooks_to_settings_local(project_root: Path) -> bool:
271
+ """Merge hooks from hooks.json into .claude/settings.local.json.
272
+
273
+ In npm mode, Claude Code reads hooks from settings files, not hooks.json
274
+ directly. This reads hooks.json from the installed package, converts
275
+ ${CLAUDE_PLUGIN_ROOT}/hooks/<script> paths to .claude/hooks/<script>,
276
+ and merges into settings.local.json with deduplication by command string.
277
+
278
+ Args:
279
+ project_root: Project root directory.
280
+
281
+ Returns:
282
+ True if settings.local.json was modified.
283
+ """
284
+ import re
285
+
286
+ claude_dir = project_root / ".claude"
287
+ settings_path = claude_dir / "settings.local.json"
288
+
289
+ # Find hooks.json from the package
290
+ hooks_json_path = None
291
+ # Strategy 1: installed npm package
292
+ pkg_root = _find_installed_package_root(project_root)
293
+ if pkg_root:
294
+ candidate = pkg_root / "hooks" / "hooks.json"
295
+ if candidate.is_file():
296
+ hooks_json_path = candidate
297
+ # Strategy 2: running from source (gaia-scan direct)
298
+ if not hooks_json_path:
299
+ candidate = _find_package_root() / "hooks" / "hooks.json"
300
+ if candidate.is_file():
301
+ hooks_json_path = candidate
302
+
303
+ if not hooks_json_path:
304
+ logger.info("hooks.json not found, skipping hooks merge")
305
+ return False
306
+
307
+ try:
308
+ hooks_data = json.loads(hooks_json_path.read_text())
309
+ except (json.JSONDecodeError, OSError):
310
+ logger.warning("hooks.json is invalid, skipping hooks merge")
311
+ return False
312
+
313
+ # Unwrap outer "hooks" key if present
314
+ source_hooks = hooks_data.get("hooks", hooks_data)
315
+
316
+ # Convert ${CLAUDE_PLUGIN_ROOT}/hooks/<script> to .claude/hooks/<script>
317
+ def convert_command(cmd: str) -> str:
318
+ return re.sub(r'\$\{CLAUDE_PLUGIN_ROOT\}/hooks/', '.claude/hooks/', cmd)
319
+
320
+ converted_hooks: Dict[str, list] = {}
321
+ for event, entries in source_hooks.items():
322
+ converted_hooks[event] = []
323
+ for entry in entries:
324
+ new_entry = dict(entry)
325
+ if "hooks" in new_entry:
326
+ new_entry["hooks"] = [
327
+ {**h, "command": convert_command(h["command"])} if "command" in h else h
328
+ for h in new_entry["hooks"]
329
+ ]
330
+ converted_hooks[event].append(new_entry)
331
+
332
+ # Load existing settings.local.json
333
+ existing: Dict[str, Any] = {}
334
+ if settings_path.exists():
335
+ try:
336
+ existing = json.loads(settings_path.read_text())
337
+ except (json.JSONDecodeError, OSError):
338
+ existing = {}
339
+
340
+ # Smart merge: deduplicate by command string
341
+ existing_hooks = existing.get("hooks", {})
342
+ changed = False
343
+
344
+ for event, new_entries in converted_hooks.items():
345
+ if event not in existing_hooks:
346
+ existing_hooks[event] = new_entries
347
+ changed = True
348
+ continue
349
+
350
+ # Collect existing command strings
351
+ existing_commands: set = set()
352
+ for entry in existing_hooks[event]:
353
+ for h in entry.get("hooks", []):
354
+ if "command" in h:
355
+ existing_commands.add(h["command"])
356
+
357
+ # Add entries whose commands are not already present
358
+ for new_entry in new_entries:
359
+ new_commands = [h["command"] for h in new_entry.get("hooks", []) if "command" in h]
360
+ all_present = len(new_commands) > 0 and all(c in existing_commands for c in new_commands)
361
+ if not all_present:
362
+ existing_hooks[event].append(new_entry)
363
+ changed = True
364
+
365
+ if not changed:
366
+ logger.info("settings.local.json hooks already up to date")
367
+ return False
368
+
369
+ existing["hooks"] = existing_hooks
370
+ claude_dir.mkdir(parents=True, exist_ok=True)
371
+ settings_path.write_text(json.dumps(existing, indent=2) + "\n")
372
+ logger.info("Merged hooks into %s", settings_path)
373
+ return True
374
+
375
+
269
376
  def install_git_hooks(project_root: Path) -> int:
270
377
  """Install commit-msg git hook to all detected git repositories.
271
378
 
@@ -0,0 +1,85 @@
1
+ """
2
+ Workspace Type Detection
3
+
4
+ Detects whether the scan root is a single-repo, monorepo, or multi-repo workspace.
5
+ Called by the orchestrator before individual scanners run, and importable by scanners
6
+ that need workspace-aware behavior.
7
+
8
+ Detection logic:
9
+ - If root has .git -> single-repo or monorepo (determined by stack scanner)
10
+ - If root has NO .git but 2+ immediate subdirectories have .git -> multi-repo-workspace
11
+ - Otherwise -> single-repo (default)
12
+ """
13
+
14
+ import logging
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import List, Optional
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Directories to always skip during workspace scanning
22
+ _SKIP_DIRS = frozenset({
23
+ "node_modules", "__pycache__", ".tox", ".venv",
24
+ "venv", "dist", "build", ".next", ".nuxt", "target",
25
+ ".pytest_cache", ".mypy_cache", ".ruff_cache", "vendor",
26
+ ".terraform", ".terragrunt-cache",
27
+ })
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class WorkspaceInfo:
32
+ """Result of workspace type detection.
33
+
34
+ Attributes:
35
+ workspace_type: One of 'single-repo', 'monorepo', 'multi-repo-workspace'.
36
+ repo_dirs: For multi-repo, list of subdirectory Paths that contain .git.
37
+ Empty for single-repo/monorepo.
38
+ """
39
+
40
+ workspace_type: str = "single-repo"
41
+ repo_dirs: List[Path] = field(default_factory=list)
42
+
43
+ @property
44
+ def is_multi_repo(self) -> bool:
45
+ return self.workspace_type == "multi-repo-workspace"
46
+
47
+
48
+ def detect_workspace_type(root: Path) -> WorkspaceInfo:
49
+ """Detect the workspace type for the given root directory.
50
+
51
+ Args:
52
+ root: Absolute path to the project root directory.
53
+
54
+ Returns:
55
+ WorkspaceInfo with the detected workspace type and repo directories.
56
+ """
57
+ # If root itself has .git, it's a normal repo (single or monorepo)
58
+ if (root / ".git").is_dir():
59
+ return WorkspaceInfo(workspace_type="single-repo")
60
+
61
+ # Check immediate subdirectories for .git
62
+ git_subdirs: List[Path] = []
63
+ try:
64
+ for entry in sorted(root.iterdir()):
65
+ if not entry.is_dir():
66
+ continue
67
+ if entry.name.startswith(".") or entry.name in _SKIP_DIRS:
68
+ continue
69
+ if (entry / ".git").is_dir():
70
+ git_subdirs.append(entry)
71
+ except (PermissionError, OSError) as exc:
72
+ logger.debug("Failed to scan subdirectories of %s: %s", root, exc)
73
+
74
+ if len(git_subdirs) >= 2:
75
+ logger.info(
76
+ "Multi-repo workspace detected: %d repos in %s",
77
+ len(git_subdirs),
78
+ root,
79
+ )
80
+ return WorkspaceInfo(
81
+ workspace_type="multi-repo-workspace",
82
+ repo_dirs=git_subdirs,
83
+ )
84
+
85
+ return WorkspaceInfo(workspace_type="single-repo")
@@ -1,42 +0,0 @@
1
- {
2
- "_note": "Legacy file. Canonical source: context-contracts.json + cloud/{provider}.json",
3
- "version": "3.0",
4
- "provider": "aws",
5
- "description": "Legacy AWS contracts (self-contained). Kept for backward compatibility with tests. Canonical source: context-contracts.json + cloud/aws.json. v3: removed backward-compat sections.",
6
- "agents": {
7
- "cloud-troubleshooter": {
8
- "read": ["project_identity", "stack", "git", "environment",
9
- "infrastructure", "orchestration", "cluster_details",
10
- "infrastructure_topology", "terraform_infrastructure",
11
- "gitops_configuration", "application_services",
12
- "monitoring_observability",
13
- "vpc_mapping", "load_balancers"],
14
- "write": ["cluster_details", "infrastructure_topology", "vpc_mapping"]
15
- },
16
- "gitops-operator": {
17
- "read": ["project_identity", "stack", "git", "environment",
18
- "infrastructure", "orchestration", "gitops_configuration",
19
- "cluster_details", "operational_guidelines"],
20
- "write": ["gitops_configuration", "cluster_details"]
21
- },
22
- "terraform-architect": {
23
- "read": ["project_identity", "stack", "git", "environment",
24
- "infrastructure", "orchestration", "terraform_infrastructure",
25
- "infrastructure_topology", "operational_guidelines",
26
- "vpc_mapping", "load_balancers", "api_gateway"],
27
- "write": ["terraform_infrastructure", "infrastructure_topology",
28
- "vpc_mapping", "load_balancers", "api_gateway"]
29
- },
30
- "devops-developer": {
31
- "read": ["project_identity", "stack", "git", "environment",
32
- "infrastructure", "application_services",
33
- "operational_guidelines"],
34
- "write": ["application_services"]
35
- },
36
- "speckit-planner": {
37
- "read": ["project_identity", "stack", "git", "environment",
38
- "infrastructure", "operational_guidelines"],
39
- "write": ["operational_guidelines"]
40
- }
41
- }
42
- }
@@ -1,39 +0,0 @@
1
- {
2
- "_note": "Legacy file. Canonical source: context-contracts.json + cloud/{provider}.json",
3
- "version": "3.0",
4
- "provider": "gcp",
5
- "description": "Legacy GCP contracts (self-contained). Kept for backward compatibility with tests. Canonical source: context-contracts.json + cloud/gcp.json. v3: removed backward-compat sections.",
6
- "agents": {
7
- "cloud-troubleshooter": {
8
- "read": ["project_identity", "stack", "git", "environment",
9
- "infrastructure", "orchestration", "cluster_details",
10
- "infrastructure_topology", "terraform_infrastructure",
11
- "gitops_configuration", "application_services",
12
- "monitoring_observability"],
13
- "write": ["cluster_details", "infrastructure_topology"]
14
- },
15
- "gitops-operator": {
16
- "read": ["project_identity", "stack", "git", "environment",
17
- "infrastructure", "orchestration", "gitops_configuration",
18
- "cluster_details", "operational_guidelines"],
19
- "write": ["gitops_configuration", "cluster_details"]
20
- },
21
- "terraform-architect": {
22
- "read": ["project_identity", "stack", "git", "environment",
23
- "infrastructure", "orchestration", "terraform_infrastructure",
24
- "infrastructure_topology", "operational_guidelines"],
25
- "write": ["terraform_infrastructure", "infrastructure_topology"]
26
- },
27
- "devops-developer": {
28
- "read": ["project_identity", "stack", "git", "environment",
29
- "infrastructure", "application_services",
30
- "operational_guidelines"],
31
- "write": ["application_services"]
32
- },
33
- "speckit-planner": {
34
- "read": ["project_identity", "stack", "git", "environment",
35
- "infrastructure", "operational_guidelines"],
36
- "write": []
37
- }
38
- }
39
- }