@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,518 @@
1
+ """
2
+ ContextWriter module for gaia-ops progressive context enrichment.
3
+
4
+ Parses CONTEXT_UPDATE blocks from agent output, validates write permissions
5
+ against contracts, and applies deep-merge updates to project-context.json.
6
+
7
+ Public API:
8
+ - parse_context_update(agent_output) -> Optional[dict]
9
+ - validate_permissions(update, agent_type, contracts) -> (dict, list)
10
+ - apply_update(context_path, update, agent_type) -> dict
11
+ - load_contracts(provider, config_dir) -> dict
12
+ - process_agent_output(agent_output, task_info) -> dict
13
+ """
14
+
15
+ import json
16
+ import logging
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Dict, List, Optional
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Dynamic import: deep_merge from tools/context/deep_merge.py
26
+ # ---------------------------------------------------------------------------
27
+ def _import_deep_merge():
28
+ """Import deep_merge function from tools/context/deep_merge.py."""
29
+ import importlib.util
30
+ import sys
31
+
32
+ # Resolve paths relative to this file:
33
+ # this file: hooks/modules/context/context_writer.py
34
+ # deep_merge: tools/context/deep_merge.py
35
+ hooks_dir = Path(__file__).resolve().parents[2] # hooks/
36
+ repo_root = hooks_dir.parent # repo root
37
+ deep_merge_path = repo_root / "tools" / "context" / "deep_merge.py"
38
+
39
+ if not deep_merge_path.exists():
40
+ raise ImportError(f"deep_merge.py not found at {deep_merge_path}")
41
+
42
+ spec = importlib.util.spec_from_file_location("deep_merge", str(deep_merge_path))
43
+ module = importlib.util.module_from_spec(spec)
44
+ sys.modules["deep_merge"] = module
45
+ spec.loader.exec_module(module)
46
+ return module.deep_merge
47
+
48
+
49
+ _deep_merge = _import_deep_merge()
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # LEGACY_AGENT_CONTRACTS (fallback when no contracts file exists)
54
+ # In legacy mode, write permissions = read permissions.
55
+ # ---------------------------------------------------------------------------
56
+ LEGACY_AGENT_CONTRACTS: Dict[str, List[str]] = {
57
+ "terraform-architect": [
58
+ "project_identity",
59
+ "stack",
60
+ "git",
61
+ "environment",
62
+ "infrastructure",
63
+ "terraform_infrastructure",
64
+ "infrastructure_topology",
65
+ "operational_guidelines",
66
+ ],
67
+ "gitops-operator": [
68
+ "project_identity",
69
+ "stack",
70
+ "git",
71
+ "environment",
72
+ "infrastructure",
73
+ "gitops_configuration",
74
+ "infrastructure_topology",
75
+ "cluster_details",
76
+ "operational_guidelines",
77
+ ],
78
+ "cloud-troubleshooter": [
79
+ "project_identity",
80
+ "stack",
81
+ "git",
82
+ "environment",
83
+ "infrastructure",
84
+ "infrastructure_topology",
85
+ "terraform_infrastructure",
86
+ "gitops_configuration",
87
+ "application_services",
88
+ "monitoring_observability",
89
+ "cluster_details",
90
+ ],
91
+ "devops-developer": [
92
+ "project_identity",
93
+ "stack",
94
+ "git",
95
+ "environment",
96
+ "infrastructure",
97
+ "application_services",
98
+ "operational_guidelines",
99
+ ],
100
+ }
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Module-level cache for load_contracts
105
+ # ---------------------------------------------------------------------------
106
+ _contracts_cache: Dict[str, dict] = {}
107
+
108
+
109
+ # ============================================================================
110
+ # 1. parse_context_update
111
+ # ============================================================================
112
+
113
+ def parse_context_update(agent_output: str) -> Optional[dict]:
114
+ """Extract and parse a CONTEXT_UPDATE block from agent output.
115
+
116
+ Searches for the ``CONTEXT_UPDATE:`` marker (case-sensitive, on its own
117
+ line), extracts the JSON that follows until end-of-output or the next
118
+ known marker, and returns the parsed dict.
119
+
120
+ Returns None when:
121
+ - No marker is found
122
+ - The JSON is malformed
123
+ - The parsed value is not a dict
124
+ """
125
+ # Find the CONTEXT_UPDATE: marker on its own line
126
+ marker = "CONTEXT_UPDATE:"
127
+ lines = agent_output.split("\n")
128
+
129
+ marker_idx = None
130
+ for i, line in enumerate(lines):
131
+ if line.strip() == marker:
132
+ marker_idx = i
133
+ break
134
+
135
+ if marker_idx is None:
136
+ return None
137
+
138
+ # Collect all text after the marker
139
+ remaining = "\n".join(lines[marker_idx + 1:]).strip()
140
+
141
+ if not remaining:
142
+ return None
143
+
144
+ # Strip markdown code fences — LLMs reading SKILL.md documentation
145
+ # often wrap the JSON in ```json ... ``` or ``` ... ``` blocks.
146
+ if remaining.startswith("```"):
147
+ fence_lines = remaining.split("\n")
148
+ # Remove opening fence (```json, ```JSON, ```, etc.)
149
+ fence_lines.pop(0)
150
+ # Remove closing fence if present
151
+ for i in range(len(fence_lines) - 1, -1, -1):
152
+ if fence_lines[i].strip() == "```":
153
+ fence_lines.pop(i)
154
+ break
155
+ remaining = "\n".join(fence_lines).strip()
156
+
157
+ if not remaining:
158
+ return None
159
+
160
+ # Use raw_decode to extract the first complete JSON value, ignoring
161
+ # any trailing text (summaries, AGENT_STATUS blocks, etc.)
162
+ decoder = json.JSONDecoder()
163
+ try:
164
+ parsed, _ = decoder.raw_decode(remaining)
165
+ except (json.JSONDecodeError, ValueError) as exc:
166
+ logger.warning("Malformed JSON in CONTEXT_UPDATE block: %s", exc)
167
+ return None
168
+
169
+ if not isinstance(parsed, dict):
170
+ return None
171
+
172
+ return parsed
173
+
174
+
175
+ # ============================================================================
176
+ # 2. validate_permissions
177
+ # ============================================================================
178
+
179
+ def validate_permissions(
180
+ update: dict,
181
+ agent_type: str,
182
+ contracts: dict,
183
+ ) -> tuple:
184
+ """Validate which sections the agent is allowed to write.
185
+
186
+ Returns ``(allowed_updates, rejected_sections)`` where:
187
+ - ``allowed_updates``: dict with only permitted sections
188
+ - ``rejected_sections``: list of section names that were rejected
189
+ """
190
+ allowed: dict = {}
191
+ rejected: List[str] = []
192
+
193
+ # Determine writable sections for this agent
194
+ agents_map = contracts.get("agents", {})
195
+
196
+ if agent_type in agents_map:
197
+ # Use explicit write list from contracts file
198
+ writable = set(agents_map[agent_type].get("write", []))
199
+ elif agent_type in LEGACY_AGENT_CONTRACTS:
200
+ # Fallback: in legacy mode, write = read
201
+ writable = set(LEGACY_AGENT_CONTRACTS[agent_type])
202
+ else:
203
+ # Unknown agent: no permissions
204
+ writable = set()
205
+
206
+ for section, data in update.items():
207
+ if section in writable:
208
+ allowed[section] = data
209
+ else:
210
+ rejected.append(section)
211
+
212
+ return allowed, rejected
213
+
214
+
215
+ # ============================================================================
216
+ # 3. apply_update
217
+ # ============================================================================
218
+
219
+ def apply_update(
220
+ context_path: Path,
221
+ update: dict,
222
+ agent_type: str,
223
+ ) -> dict:
224
+ """Apply a validated update to project-context.json using deep merge.
225
+
226
+ Performs an atomic write (write to .tmp, then rename) and appends an
227
+ audit entry to ``context-audit.jsonl`` in the same directory.
228
+
229
+ Returns an audit entry dict. Never raises on I/O errors.
230
+ """
231
+ context_path = Path(context_path)
232
+ timestamp = datetime.now(timezone.utc).isoformat()
233
+ sections_updated = list(update.keys())
234
+
235
+ audit_entry = {
236
+ "timestamp": timestamp,
237
+ "agent": agent_type,
238
+ "sections_updated": sections_updated,
239
+ "changes": {},
240
+ "success": False,
241
+ "error": None,
242
+ }
243
+
244
+ try:
245
+ # Read current context
246
+ current = json.loads(context_path.read_text())
247
+
248
+ # Deep merge each section
249
+ all_changes = {}
250
+ for section, section_data in update.items():
251
+ current_section = current.get("sections", {}).get(section, {})
252
+ merged_section, diff = _deep_merge(current_section, section_data)
253
+
254
+ # Ensure sections dict exists
255
+ if "sections" not in current:
256
+ current["sections"] = {}
257
+ current["sections"][section] = merged_section
258
+
259
+ if diff:
260
+ all_changes[section] = diff
261
+
262
+ # Update metadata timestamp
263
+ if "metadata" not in current:
264
+ current["metadata"] = {}
265
+ current["metadata"]["last_updated"] = timestamp
266
+
267
+ # Atomic write: write to .tmp, then rename
268
+ tmp_path = context_path.with_suffix(".tmp")
269
+ tmp_path.write_text(json.dumps(current, indent=2))
270
+ tmp_path.rename(context_path)
271
+
272
+ audit_entry["changes"] = all_changes
273
+ audit_entry["success"] = True
274
+
275
+ except Exception as exc:
276
+ logger.error("Failed to apply context update: %s", exc)
277
+ audit_entry["error"] = str(exc)
278
+ audit_entry["success"] = False
279
+ return audit_entry
280
+
281
+ # Append audit entry to context-audit.jsonl
282
+ try:
283
+ audit_file = context_path.parent / "context-audit.jsonl"
284
+ with open(audit_file, "a") as f:
285
+ f.write(json.dumps(audit_entry) + "\n")
286
+ except Exception as exc:
287
+ logger.warning("Failed to write audit entry: %s", exc)
288
+ # Audit write failure doesn't affect the main result
289
+
290
+ return audit_entry
291
+
292
+
293
+ # ============================================================================
294
+ # 4. load_contracts
295
+ # ============================================================================
296
+
297
+ def load_contracts(provider: str, config_dir: Path) -> dict:
298
+ """Load agent contracts using the base+cloud merge strategy, with caching.
299
+
300
+ Strategy (in priority order):
301
+ 1. Load base contracts from context-contracts.json (cloud-agnostic)
302
+ 2. Merge cloud overrides from cloud/{provider}.json (extend read/write lists)
303
+ 3. Fallback: try legacy context-contracts.{provider}.json
304
+ 4. Final fallback: LEGACY_AGENT_CONTRACTS hardcoded dict (write = read)
305
+
306
+ Results are cached by provider string.
307
+ """
308
+ config_dir = Path(config_dir)
309
+
310
+ # Check cache first
311
+ if provider in _contracts_cache:
312
+ return _contracts_cache[provider]
313
+
314
+ result = _merge_base_and_cloud(provider, config_dir)
315
+ _contracts_cache[provider] = result
316
+ return result
317
+
318
+
319
+ def _merge_base_and_cloud(provider: str, config_dir: Path) -> dict:
320
+ """Load and merge base + cloud/{provider}.json contracts.
321
+
322
+ Returns a merged contracts dict with 'agents' keyed by agent name,
323
+ each containing 'read' and 'write' lists.
324
+ """
325
+ base_file = config_dir / "context-contracts.json"
326
+ cloud_file = config_dir / "cloud" / f"{provider}.json"
327
+
328
+ # Step 1: Load base contracts
329
+ base_contracts = None
330
+ if base_file.exists():
331
+ try:
332
+ base_contracts = json.loads(base_file.read_text())
333
+ except (json.JSONDecodeError, OSError) as exc:
334
+ logger.warning("Failed to load base contracts from %s: %s", base_file, exc)
335
+
336
+ # Step 2: Final fallback to hardcoded LEGACY_AGENT_CONTRACTS
337
+ if base_contracts is None:
338
+ logger.warning("No contract files found in %s, using hardcoded legacy contracts", config_dir)
339
+ return {
340
+ "version": "legacy",
341
+ "provider": provider,
342
+ "agents": {
343
+ agent: {"read": sections, "write": list(sections)}
344
+ for agent, sections in LEGACY_AGENT_CONTRACTS.items()
345
+ },
346
+ }
347
+
348
+ # Step 4: Merge cloud-specific overrides
349
+ if cloud_file.exists():
350
+ try:
351
+ cloud_overrides = json.loads(cloud_file.read_text())
352
+ for agent_name, agent_overrides in cloud_overrides.get("agents", {}).items():
353
+ if agent_name in base_contracts.get("agents", {}):
354
+ existing_read = base_contracts["agents"][agent_name].get("read", [])
355
+ existing_write = base_contracts["agents"][agent_name].get("write", [])
356
+ extra_read = [s for s in agent_overrides.get("read", []) if s not in existing_read]
357
+ extra_write = [s for s in agent_overrides.get("write", []) if s not in existing_write]
358
+ base_contracts["agents"][agent_name]["read"] = existing_read + extra_read
359
+ base_contracts["agents"][agent_name]["write"] = existing_write + extra_write
360
+ else:
361
+ base_contracts["agents"][agent_name] = agent_overrides
362
+ logger.info("Merged %s cloud overrides from %s", provider.upper(), cloud_file)
363
+ except (json.JSONDecodeError, OSError) as exc:
364
+ logger.warning("Failed to load cloud overrides from %s: %s — skipping", cloud_file, exc)
365
+
366
+ return base_contracts
367
+
368
+
369
+ # ============================================================================
370
+ # 5. process_agent_output
371
+ # ============================================================================
372
+
373
+ def process_agent_output(agent_output: str, task_info: dict) -> dict:
374
+ """Orchestrate the full context-update flow.
375
+
376
+ Steps: parse -> detect provider -> load contracts -> validate -> apply.
377
+
378
+ Parameters
379
+ ----------
380
+ agent_output : str
381
+ Full agent output string.
382
+ task_info : dict
383
+ Must contain: ``agent_type``, ``context_path``, ``config_dir``.
384
+
385
+ Returns
386
+ -------
387
+ dict
388
+ ``{updated, sections/sections_updated, rejected, error}``
389
+ """
390
+ result = {
391
+ "updated": False,
392
+ "sections_updated": [],
393
+ "rejected": [],
394
+ "error": None,
395
+ }
396
+
397
+ # 1. Parse CONTEXT_UPDATE
398
+ update = parse_context_update(agent_output)
399
+ if update is None:
400
+ return result
401
+
402
+ agent_type = task_info.get("agent_type", "unknown")
403
+ context_path = Path(task_info.get("context_path", ""))
404
+ config_dir = Path(task_info.get("config_dir", ""))
405
+
406
+ # 2. Detect cloud provider from existing context metadata
407
+ provider = "gcp" # default
408
+ try:
409
+ if context_path.exists():
410
+ ctx = json.loads(context_path.read_text())
411
+ provider = ctx.get("metadata", {}).get("cloud_provider", "gcp").lower()
412
+ except Exception:
413
+ pass
414
+
415
+ # 3. Load contracts
416
+ # Clear cache to avoid cross-test pollution (keyed by provider+config_dir)
417
+ cache_key = f"{provider}:{config_dir}"
418
+ contracts = _load_contracts_with_dir(provider, config_dir)
419
+
420
+ # 4. Validate permissions
421
+ allowed, rejected = validate_permissions(update, agent_type, contracts)
422
+ result["rejected"] = rejected
423
+
424
+ # 5. If nothing allowed, return early
425
+ if not allowed:
426
+ return result
427
+
428
+ # 6. Apply update
429
+ audit = apply_update(context_path, allowed, agent_type)
430
+
431
+ if audit.get("success"):
432
+ result["updated"] = True
433
+ result["sections_updated"] = list(allowed.keys())
434
+ else:
435
+ result["error"] = audit.get("error")
436
+
437
+ return result
438
+
439
+
440
+ def _load_contracts_with_dir(provider: str, config_dir: Path) -> dict:
441
+ """Load contracts, bypassing the module-level cache for process_agent_output.
442
+
443
+ This ensures each call with a different config_dir gets fresh results
444
+ while still allowing load_contracts to cache for repeated calls with
445
+ the same provider. Uses the same base+cloud merge strategy as load_contracts.
446
+ """
447
+ return _merge_base_and_cloud(provider, Path(config_dir))
448
+
449
+
450
+ # ============================================================================
451
+ # 6. process_context_updates (thin wrapper for subagent_stop integration)
452
+ # ============================================================================
453
+
454
+ def process_context_updates(
455
+ agent_output: str,
456
+ task_info: dict,
457
+ find_claude_dir_fn=None,
458
+ ) -> Optional[dict]:
459
+ """
460
+ Process CONTEXT_UPDATE blocks from agent output via context_writer.
461
+
462
+ Loads project-context.json, resolves config_dir from .claude, and calls
463
+ process_agent_output() to apply progressive enrichment.
464
+
465
+ This function MUST NOT break the existing hook flow -- all errors are caught
466
+ and logged, returning None on failure.
467
+
468
+ Args:
469
+ agent_output: Complete output from agent execution
470
+ task_info: Task metadata (agent, description, task_id)
471
+ find_claude_dir_fn: Callable that returns the .claude Path. Defaults to
472
+ modules.core.paths.find_claude_dir if not provided.
473
+
474
+ Returns:
475
+ Result dict from process_agent_output, or None on error
476
+ """
477
+ try:
478
+ if find_claude_dir_fn is None:
479
+ from ..core.paths import find_claude_dir
480
+ find_claude_dir_fn = find_claude_dir
481
+
482
+ # Find project-context.json via find_claude_dir
483
+ claude_dir = find_claude_dir_fn()
484
+ context_path = claude_dir / "project-context" / "project-context.json"
485
+
486
+ if not context_path.exists():
487
+ logger.debug("project-context.json not found at %s, skipping context updates", context_path)
488
+ return None
489
+
490
+ # Determine config_dir (inside .claude directory)
491
+ config_dir = claude_dir / "config"
492
+
493
+ # Build task_info dict for process_agent_output
494
+ agent_type = task_info.get("agent", "unknown")
495
+ task_info_for_writer = {
496
+ "agent_type": agent_type,
497
+ "context_path": str(context_path),
498
+ "config_dir": str(config_dir),
499
+ }
500
+
501
+ result = process_agent_output(agent_output, task_info_for_writer)
502
+
503
+ if result and result.get("updated"):
504
+ logger.info(
505
+ "Context updated by %s: sections=%s",
506
+ agent_type, result.get("sections_updated", []),
507
+ )
508
+ if result and result.get("rejected"):
509
+ logger.debug(
510
+ "Context sections rejected for %s: %s",
511
+ agent_type, result.get("rejected", []),
512
+ )
513
+
514
+ return result
515
+
516
+ except Exception as e:
517
+ logger.debug("Context update processing failed (non-fatal): %s", e)
518
+ return None
@@ -0,0 +1,161 @@
1
+ """Load context contracts and merge agent permissions.
2
+
3
+ Subsystem 2 of the pre_tool_use Task/Agent path.
4
+
5
+ Loads context-contracts.json + cloud overlays, merges agent permissions,
6
+ finds empty writable sections, and builds a reminder string.
7
+
8
+ Cloud provider detection (formerly infrastructure_reader) is internal
9
+ to the contract loading process.
10
+ """
11
+
12
+ import json
13
+ import logging
14
+ from pathlib import Path
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _detect_cloud_from_infrastructure(sections: dict) -> str:
20
+ """Extract cloud provider name from v2 infrastructure.cloud_providers section.
21
+
22
+ Args:
23
+ sections: The sections dict from project-context.json.
24
+
25
+ Returns:
26
+ Cloud provider name string (e.g. aws, gcp) or empty string.
27
+ """
28
+ infra = sections.get("infrastructure", {})
29
+ if isinstance(infra, dict):
30
+ providers = infra.get("cloud_providers", [])
31
+ if isinstance(providers, list) and providers:
32
+ primary = providers[0]
33
+ if isinstance(primary, dict):
34
+ return primary.get("name", "")
35
+ return ""
36
+
37
+
38
+ def build_context_update_reminder(
39
+ subagent_type: str,
40
+ project_agents: list,
41
+ hooks_dir: Path = None,
42
+ ) -> str:
43
+ """Check which writable sections are empty and build a reminder.
44
+
45
+ Reads the context contracts to find writable sections for this agent,
46
+ then checks project-context.json to see which are empty.
47
+
48
+ Args:
49
+ subagent_type: The agent type string (e.g. devops-developer).
50
+ project_agents: List of valid project agent names.
51
+ hooks_dir: Path to the hooks directory (for fallback config lookup).
52
+ Defaults to Path(__file__).parent.parent.parent if None.
53
+
54
+ Returns:
55
+ Reminder string or empty string if no empty sections.
56
+ """
57
+ if subagent_type not in project_agents:
58
+ return ""
59
+
60
+ if hooks_dir is None:
61
+ hooks_dir = Path(__file__).parent.parent.parent
62
+
63
+ # Load contracts to find writable sections.
64
+ # Strategy: load context-contracts.json (base) then merge cloud/{provider}.json.
65
+ # Fallback to legacy per-provider files for backward compatibility.
66
+ # We detect the cloud provider from project-context.json first.
67
+ cloud_provider = "gcp" # default
68
+ pc_paths_for_provider = [
69
+ Path(".claude/project-context/project-context.json"),
70
+ Path("project-context.json"),
71
+ ]
72
+ for pp in pc_paths_for_provider:
73
+ if pp.exists():
74
+ try:
75
+ pc_data = json.loads(pp.read_text())
76
+ detected = (
77
+ pc_data.get("metadata", {}).get("cloud_provider", "")
78
+ or _detect_cloud_from_infrastructure(pc_data.get("sections", {}))
79
+ )
80
+ if detected:
81
+ cloud_provider = detected.lower()
82
+ break
83
+ except Exception:
84
+ continue
85
+
86
+ # Candidate config directories (installed project first, package fallback)
87
+ config_dirs = [
88
+ Path(".claude/config"),
89
+ hooks_dir.parent / "config",
90
+ ]
91
+
92
+ writable = []
93
+ for config_dir in config_dirs:
94
+ if not config_dir.is_dir():
95
+ continue
96
+ # Load base contracts
97
+ base_file = config_dir / "context-contracts.json"
98
+ cloud_file = config_dir / "cloud" / f"{cloud_provider}.json"
99
+
100
+ merged_agents = {}
101
+ if base_file.exists():
102
+ try:
103
+ data = json.loads(base_file.read_text())
104
+ merged_agents = data.get("agents", {})
105
+ except Exception:
106
+ pass
107
+
108
+ # Merge cloud overrides
109
+ if merged_agents and cloud_file.exists():
110
+ try:
111
+ cloud_data = json.loads(cloud_file.read_text())
112
+ agent_cloud = cloud_data.get("agents", {}).get(subagent_type, {})
113
+ base_write = merged_agents.get(subagent_type, {}).get("write", [])
114
+ extra_write = [s for s in agent_cloud.get("write", []) if s not in base_write]
115
+ if subagent_type in merged_agents:
116
+ merged_agents[subagent_type]["write"] = base_write + extra_write
117
+ except Exception:
118
+ pass
119
+
120
+ if merged_agents:
121
+ agent_perms = merged_agents.get(subagent_type, {})
122
+ writable = agent_perms.get("write", [])
123
+ if writable:
124
+ break
125
+
126
+ if not writable:
127
+ return ""
128
+
129
+ # Load project-context.json to find empty sections
130
+ pc_paths = [
131
+ Path(".claude/project-context/project-context.json"),
132
+ Path("project-context.json"),
133
+ ]
134
+
135
+ sections = {}
136
+ for pp in pc_paths:
137
+ if pp.exists():
138
+ try:
139
+ pc = json.loads(pp.read_text())
140
+ sections = pc.get("sections", {})
141
+ break
142
+ except Exception:
143
+ continue
144
+
145
+ # Find empty writable sections
146
+ empty = []
147
+ for section_name in writable:
148
+ section_data = sections.get(section_name, {})
149
+ if not section_data or section_data == {}:
150
+ empty.append(section_name)
151
+
152
+ if not empty:
153
+ return ""
154
+
155
+ empty_list = ", ".join(f"`{s}`" for s in empty)
156
+ return (
157
+ f"\n**CONTEXT_UPDATE REQUIRED:** Your writable sections {empty_list} "
158
+ f"are currently EMPTY. After completing your task, you MUST emit a "
159
+ f"CONTEXT_UPDATE block with any data you discovered. "
160
+ f"See \"Context Updater Protocol\" above for the format.\n\n"
161
+ )