@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,753 @@
1
+ """
2
+ Setup / Installation Functions for gaia-scan
3
+
4
+ Ported from the original gaia-init — provides all the installation and setup
5
+ functionality that gaia-scan needs when operating on a fresh project
6
+ (Mode 1) or refreshing an existing project (Mode 2).
7
+
8
+ Functions:
9
+ - create_claude_directory: mkdir .claude/ with symlinks and subdirs
10
+ - copy_claude_md: deprecated no-op (identity now via submit hook)
11
+ - copy_settings_json: create minimal settings.json only if missing (non-invasive)
12
+ - install_git_hooks: copy commit-msg hook to all git repos
13
+ - generate_governance: interpolate governance.template.md
14
+ - ensure_gaia_ops_package: npm install @jaguilar87/gaia-ops
15
+ - ensure_claude_code: check/install claude CLI
16
+ - generate_project_context: create/merge project-context.json
17
+ """
18
+
19
+ import json
20
+ import logging
21
+ import os
22
+ import shutil
23
+ import subprocess
24
+ from datetime import datetime, timezone
25
+ from pathlib import Path
26
+ from typing import Any, Dict, List, Optional
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ def _find_package_root() -> Path:
32
+ """Find the gaia-ops plugin root directory.
33
+
34
+ Returns the directory containing this file's grandparent (tools/scan/setup.py
35
+ -> tools/ -> plugin root). This works both when running from the plugin
36
+ directory directly and when installed as a package.
37
+ """
38
+ return Path(__file__).resolve().parent.parent.parent
39
+
40
+
41
+ def _find_installed_package_root(project_root: Path) -> Optional[Path]:
42
+ """Find the installed @jaguilar87/gaia-ops package in node_modules.
43
+
44
+ Args:
45
+ project_root: Project root directory.
46
+
47
+ Returns:
48
+ Path to the package root, or None if not found.
49
+ """
50
+ pkg_path = project_root / "node_modules" / "@jaguilar87" / "gaia-ops"
51
+ if pkg_path.is_dir():
52
+ return pkg_path
53
+ return None
54
+
55
+
56
+ def _get_template_path(name: str) -> Path:
57
+ """Get the path to a template file.
58
+
59
+ Args:
60
+ name: Template filename (e.g., 'governance.template.md').
61
+
62
+ Returns:
63
+ Absolute path to the template file.
64
+ """
65
+ return _find_package_root() / "templates" / name
66
+
67
+
68
+ def ensure_gaia_ops_package(project_root: Path) -> bool:
69
+ """Ensure @jaguilar87/gaia-ops is installed as npm dependency.
70
+
71
+ Checks node_modules for the package. If not found, creates package.json
72
+ if needed and runs npm install.
73
+
74
+ Args:
75
+ project_root: Project root directory.
76
+
77
+ Returns:
78
+ True if package is available (already installed or newly installed).
79
+ """
80
+ pkg_path = project_root / "node_modules" / "@jaguilar87" / "gaia-ops" / "package.json"
81
+ if pkg_path.is_file():
82
+ logger.info("@jaguilar87/gaia-ops already installed")
83
+ return True
84
+
85
+ # Create package.json if missing
86
+ package_json_path = project_root / "package.json"
87
+ if not package_json_path.is_file():
88
+ initial_pkg = {
89
+ "name": "my-project",
90
+ "version": "1.0.0",
91
+ "private": True,
92
+ "dependencies": {},
93
+ }
94
+ package_json_path.write_text(json.dumps(initial_pkg, indent=2))
95
+
96
+ try:
97
+ subprocess.run(
98
+ ["npm", "install", "@jaguilar87/gaia-ops"],
99
+ cwd=str(project_root),
100
+ capture_output=True,
101
+ text=True,
102
+ timeout=120,
103
+ check=True,
104
+ )
105
+ logger.info("@jaguilar87/gaia-ops installed")
106
+ return True
107
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as exc:
108
+ logger.error("Failed to install @jaguilar87/gaia-ops: %s", exc)
109
+ return False
110
+
111
+
112
+ def ensure_claude_code(skip_install: bool = False) -> Dict[str, Any]:
113
+ """Check if Claude Code CLI is installed, optionally install it.
114
+
115
+ Args:
116
+ skip_install: If True, skip installation attempt.
117
+
118
+ Returns:
119
+ Dict with 'installed' (bool) and 'version' (str or None).
120
+ """
121
+ # Try to get version
122
+ for cmd in ["claude --version", "claude-code --version"]:
123
+ try:
124
+ result = subprocess.run(
125
+ cmd.split(),
126
+ capture_output=True,
127
+ text=True,
128
+ timeout=10,
129
+ )
130
+ if result.returncode == 0:
131
+ version = result.stdout.strip().split("\n")[0]
132
+ return {"installed": True, "version": version}
133
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
134
+ continue
135
+
136
+ if skip_install:
137
+ logger.warning("Claude Code not installed (--skip-claude-install used)")
138
+ return {"installed": False, "version": None}
139
+
140
+ # Attempt installation
141
+ try:
142
+ subprocess.run(
143
+ ["npm", "install", "-g", "@anthropic-ai/claude-code"],
144
+ capture_output=True,
145
+ text=True,
146
+ timeout=120,
147
+ check=True,
148
+ )
149
+ logger.info("Claude Code installed")
150
+ return {"installed": True, "version": "newly installed"}
151
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as exc:
152
+ logger.warning("Failed to install Claude Code: %s", exc)
153
+ return {"installed": False, "version": None}
154
+
155
+
156
+ def create_claude_directory(project_root: Path) -> List[str]:
157
+ """Create .claude/ directory with symlinks to the gaia-ops package.
158
+
159
+ Creates:
160
+ - Symlinks: agents, tools, hooks, commands, templates, config, speckit, skills, CHANGELOG.md
161
+ - Directories: logs, tests, project-context, project-context/workflow-episodic-memory, approvals
162
+
163
+ Args:
164
+ project_root: Project root directory.
165
+
166
+ Returns:
167
+ List of created symlink names (for reporting).
168
+ """
169
+ claude_dir = project_root / ".claude"
170
+ claude_dir.mkdir(exist_ok=True)
171
+
172
+ # Find the installed package for symlinks
173
+ package_path = _find_installed_package_root(project_root)
174
+ if package_path is None:
175
+ # Fallback: use the plugin root directly (running from source)
176
+ package_path = _find_package_root()
177
+
178
+ # Compute relative path from .claude/ to the package
179
+ try:
180
+ rel_path = os.path.relpath(str(package_path), str(claude_dir))
181
+ except ValueError:
182
+ # On Windows, relpath can fail across drives
183
+ rel_path = str(package_path)
184
+
185
+ # Create symlinks
186
+ symlink_names = [
187
+ "agents", "tools", "hooks", "commands",
188
+ "templates", "config", "speckit", "skills",
189
+ ]
190
+ created = []
191
+
192
+ for name in symlink_names:
193
+ link_path = claude_dir / name
194
+ target = os.path.join(rel_path, name)
195
+
196
+ if link_path.exists() or link_path.is_symlink():
197
+ link_path.unlink()
198
+
199
+ try:
200
+ os.symlink(target, str(link_path))
201
+ created.append(name)
202
+ except OSError as exc:
203
+ logger.warning("Failed to create symlink %s: %s", name, exc)
204
+
205
+ # CHANGELOG.md symlink
206
+ changelog_link = claude_dir / "CHANGELOG.md"
207
+ if changelog_link.exists() or changelog_link.is_symlink():
208
+ changelog_link.unlink()
209
+ try:
210
+ os.symlink(os.path.join(rel_path, "CHANGELOG.md"), str(changelog_link))
211
+ created.append("CHANGELOG.md")
212
+ except OSError as exc:
213
+ logger.warning("Failed to create CHANGELOG.md symlink: %s", exc)
214
+
215
+ # Create project-specific directories (NOT symlinked)
216
+ for subdir in [
217
+ "logs",
218
+ "tests",
219
+ "project-context",
220
+ os.path.join("project-context", "workflow-episodic-memory"),
221
+ "approvals",
222
+ ]:
223
+ (claude_dir / subdir).mkdir(parents=True, exist_ok=True)
224
+
225
+ return created
226
+
227
+
228
+ def copy_claude_md(project_root: Path) -> bool:
229
+ """Deprecated — CLAUDE.md is no longer generated from template.
230
+
231
+ Orchestrator identity is now injected by the UserPromptSubmit hook
232
+ via ops_identity.py + deterministic surface routing + on-demand skills (agent-response).
233
+ This avoids two sources of truth.
234
+
235
+ Kept as no-op for backward compatibility with callers.
236
+ """
237
+ logger.info("copy_claude_md skipped — identity now injected via submit hook")
238
+ return True
239
+
240
+
241
+ def copy_settings_json(project_root: Path) -> bool:
242
+ """Create a minimal .claude/settings.json only if it does not exist.
243
+
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.
247
+
248
+ Args:
249
+ project_root: Project root directory.
250
+
251
+ Returns:
252
+ True if file exists (created or already present).
253
+ """
254
+ dest_path = project_root / ".claude" / "settings.json"
255
+
256
+ if dest_path.is_file():
257
+ logger.info("settings.json already exists — not overwriting")
258
+ return True
259
+
260
+ try:
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)")
264
+ return True
265
+ except OSError as exc:
266
+ logger.error("Failed to write settings.json: %s", exc)
267
+ return False
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
+
376
+ def install_git_hooks(project_root: Path) -> int:
377
+ """Install commit-msg git hook to all detected git repositories.
378
+
379
+ Copies git-hooks/commit-msg from the package to .git/hooks/ in all
380
+ repos found in the project root and its immediate subdirectories.
381
+
382
+ Args:
383
+ project_root: Project root directory.
384
+
385
+ Returns:
386
+ Number of repos where hooks were installed.
387
+ """
388
+ hook_source = _find_package_root() / "git-hooks" / "commit-msg"
389
+ if not hook_source.is_file():
390
+ logger.warning("git-hooks/commit-msg not found in package, skipping")
391
+ return 0
392
+
393
+ # Find git repos: project root and immediate subdirectories
394
+ candidates = [project_root]
395
+ try:
396
+ for entry in project_root.iterdir():
397
+ if entry.is_dir() and not entry.name.startswith(".") and entry.name != "node_modules":
398
+ candidates.append(entry)
399
+ except OSError:
400
+ pass
401
+
402
+ installed = 0
403
+ for dir_path in candidates:
404
+ git_hooks_dir = dir_path / ".git" / "hooks"
405
+ if not git_hooks_dir.is_dir():
406
+ continue
407
+
408
+ dest = git_hooks_dir / "commit-msg"
409
+ try:
410
+ shutil.copy2(str(hook_source), str(dest))
411
+ os.chmod(str(dest), 0o755)
412
+ installed += 1
413
+ except OSError as exc:
414
+ logger.warning("Failed to install hook in %s: %s", dir_path, exc)
415
+
416
+ return installed
417
+
418
+
419
+ def generate_governance(project_root: Path, config: Dict[str, Any]) -> bool:
420
+ """Generate governance.md from template with config interpolation.
421
+
422
+ Only creates governance.md if it does not already exist (it is managed
423
+ by speckit.init after initial creation).
424
+
425
+ Args:
426
+ project_root: Project root directory.
427
+ config: Configuration dict with keys: cloud_provider, region,
428
+ project_id, cluster_name, gitops, terraform.
429
+
430
+ Returns:
431
+ True if governance.md was created or already exists.
432
+ """
433
+ speckit_root = config.get("speckit_root", ".claude/project-context/speckit-project-specs")
434
+
435
+ if os.path.isabs(speckit_root):
436
+ resolved_root = Path(speckit_root)
437
+ else:
438
+ resolved_root = project_root / speckit_root
439
+
440
+ resolved_root.mkdir(parents=True, exist_ok=True)
441
+ dest_path = resolved_root / "governance.md"
442
+
443
+ if dest_path.is_file():
444
+ logger.info("governance.md already exists -- skipping (managed by speckit.init)")
445
+ return True
446
+
447
+ template_path = _get_template_path("governance.template.md")
448
+ if not template_path.is_file():
449
+ logger.warning("governance.template.md not found -- skipping")
450
+ return False
451
+
452
+ try:
453
+ template = template_path.read_text()
454
+
455
+ cloud_provider = config.get("cloud_provider", "gcp")
456
+ k8s_platform = {
457
+ "aws": "EKS",
458
+ "gcp": "GKE",
459
+ }.get(cloud_provider, "Kubernetes")
460
+
461
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
462
+
463
+ interpolated = (
464
+ template
465
+ .replace("[CLOUD_PROVIDER]", (cloud_provider or "gcp").upper())
466
+ .replace("[PRIMARY_REGION]", config.get("region", "") or "N/A")
467
+ .replace("[PROJECT_ID]", config.get("project_id", "") or "N/A")
468
+ .replace("[CLUSTER_NAME]", config.get("cluster_name", "") or "N/A")
469
+ .replace("[GITOPS_PATH]", config.get("gitops", "") or "N/A")
470
+ .replace("[TERRAFORM_PATH]", config.get("terraform", "") or "N/A")
471
+ .replace("[POSTGRES_INSTANCE]", "N/A")
472
+ .replace("[CONTAINER_REGISTRY]", "N/A")
473
+ .replace("[K8S_PLATFORM]", k8s_platform)
474
+ .replace("[DATE]", today)
475
+ )
476
+
477
+ dest_path.write_text(interpolated)
478
+ logger.info("governance.md created at %s", dest_path)
479
+ return True
480
+
481
+ except OSError as exc:
482
+ logger.error("Failed to create governance.md: %s", exc)
483
+ return False
484
+
485
+
486
+ def generate_project_context(
487
+ project_root: Path,
488
+ config: Dict[str, Any],
489
+ scan_context: Optional[Dict[str, Any]] = None,
490
+ ) -> bool:
491
+ """Generate or merge project-context.json from config and scan results.
492
+
493
+ For fresh projects (no existing file): writes a full generated context
494
+ that includes scan results if available.
495
+
496
+ For existing projects: merges metadata and paths from scan, preserves
497
+ agent-enriched sections.
498
+
499
+ Args:
500
+ project_root: Project root directory.
501
+ config: Configuration dict with detected/user-provided values.
502
+ scan_context: Full context from scan orchestrator (if available).
503
+
504
+ Returns:
505
+ True if file was written successfully.
506
+ """
507
+ dest_path = (
508
+ project_root / ".claude" / "project-context" / "project-context.json"
509
+ )
510
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
511
+
512
+ now_iso = datetime.now(timezone.utc).isoformat()
513
+
514
+ # If we have scan_context from the orchestrator, it already has the
515
+ # correct v2 schema structure. Use it as the base.
516
+ if scan_context:
517
+ # Enrich scan context with user-provided config values
518
+ context = _enrich_scan_context(scan_context, config, now_iso, project_root)
519
+ else:
520
+ # Build a minimal context from config alone
521
+ context = _build_minimal_context(config, now_iso, project_root)
522
+
523
+ try:
524
+ if not dest_path.is_file():
525
+ # First-time install: write the full context
526
+ dest_path.write_text(json.dumps(context, indent=2) + "\n")
527
+ logger.info("project-context.json generated")
528
+ return True
529
+
530
+ # File exists -- merge
531
+ try:
532
+ existing = json.loads(dest_path.read_text())
533
+ except (json.JSONDecodeError, OSError):
534
+ dest_path.write_text(json.dumps(context, indent=2) + "\n")
535
+ logger.info("project-context.json regenerated (previous was invalid)")
536
+ return True
537
+
538
+ merged = _merge_project_context(existing, context)
539
+ dest_path.write_text(json.dumps(merged, indent=2) + "\n")
540
+ logger.info("project-context.json updated (metadata+paths synced, sections preserved)")
541
+ return True
542
+
543
+ except OSError as exc:
544
+ logger.error("Failed to write project-context.json: %s", exc)
545
+ return False
546
+
547
+
548
+ def _enrich_scan_context(
549
+ scan_context: Dict[str, Any],
550
+ config: Dict[str, Any],
551
+ now_iso: str,
552
+ project_root: Path,
553
+ ) -> Dict[str, Any]:
554
+ """Enrich scan context with user-provided config values."""
555
+ import copy
556
+ context = copy.deepcopy(scan_context)
557
+
558
+ # Ensure metadata exists
559
+ meta = context.setdefault("metadata", {})
560
+ meta["version"] = meta.get("version", "2.0")
561
+ meta["last_updated"] = now_iso
562
+ meta["created_by"] = "gaia-scan"
563
+
564
+ # Update infrastructure.paths from config (user overrides trump scan)
565
+ sections = context.setdefault("sections", {})
566
+ infra = sections.setdefault("infrastructure", {})
567
+ infra_paths = infra.setdefault("paths", {})
568
+ if config.get("gitops"):
569
+ infra_paths["gitops"] = config["gitops"]
570
+ if config.get("terraform"):
571
+ infra_paths["terraform"] = config["terraform"]
572
+ if config.get("app_services"):
573
+ infra_paths["app_services"] = config["app_services"]
574
+ # Remove top-level paths if present (single source: infrastructure.paths)
575
+ context.pop("paths", None)
576
+
577
+ # Ensure operational_guidelines has speckit_root
578
+ sections = context.setdefault("sections", {})
579
+ op_guide = sections.setdefault("operational_guidelines", {})
580
+ if "speckit_root" not in op_guide:
581
+ op_guide["speckit_root"] = config.get(
582
+ "speckit_root",
583
+ ".claude/project-context/speckit-project-specs",
584
+ )
585
+
586
+ # Enrich sections from contract file
587
+ _enrich_from_contracts(context, config, project_root)
588
+
589
+ return context
590
+
591
+
592
+ def _build_minimal_context(
593
+ config: Dict[str, Any],
594
+ now_iso: str,
595
+ project_root: Path,
596
+ ) -> Dict[str, Any]:
597
+ """Build a minimal project-context.json from config when no scan data available."""
598
+ cloud_provider = config.get("cloud_provider", "gcp")
599
+ project_name = config.get("project_name", project_root.name)
600
+
601
+ metadata = {
602
+ "version": "2.0",
603
+ "last_updated": now_iso,
604
+ "project_name": project_name,
605
+ "project_root": ".",
606
+ "created_by": "gaia-scan",
607
+ "cloud_provider": cloud_provider,
608
+ "environment": "non-prod",
609
+ "primary_region": config.get("region", ""),
610
+ }
611
+
612
+ if cloud_provider in ("gcp", "multi-cloud") and config.get("project_id"):
613
+ metadata["project_id"] = config["project_id"]
614
+ if cloud_provider in ("aws", "multi-cloud") and config.get("project_id"):
615
+ metadata["aws_account"] = config["project_id"]
616
+
617
+ cloud_entry: Dict[str, Any] = {
618
+ "name": cloud_provider,
619
+ "region": config.get("region", ""),
620
+ }
621
+ if cloud_provider in ("gcp", "multi-cloud") and config.get("project_id"):
622
+ cloud_entry["project_id"] = config["project_id"]
623
+ if cloud_provider in ("aws", "multi-cloud") and config.get("project_id"):
624
+ cloud_entry["account_id"] = config["project_id"]
625
+
626
+ speckit_root = config.get("speckit_root", ".claude/project-context/speckit-project-specs")
627
+
628
+ # Build paths dict, filtering out empty strings
629
+ infra_paths: Dict[str, str] = {}
630
+ for key in ("gitops", "terraform", "app_services"):
631
+ val = config.get(key, "")
632
+ if val:
633
+ infra_paths[key] = val
634
+
635
+ context = {
636
+ "metadata": metadata,
637
+ "sections": {
638
+ "project_identity": {
639
+ "name": project_name,
640
+ "type": "application",
641
+ },
642
+ "stack": {"languages": [], "frameworks": [], "build_tools": []},
643
+ "git": {
644
+ "platform": config.get("git_platform"),
645
+ "remotes": [],
646
+ "default_branch": "main",
647
+ },
648
+ "environment": {"runtimes": [], "os": {}},
649
+ "infrastructure": {
650
+ "cloud_providers": [cloud_entry],
651
+ "ci_cd": (
652
+ [{"platform": config["ci_platform"]}]
653
+ if config.get("ci_platform")
654
+ else []
655
+ ),
656
+ "paths": infra_paths,
657
+ },
658
+ "operational_guidelines": {
659
+ "speckit_root": speckit_root,
660
+ "commit_standards": {
661
+ "format": "conventional_commits",
662
+ "validation_required": True,
663
+ "config_path": ".claude/config/git_standards.json",
664
+ },
665
+ },
666
+ },
667
+ }
668
+
669
+ _enrich_from_contracts(context, config, project_root)
670
+ return context
671
+
672
+
673
+ def _enrich_from_contracts(
674
+ context: Dict[str, Any],
675
+ config: Dict[str, Any],
676
+ project_root: Path,
677
+ ) -> None:
678
+ """Enrich context sections from contract file (progressive context enrichment).
679
+
680
+ Only creates empty {} placeholders for scanner-owned sections that agents
681
+ need to read. Agent-enriched and mixed sections are NOT pre-created --
682
+ they should only exist when populated with actual data. The exception is
683
+ architecture_overview, which always exists (even empty) because all agent
684
+ contracts reference it.
685
+ """
686
+ try:
687
+ cloud_provider = config.get("cloud_provider", "gcp")
688
+ provider = "gcp" if cloud_provider == "multi-cloud" else cloud_provider
689
+ contract_path = _find_package_root() / "config" / f"context-contracts.{provider}.json"
690
+
691
+ if not contract_path.is_file():
692
+ return
693
+
694
+ contracts = json.loads(contract_path.read_text())
695
+ contract_sections: set = set()
696
+ for agent in (contracts.get("agents") or {}).values():
697
+ for s in agent.get("read", []):
698
+ contract_sections.add(s)
699
+ for s in agent.get("write", []):
700
+ contract_sections.add(s)
701
+
702
+ # Sections that should NOT be pre-created as empty {}.
703
+ # They only exist when an agent or scanner populates them with data.
704
+ # architecture_overview is the exception -- always present.
705
+ from tools.scan.merge import AGENT_ENRICHED_SECTIONS, MIXED_SECTION_SCANNER_FIELDS
706
+ skip_empty = (
707
+ AGENT_ENRICHED_SECTIONS
708
+ | frozenset(MIXED_SECTION_SCANNER_FIELDS.keys())
709
+ ) - {"architecture_overview"}
710
+
711
+ sections = context.setdefault("sections", {})
712
+ for section in contract_sections:
713
+ if section not in sections:
714
+ if section in skip_empty:
715
+ continue
716
+ sections[section] = {}
717
+
718
+ except (json.JSONDecodeError, OSError):
719
+ pass
720
+
721
+
722
+ def _merge_project_context(
723
+ existing: Dict[str, Any],
724
+ new_context: Dict[str, Any],
725
+ ) -> Dict[str, Any]:
726
+ """Merge new context into existing, preserving agent-enriched sections.
727
+
728
+ Strategy:
729
+ - metadata: field-by-field replace from new
730
+ - paths: field-by-field replace from new
731
+ - sections: preserve existing content; add new sections if absent
732
+ """
733
+ import copy
734
+
735
+ merged = {
736
+ "metadata": {
737
+ **(existing.get("metadata") or {}),
738
+ **(new_context.get("metadata") or {}),
739
+ "last_updated": datetime.now(timezone.utc).isoformat(),
740
+ },
741
+ "sections": {
742
+ # Start from new context sections as schema base,
743
+ # then override with existing sections that have content
744
+ **(new_context.get("sections") or {}),
745
+ **{
746
+ k: v
747
+ for k, v in (existing.get("sections") or {}).items()
748
+ if v is not None and isinstance(v, dict) and len(v) > 0
749
+ },
750
+ },
751
+ }
752
+
753
+ return merged