@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
@@ -18,7 +18,7 @@ import os
18
18
  import re
19
19
  import time
20
20
  from pathlib import Path
21
- from typing import Any, Dict, Optional
21
+ from typing import Any, Dict, List, Optional
22
22
 
23
23
  from .base import HookAdapter
24
24
  from .types import (
@@ -49,7 +49,7 @@ class ClaudeCodeAdapter(HookAdapter):
49
49
  - session_id: str
50
50
  - tool_name: str (PreToolUse / PostToolUse)
51
51
  - tool_input: dict (PreToolUse / PostToolUse)
52
- - tool_result: dict (PostToolUse only)
52
+ - tool_response: dict (PostToolUse only)
53
53
  - agent_type: str (SubagentStop only)
54
54
  - agent_id: str (SubagentStop only)
55
55
  - agent_transcript_path: str (SubagentStop only)
@@ -177,16 +177,23 @@ class ClaudeCodeAdapter(HookAdapter):
177
177
  # ------------------------------------------------------------------ #
178
178
 
179
179
  def format_context_response(self, result: ContextResult) -> HookResponse:
180
- """Format a ContextResult for context injection hooks.
180
+ """Format a ContextResult for SubagentStart context injection.
181
181
 
182
- Returns additionalContext field that Claude Code appends to the prompt.
183
- Note: UserPromptSubmit was simplified to a static echo in settings.json.
184
- This method is retained for SubagentStart and future context injection hooks.
182
+ Claude Code expects SubagentStart hooks to return::
183
+
184
+ {"hookSpecificOutput": {"hookEventName": "SubagentStart",
185
+ "additionalContext": "..."}}
186
+
187
+ The additionalContext string is appended to the subagent's system prompt.
185
188
  """
186
- output: Dict[str, Any] = {}
189
+ hook_specific: Dict[str, Any] = {
190
+ "hookEventName": "SubagentStart",
191
+ }
187
192
 
188
193
  if result.context_injected and result.additional_context:
189
- output["additionalContext"] = result.additional_context
194
+ hook_specific["additionalContext"] = result.additional_context
195
+
196
+ output: Dict[str, Any] = {"hookSpecificOutput": hook_specific}
190
197
 
191
198
  if result.sections_provided:
192
199
  output["sections_provided"] = result.sections_provided
@@ -324,12 +331,12 @@ class ClaudeCodeAdapter(HookAdapter):
324
331
  """
325
332
  tool_name = raw.get("tool_name", "")
326
333
  tool_input = raw.get("tool_input", {})
327
- tool_result = raw.get("tool_result", {})
334
+ tool_response = raw.get("tool_response", {})
328
335
  session_id = raw.get("session_id", "")
329
336
 
330
337
  command = tool_input.get("command", "")
331
- output = tool_result.get("output", "")
332
- exit_code = tool_result.get("exit_code", 0)
338
+ output = tool_response.get("output", "")
339
+ exit_code = tool_response.get("exit_code", 0)
333
340
 
334
341
  return ToolResult(
335
342
  tool_name=tool_name,
@@ -407,24 +414,11 @@ class ClaudeCodeAdapter(HookAdapter):
407
414
  context injection, approval handling, and response formatting.
408
415
  """
409
416
  from modules.core.state import create_pre_hook_state, save_hook_state
410
- from modules.security.approval_constants import (
411
- NONCE_APPROVAL_PATTERN,
412
- )
413
- from modules.security.approval_messages import (
414
- build_activation_failed_message,
415
- build_deprecated_approval_message,
416
- build_invalid_nonce_message,
417
- )
418
417
  from modules.security.approval_grants import (
419
- activate_pending_approval,
420
418
  cleanup_expired_grants,
421
419
  )
422
420
  from modules.tools.bash_validator import BashValidator
423
421
  from modules.tools.task_validator import TaskValidator, AVAILABLE_AGENTS, META_AGENTS
424
- from modules.security.prompt_validator import classify_resume_prompt
425
- from modules.context.context_injector import inject_project_context
426
- from modules.session.session_event_injector import inject_session_events
427
-
428
422
  hook_data = event.payload
429
423
  tool_name = hook_data.get("tool_name") or ""
430
424
  tool_input = hook_data.get("tool_input", {})
@@ -432,6 +426,28 @@ class ClaudeCodeAdapter(HookAdapter):
432
426
  logger.info("Hook invoked: tool=%s, params=%s", tool_name, json.dumps(tool_input)[:200])
433
427
 
434
428
  try:
429
+ # ── Delegate mode gate ─────────────────────────────────
430
+ # Must run before any other logic. When enabled, the
431
+ # orchestrator (main session) is restricted to dispatch-only
432
+ # tools. Subagents are unaffected.
433
+ from modules.orchestrator.delegate_mode import check_delegate_mode
434
+
435
+ dm_result = check_delegate_mode(tool_name, hook_data)
436
+ if dm_result.blocked:
437
+ logger.warning(
438
+ "DELEGATE_MODE denied %s for orchestrator", tool_name,
439
+ )
440
+ return HookResponse(
441
+ output={
442
+ "hookSpecificOutput": {
443
+ "hookEventName": "PreToolUse",
444
+ "permissionDecision": "deny",
445
+ "permissionDecisionReason": dm_result.reason,
446
+ }
447
+ },
448
+ exit_code=0,
449
+ )
450
+
435
451
  # Periodic cleanup of expired approval grants
436
452
  cleanup_expired_grants()
437
453
 
@@ -441,12 +457,13 @@ class ClaudeCodeAdapter(HookAdapter):
441
457
  return HookResponse(output="Error: Invalid parameters", exit_code=2)
442
458
 
443
459
  if tool_name.lower() == "bash":
444
- return self._adapt_bash(tool_name, tool_input)
460
+ return self._adapt_bash(tool_name, tool_input, hook_data=hook_data)
445
461
  elif tool_name.lower() in ("task", "agent"):
446
462
  hooks_dir = Path(__file__).parent.parent
447
463
  project_agents = [a for a in AVAILABLE_AGENTS if a not in META_AGENTS]
448
464
  return self._adapt_task(
449
465
  tool_name, tool_input, project_agents, hooks_dir,
466
+ session_id=event.session_id,
450
467
  )
451
468
  elif tool_name.lower() == "sendmessage":
452
469
  return self._adapt_send_message(tool_name, tool_input)
@@ -461,8 +478,20 @@ class ClaudeCodeAdapter(HookAdapter):
461
478
  exit_code=2,
462
479
  )
463
480
 
464
- def _adapt_bash(self, tool_name: str, parameters: dict) -> HookResponse:
465
- """Handle Bash tool validation within the adapter."""
481
+ def _adapt_bash(
482
+ self,
483
+ tool_name: str,
484
+ parameters: dict,
485
+ hook_data: dict | None = None,
486
+ ) -> HookResponse:
487
+ """Handle Bash tool validation within the adapter.
488
+
489
+ Args:
490
+ tool_name: The tool name ("Bash").
491
+ parameters: The tool_input dict (contains "command").
492
+ hook_data: Full hook event payload -- used to detect subagent
493
+ context via the ``agent_id`` field.
494
+ """
466
495
  from modules.core.state import create_pre_hook_state, save_hook_state
467
496
  from modules.tools.bash_validator import BashValidator
468
497
 
@@ -470,8 +499,15 @@ class ClaudeCodeAdapter(HookAdapter):
470
499
  if not command:
471
500
  return HookResponse(output="Error: Bash tool requires a command", exit_code=2)
472
501
 
502
+ # Detect subagent context: if agent_id is present in the hook event,
503
+ # the command is running inside a subagent (not the orchestrator).
504
+ is_subagent = bool(hook_data and hook_data.get("agent_id"))
505
+ session_id = (hook_data or {}).get("session_id", "")
506
+
473
507
  validator = BashValidator()
474
- result = validator.validate(command)
508
+ result = validator.validate(
509
+ command, is_subagent=is_subagent, session_id=session_id,
510
+ )
475
511
 
476
512
  if not result.allowed:
477
513
  from modules.core.plugin_mode import is_ops_mode
@@ -542,11 +578,13 @@ class ClaudeCodeAdapter(HookAdapter):
542
578
  parameters: dict,
543
579
  project_agents: list,
544
580
  hooks_dir: Path,
581
+ session_id: str = "",
545
582
  ) -> HookResponse:
546
583
  """Handle Task/Agent tool validation within the adapter.
547
584
 
548
- Uses additionalContext (Phase 2) instead of prompt mutation.
549
- Validation runs against the original prompt, eliminating T3 false positives.
585
+ Builds project context and caches it for SubagentStart to forward.
586
+ PreToolUse no longer returns additionalContext directly -- that would
587
+ inject it into the orchestrator instead of the subagent.
550
588
  """
551
589
  from modules.core.state import create_pre_hook_state, save_hook_state
552
590
  from modules.tools.task_validator import TaskValidator
@@ -575,18 +613,48 @@ class ClaudeCodeAdapter(HookAdapter):
575
613
 
576
614
  logger.info("ALLOWED Task: %s", result.agent_name)
577
615
 
616
+ # Cache context for SubagentStart to pick up and forward to the subagent.
617
+ # PreToolUse:Agent additionalContext goes to the orchestrator (wrong target).
578
618
  additional = "\n".join(filter(None, [context_text, events_text]))
619
+
620
+ # Fallback: if build_project_context returned None because the
621
+ # orchestrator already embedded context in the prompt (dedup guard),
622
+ # extract the embedded context so SubagentStart can still inject it
623
+ # via additionalContext.
624
+ if not additional:
625
+ prompt = parameters.get("prompt", "")
626
+ marker = "# Project Context"
627
+ if marker in prompt:
628
+ # Extract everything from the marker onwards as context.
629
+ # The orchestrator copied its own injected context into the
630
+ # Agent tool prompt; we forward it to SubagentStart instead.
631
+ idx = prompt.index(marker)
632
+ additional = prompt[idx:]
633
+ logger.info(
634
+ "Extracted embedded context from prompt for caching "
635
+ "(len=%d, agent=%s)",
636
+ len(additional), result.agent_name,
637
+ )
638
+
579
639
  if additional:
580
- logger.info("Returning additionalContext for %s (context injected)", result.agent_name)
581
- output = {
582
- "hookSpecificOutput": {
583
- "hookEventName": "PreToolUse",
584
- "permissionDecision": "allow",
585
- "permissionDecisionReason": f"Context injected for {result.agent_name}",
586
- "additionalContext": additional,
587
- }
588
- }
589
- return HookResponse(output=output, exit_code=0)
640
+ effective_session_id = session_id or "unknown"
641
+ agent_type = result.agent_name or "unknown"
642
+ self._cache_context_for_subagent(effective_session_id, agent_type, additional)
643
+ logger.info(
644
+ "Cached context for SubagentStart: agent=%s, session=%s",
645
+ agent_type, effective_session_id,
646
+ )
647
+
648
+ # Write AGENT_DISPATCH event (non-blocking)
649
+ try:
650
+ from modules.events.event_writer import EventWriter, AGENT_DISPATCH
651
+ prompt = parameters.get("prompt", "")
652
+ EventWriter().write_event(
653
+ AGENT_DISPATCH, "hook", result.agent_name or "unknown",
654
+ f"dispatched for: {prompt[:100]}",
655
+ )
656
+ except Exception:
657
+ pass # Events are non-critical
590
658
 
591
659
  return HookResponse(output={}, exit_code=0)
592
660
 
@@ -595,8 +663,9 @@ class ClaudeCodeAdapter(HookAdapter):
595
663
  ) -> HookResponse:
596
664
  """Handle SendMessage tool validation for agent resumption.
597
665
 
598
- Validates agent ID format and message content, then runs nonce
599
- approval checks. Does NOT inject project context (it's a resume).
666
+ Validates agent ID format and message content. Does NOT inject
667
+ project context (it's a resume). Nonce relay is no longer processed
668
+ here -- approval grants are activated by the UserPromptSubmit hook.
600
669
  """
601
670
  from modules.core.state import create_pre_hook_state, save_hook_state
602
671
 
@@ -630,74 +699,19 @@ class ClaudeCodeAdapter(HookAdapter):
630
699
 
631
700
  logger.info("SENDMESSAGE: Resuming agent %s", agent_id)
632
701
 
633
- approval_error, has_approval = self._adapt_resume_approval(agent_id, message)
634
- if approval_error:
635
- return HookResponse(output=approval_error, exit_code=2)
636
-
637
702
  state = create_pre_hook_state(
638
703
  tool_name=tool_name,
639
704
  command=f"SendMessage:{agent_id}",
640
705
  tier="T0",
641
706
  allowed=True,
642
707
  is_t3=False,
643
- has_approval=has_approval,
708
+ has_approval=False,
644
709
  )
645
710
  save_hook_state(state)
646
711
 
647
712
  logger.info("ALLOWED SendMessage: agent %s - message length: %d", agent_id, len(message))
648
713
  return HookResponse(output={}, exit_code=0)
649
714
 
650
- def _adapt_resume_approval(
651
- self, resume_id: str, prompt: str,
652
- ) -> tuple[str | None, bool]:
653
- """Process nonce approval indicators for Task resume."""
654
- from modules.security.approval_constants import NONCE_APPROVAL_PATTERN
655
- from modules.security.approval_messages import (
656
- build_activation_failed_message,
657
- build_deprecated_approval_message,
658
- build_invalid_nonce_message,
659
- )
660
- from modules.security.approval_grants import activate_pending_approval
661
- from modules.security.prompt_validator import classify_resume_prompt
662
-
663
- classification = classify_resume_prompt(prompt)
664
-
665
- if classification == "nonce":
666
- nonce = NONCE_APPROVAL_PATTERN.search(prompt).group(1)
667
- activation = activate_pending_approval(nonce)
668
- status_text = getattr(activation.status, "value", str(activation.status))
669
- if activation.success:
670
- grant_path = activation.grant_path
671
- grant_name = grant_path.name if grant_path else "<unknown>"
672
- logger.info(
673
- "Nonce approval activated for resume %s: nonce=%s, file=%s",
674
- resume_id, nonce, grant_name,
675
- )
676
- return None, True
677
-
678
- logger.warning(
679
- "Denied resume %s: nonce approval activation failed for nonce=%s "
680
- "(status=%s, reason=%s)",
681
- resume_id, nonce, status_text, activation.reason,
682
- )
683
- return build_activation_failed_message(nonce, status_text, activation.reason), False
684
-
685
- if classification == "malformed_nonce":
686
- logger.warning(
687
- "Denied resume %s: malformed nonce approval token in prompt='%s'",
688
- resume_id, prompt[:120],
689
- )
690
- return build_invalid_nonce_message(), False
691
-
692
- if classification == "deprecated":
693
- logger.warning(
694
- "Denied resume %s: deprecated legacy approval phrase detected",
695
- resume_id,
696
- )
697
- return build_deprecated_approval_message(), False
698
-
699
- return None, False
700
-
701
715
  @staticmethod
702
716
  def _format_blocked_message(result) -> str:
703
717
  """Format blocked command message. Delegates to blocked_message_formatter."""
@@ -713,25 +727,32 @@ class ClaudeCodeAdapter(HookAdapter):
713
727
 
714
728
  Orchestrates: state retrieval, duration computation, audit logging,
715
729
  T3 grant confirmation, critical event detection, session context
716
- writing, and state cleanup.
730
+ writing, state cleanup, and AskUserQuestion grant activation.
717
731
  """
718
732
  from modules.core.state import get_hook_state, clear_hook_state
719
733
  from modules.audit.logger import log_execution
720
734
  from modules.audit.event_detector import detect_critical_event
721
735
  from modules.session.session_context_writer import SessionContextWriter
722
- from modules.security.approval_grants import check_approval_grant, confirm_grant
736
+ from modules.security.approval_grants import check_approval_grant, confirm_grant, consume_grant
723
737
 
724
738
  hook_data = event.payload
725
739
  tool_result_data = self.parse_post_tool_use(hook_data)
726
740
  logger.info("Post-hook event: %s", hook_data.get("hook_event_name"))
727
741
 
728
- raw_tool_result = hook_data.get("tool_result", {})
742
+ raw_tool_response = hook_data.get("tool_response", {})
729
743
  tool_name = tool_result_data.tool_name
730
744
  parameters = hook_data.get("tool_input", {})
731
745
  output = tool_result_data.output
732
- duration = raw_tool_result.get("duration_ms", 0) / 1000.0
746
+ duration = raw_tool_response.get("duration_ms", 0) / 1000.0
733
747
  success = tool_result_data.exit_code == 0
734
748
 
749
+ # ------------------------------------------------------------- #
750
+ # AskUserQuestion: check if user approved a pending T3 grant
751
+ # ------------------------------------------------------------- #
752
+ if tool_name == "AskUserQuestion":
753
+ self._handle_ask_user_question_result(hook_data)
754
+ return HookResponse(output={}, exit_code=0)
755
+
735
756
  try:
736
757
  pre_state = get_hook_state()
737
758
  tier = pre_state.tier if pre_state else "unknown"
@@ -753,12 +774,14 @@ class ClaudeCodeAdapter(HookAdapter):
753
774
  # Confirm unconfirmed T3 grants after successful Bash execution
754
775
  if tool_name == "Bash" and success:
755
776
  command = parameters.get("command", "")
777
+ session_id = hook_data.get("session_id", "")
756
778
  if command:
757
- grant = check_approval_grant(command)
779
+ grant = check_approval_grant(command, session_id=session_id)
758
780
  if grant is not None and not grant.confirmed:
759
- confirm_grant(command)
781
+ confirm_grant(command, session_id=session_id)
782
+ consume_grant(command, session_id=session_id) # Single-use: mark as consumed
760
783
  logger.info(
761
- "T3 grant confirmed post-execution: %s", command[:80],
784
+ "T3 grant confirmed and consumed post-execution: %s", command[:80],
762
785
  )
763
786
 
764
787
  events = detect_critical_event(tool_name, parameters, output, success)
@@ -767,6 +790,20 @@ class ClaudeCodeAdapter(HookAdapter):
767
790
  for evt in events:
768
791
  writer.update_context(evt.to_dict())
769
792
 
793
+ # Write COMMAND_EXECUTED event for T2+ Bash commands only (non-blocking)
794
+ if tool_name == "Bash" and tier in ("T2", "T3"):
795
+ try:
796
+ from modules.events.event_writer import EventWriter, COMMAND_EXECUTED
797
+ cmd = parameters.get("command", "")
798
+ EventWriter().write_event(
799
+ COMMAND_EXECUTED, "hook", "",
800
+ f"{'ok' if success else 'error'}: {cmd[:120]}",
801
+ severity="info" if success else "warning",
802
+ meta={"tier": tier},
803
+ )
804
+ except Exception:
805
+ pass # Events are non-critical
806
+
770
807
  clear_hook_state()
771
808
  logger.debug("Post-hook completed for %s", tool_name)
772
809
 
@@ -775,6 +812,77 @@ class ClaudeCodeAdapter(HookAdapter):
775
812
 
776
813
  return HookResponse(output={}, exit_code=0)
777
814
 
815
+ # ------------------------------------------------------------------ #
816
+ # _handle_ask_user_question_result: grant activation from user answer
817
+ # ------------------------------------------------------------------ #
818
+
819
+ def _handle_ask_user_question_result(self, hook_data: Dict[str, Any]) -> None:
820
+ """Conditionally activate pending grants based on user's answer.
821
+
822
+ Inspects the answers dict from tool_response (or tool_input as fallback)
823
+ to determine if the user approved. Only activates grants when at least
824
+ one answer value contains "approve" (case-insensitive).
825
+
826
+ Never blocks (no exceptions raised to caller).
827
+ """
828
+ import json as _json
829
+ from modules.security.approval_grants import (
830
+ activate_grants_for_session,
831
+ get_pending_approvals_for_session,
832
+ )
833
+
834
+ session_id = hook_data.get("session_id", "") or os.environ.get("CLAUDE_SESSION_ID", "")
835
+
836
+ # Debug logging
837
+ tool_response = hook_data.get("tool_response", {})
838
+ logger.info("AskUserQuestion PostToolUse keys: %s", list(hook_data.keys()))
839
+ logger.info("AskUserQuestion tool_response: %s", _json.dumps(tool_response, default=str)[:500])
840
+
841
+ # Extract answers from tool_response first, then tool_input as fallback
842
+ answers = {}
843
+ if isinstance(tool_response, dict):
844
+ answers = tool_response.get("answers", {})
845
+ if not answers and isinstance(hook_data.get("tool_input", {}), dict):
846
+ answers = hook_data.get("tool_input", {}).get("answers", {})
847
+
848
+ if not answers:
849
+ logger.info("AskUserQuestion: no answers found in payload, skipping grant activation")
850
+ return
851
+
852
+ # Check if any answer contains "approve"
853
+ user_approved = any("approve" in str(v).lower() for v in answers.values())
854
+
855
+ if not user_approved:
856
+ logger.info(
857
+ "AskUserQuestion: user did not approve (answers: %s), skipping grant activation",
858
+ {k: v for k, v in answers.items()},
859
+ )
860
+ return
861
+
862
+ # User approved -- activate grants
863
+ logger.info("AskUserQuestion: user approved, activating grants for session %s", session_id[:12])
864
+
865
+ try:
866
+ if not session_id:
867
+ logger.info("AskUserQuestion: no session_id available, skipping grant activation")
868
+ return
869
+
870
+ # Check for pending approvals before activating
871
+ pending = get_pending_approvals_for_session(session_id)
872
+ if not pending:
873
+ logger.info("AskUserQuestion: no pending grants for session %s", session_id)
874
+ return
875
+
876
+ results = activate_grants_for_session(session_id)
877
+ activated = sum(1 for r in results if r.success)
878
+ logger.info(
879
+ "AskUserQuestion activated %d/%d pending grants for session %s",
880
+ activated, len(results), session_id,
881
+ )
882
+
883
+ except Exception as e:
884
+ logger.error("Error in _handle_ask_user_question_result: %s", e, exc_info=True)
885
+
778
886
  # ------------------------------------------------------------------ #
779
887
  # adapt_subagent_stop: full subagent-stop lifecycle
780
888
  # ------------------------------------------------------------------ #
@@ -792,7 +900,6 @@ class ClaudeCodeAdapter(HookAdapter):
792
900
  requires_consolidation_report,
793
901
  validate as validate_contract,
794
902
  validate_approval_request,
795
- validate_awaiting_approval_has_nonce,
796
903
  validate_verbatim_outputs_consistency,
797
904
  )
798
905
  from modules.agents.response_contract import (
@@ -986,6 +1093,24 @@ class ClaudeCodeAdapter(HookAdapter):
986
1093
  commands_executed=commands_executed,
987
1094
  )
988
1095
 
1096
+ # Write AGENT_COMPLETE event (non-blocking)
1097
+ try:
1098
+ from modules.events.event_writer import EventWriter, AGENT_COMPLETE
1099
+ _plan = ""
1100
+ if parsed_contract and isinstance(parsed_contract.get("agent_status"), dict):
1101
+ _plan = str(parsed_contract["agent_status"].get("plan_status", ""))
1102
+ _key_outputs = []
1103
+ if parsed_contract and isinstance(parsed_contract.get("evidence_report"), dict):
1104
+ _key_outputs = parsed_contract["evidence_report"].get("key_outputs", [])
1105
+ _summary = "; ".join(str(o) for o in _key_outputs[:2]) if _key_outputs else ""
1106
+ EventWriter().write_event(
1107
+ AGENT_COMPLETE, "hook", agent_type,
1108
+ _plan or "completed",
1109
+ meta={"episode_id": episode_id, "summary": _summary[:200]},
1110
+ )
1111
+ except Exception:
1112
+ pass # Events are non-critical
1113
+
989
1114
  contract_attempts = 0
990
1115
  if not response_contract.valid:
991
1116
  try:
@@ -1007,19 +1132,11 @@ class ClaudeCodeAdapter(HookAdapter):
1007
1132
  )
1008
1133
 
1009
1134
  # ----------------------------------------------------------
1010
- # False pending-approval detection
1011
- # Advisory only -- adds to anomalies but never blocks.
1135
+ # Extract plan_status for downstream checks
1012
1136
  # ----------------------------------------------------------
1013
1137
  _plan_status = ""
1014
1138
  if parsed_contract and isinstance(parsed_contract.get("agent_status"), dict):
1015
1139
  _plan_status = str(parsed_contract["agent_status"].get("plan_status", ""))
1016
- false_pa_check = validate_awaiting_approval_has_nonce(agent_output, _plan_status)
1017
- if false_pa_check:
1018
- anomalies.append(false_pa_check)
1019
- logger.info(
1020
- "AWAITING_APPROVAL without nonce for %s: %s",
1021
- agent_type, false_pa_check.get("detail", ""),
1022
- )
1023
1140
 
1024
1141
  # ----------------------------------------------------------
1025
1142
  # Approval request validation
@@ -1145,9 +1262,16 @@ class ClaudeCodeAdapter(HookAdapter):
1145
1262
  QualityResult with quality assessment.
1146
1263
  Default: quality_sufficient=True (passthrough until business logic wired).
1147
1264
  """
1148
- # Extract stop reason and response content for future quality checks
1149
- _stop_reason = raw.get("stop_reason", "")
1150
- _last_message = raw.get("last_assistant_message", "")
1265
+ # Write SESSION_END event (non-blocking)
1266
+ try:
1267
+ from modules.events.event_writer import EventWriter, SESSION_END
1268
+ stop_reason = raw.get("stop_reason", "unknown")
1269
+ EventWriter().write_event(
1270
+ SESSION_END, "hook", "",
1271
+ f"session ended: {stop_reason}",
1272
+ )
1273
+ except Exception:
1274
+ pass # Events are non-critical
1151
1275
 
1152
1276
  return QualityResult(
1153
1277
  quality_sufficient=True,
@@ -1170,10 +1294,6 @@ class ClaudeCodeAdapter(HookAdapter):
1170
1294
  VerificationResult with criteria assessment.
1171
1295
  Default: criteria_met=True (passthrough until business logic wired).
1172
1296
  """
1173
- # Extract task details for future verification logic
1174
- _task_id = raw.get("task_id", "")
1175
- _task_output = raw.get("task_output", "")
1176
-
1177
1297
  return VerificationResult(
1178
1298
  criteria_met=True,
1179
1299
  verified_items=[],
@@ -1181,15 +1301,134 @@ class ClaudeCodeAdapter(HookAdapter):
1181
1301
  block_completion=False,
1182
1302
  )
1183
1303
 
1304
+ # ------------------------------------------------------------------ #
1305
+ # Context cache: PreToolUse -> SubagentStart bridge
1306
+ # ------------------------------------------------------------------ #
1307
+
1308
+ CONTEXT_CACHE_DIR = Path("/tmp/gaia-context-cache")
1309
+ CONTEXT_CACHE_TTL_SECONDS = 60 # Cache entries older than this are stale
1310
+
1311
+ def _cache_context_for_subagent(
1312
+ self, session_id: str, agent_type: str, context: str,
1313
+ ) -> Path:
1314
+ """Write built context to a cache file for SubagentStart consumption.
1315
+
1316
+ Returns the path to the cache file.
1317
+ """
1318
+ self.CONTEXT_CACHE_DIR.mkdir(parents=True, exist_ok=True)
1319
+ timestamp = int(time.time() * 1000)
1320
+ cache_file = self.CONTEXT_CACHE_DIR / f"{session_id}-{timestamp}.json"
1321
+ payload = {
1322
+ "context": context,
1323
+ "agent_type": agent_type,
1324
+ "session_id": session_id,
1325
+ "created_at": time.time(),
1326
+ }
1327
+ cache_file.write_text(json.dumps(payload))
1328
+ logger.debug("Context cache written: %s", cache_file)
1329
+ return cache_file
1330
+
1331
+ def _read_cached_context(self, session_id: str) -> Optional[Dict[str, Any]]:
1332
+ """Read and consume the most recent cached context for a session.
1333
+
1334
+ Finds the newest cache file matching the session_id, reads it,
1335
+ deletes it (one-shot consumption), and cleans up stale entries.
1336
+
1337
+ Returns None if no cache is found.
1338
+ """
1339
+ if not self.CONTEXT_CACHE_DIR.exists():
1340
+ return None
1341
+
1342
+ # Find all cache files for this session, sorted newest-first
1343
+ candidates: List[Path] = sorted(
1344
+ self.CONTEXT_CACHE_DIR.glob(f"{session_id}-*.json"),
1345
+ key=lambda p: p.stat().st_mtime,
1346
+ reverse=True,
1347
+ )
1348
+
1349
+ if not candidates:
1350
+ # Fallback: try to find the most recent cache file regardless of
1351
+ # session_id, since the orchestrator session_id and the subagent
1352
+ # session_id may differ.
1353
+ all_files = sorted(
1354
+ self.CONTEXT_CACHE_DIR.glob("*.json"),
1355
+ key=lambda p: p.stat().st_mtime,
1356
+ reverse=True,
1357
+ )
1358
+ candidates = all_files
1359
+
1360
+ now = time.time()
1361
+ result = None
1362
+
1363
+ for cache_file in candidates:
1364
+ try:
1365
+ data = json.loads(cache_file.read_text())
1366
+ age = now - data.get("created_at", 0)
1367
+
1368
+ if age > self.CONTEXT_CACHE_TTL_SECONDS:
1369
+ # Stale entry -- clean up
1370
+ cache_file.unlink(missing_ok=True)
1371
+ logger.debug("Cleaned stale context cache: %s (age=%.1fs)", cache_file.name, age)
1372
+ continue
1373
+
1374
+ # Found a valid entry -- consume it
1375
+ result = data
1376
+ cache_file.unlink(missing_ok=True)
1377
+ logger.debug("Consumed context cache: %s (age=%.1fs)", cache_file.name, age)
1378
+ break
1379
+
1380
+ except (json.JSONDecodeError, OSError) as exc:
1381
+ logger.warning("Failed to read context cache %s: %s", cache_file, exc)
1382
+ cache_file.unlink(missing_ok=True)
1383
+ continue
1384
+
1385
+ # Clean up any remaining stale files (background hygiene)
1386
+ self._cleanup_stale_cache(now)
1387
+
1388
+ return result
1389
+
1390
+ def _cleanup_stale_cache(self, now: float) -> None:
1391
+ """Remove cache files older than TTL."""
1392
+ if not self.CONTEXT_CACHE_DIR.exists():
1393
+ return
1394
+ for f in self.CONTEXT_CACHE_DIR.glob("*.json"):
1395
+ try:
1396
+ data = json.loads(f.read_text())
1397
+ if now - data.get("created_at", 0) > self.CONTEXT_CACHE_TTL_SECONDS:
1398
+ f.unlink(missing_ok=True)
1399
+ except (json.JSONDecodeError, OSError):
1400
+ f.unlink(missing_ok=True)
1401
+
1184
1402
  # ------------------------------------------------------------------ #
1185
1403
  # P2: adapt_subagent_start
1186
1404
  # ------------------------------------------------------------------ #
1187
1405
 
1188
1406
  def adapt_subagent_start(self, raw: dict) -> ContextResult:
1189
- """Parse SubagentStart event. Context injection is handled by PreToolUse."""
1190
- _agent_type = raw.get("agent_type", "")
1191
- _task_description = raw.get("task_description", "")
1407
+ """Parse SubagentStart event and forward cached context to the subagent.
1408
+
1409
+ PreToolUse:Agent caches the built project context. This method reads
1410
+ the cache and returns it as additionalContext so Claude Code injects
1411
+ it into the subagent (not the orchestrator).
1412
+ """
1413
+ session_id = raw.get("session_id", "")
1192
1414
 
1415
+ cached = self._read_cached_context(session_id)
1416
+ if cached:
1417
+ logger.info(
1418
+ "SubagentStart: forwarding cached context for agent=%s (session=%s)",
1419
+ cached.get("agent_type", "unknown"),
1420
+ session_id,
1421
+ )
1422
+ return ContextResult(
1423
+ context_injected=True,
1424
+ additional_context=cached["context"],
1425
+ sections_provided=[],
1426
+ )
1427
+
1428
+ logger.info(
1429
+ "SubagentStart: no cached context found for session=%s (passthrough)",
1430
+ session_id,
1431
+ )
1193
1432
  return ContextResult(
1194
1433
  context_injected=False,
1195
1434
  additional_context=None,