@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,496 @@
1
+ """
2
+ Runtime validation for agent response contracts.
3
+
4
+ Validates the structured JSON contract block returned by agents
5
+ (``json:contract`` fenced blocks parsed by ``contract_validator.parse_contract``).
6
+
7
+ Validated sections:
8
+ - agent_status (plan_status, agent_id, pending_steps, next_action)
9
+ - evidence_report (patterns_checked, files_checked, commands_run, key_outputs, ...)
10
+ - consolidation_report (ownership_assessment, confirmed_findings, ...)
11
+
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import re
18
+ import time
19
+ from dataclasses import asdict, dataclass
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+ from typing import Dict, List, Optional
23
+
24
+ from ..core.paths import get_session_dir
25
+ from ..core.state import get_session_id
26
+ from .contract_validator import parse_contract
27
+
28
+
29
+ VALID_PLAN_STATUSES = {
30
+ "IN_PROGRESS",
31
+ "REVIEW",
32
+ "COMPLETE",
33
+ "BLOCKED",
34
+ "NEEDS_INPUT",
35
+ }
36
+
37
+ # Evidence is required for ALL valid states -- no exclusions.
38
+ EVIDENCE_REQUIRED_PLAN_STATUSES = VALID_PLAN_STATUSES
39
+
40
+ EVIDENCE_FIELDS = [
41
+ "PATTERNS_CHECKED",
42
+ "FILES_CHECKED",
43
+ "COMMANDS_RUN",
44
+ "KEY_OUTPUTS",
45
+ "VERBATIM_OUTPUTS",
46
+ "CROSS_LAYER_IMPACTS",
47
+ "OPEN_GAPS",
48
+ ]
49
+ VALID_OWNERSHIP_ASSESSMENTS = {
50
+ "owned_here",
51
+ "cross_surface_dependency",
52
+ "not_my_surface",
53
+ }
54
+ # Bullet-list fields only; OWNERSHIP_ASSESSMENT is validated separately as a key-value enum.
55
+ CONSOLIDATION_FIELDS = [
56
+ "CONFIRMED_FINDINGS",
57
+ "SUSPECTED_FINDINGS",
58
+ "CONFLICTS",
59
+ "OPEN_GAPS",
60
+ "NEXT_BEST_AGENT",
61
+ ]
62
+
63
+ RECOMMENDED_ACTION_NONE = "none"
64
+
65
+ # Statuses that should carry an approval_request block
66
+ APPROVAL_REQUEST_STATUSES = {"REVIEW"}
67
+
68
+ APPROVAL_REQUEST_REQUIRED_FIELDS = [
69
+ "operation",
70
+ "exact_content",
71
+ "scope",
72
+ "risk_level",
73
+ "rollback",
74
+ "verification",
75
+ ]
76
+
77
+ VALID_RISK_LEVELS = {"LOW", "MEDIUM", "HIGH", "CRITICAL"}
78
+
79
+ _NONCE_HEX_PATTERN = re.compile(r"^[a-f0-9]{32}$")
80
+
81
+ _AGENT_ID_PATTERN = re.compile(r"^a[0-9a-f]{5,}$")
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class AgentStatusBlock:
86
+ marker_present: bool
87
+ plan_status: str
88
+ pending_steps: str
89
+ next_action: str
90
+ agent_id: str
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class EvidenceReportBlock:
95
+ marker_present: bool
96
+ fields: Dict[str, List[str]]
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class ConsolidationReportBlock:
101
+ marker_present: bool
102
+ ownership_assessment: str
103
+ fields: Dict[str, List[str]]
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class ResponseContractValidation:
108
+ valid: bool
109
+ severity: str
110
+ missing: List[str]
111
+ invalid: List[str]
112
+ warnings: List[str]
113
+ evidence_required: bool
114
+ consolidation_required: bool
115
+ recommended_action: str
116
+ agent_status: AgentStatusBlock
117
+ evidence_report: EvidenceReportBlock
118
+ consolidation_report: ConsolidationReportBlock
119
+
120
+ def to_dict(self) -> Dict[str, object]:
121
+ return asdict(self)
122
+
123
+
124
+ # ============================================================================
125
+ # JSON contract -> dataclass extraction helpers
126
+ # ============================================================================
127
+
128
+ def _get_str(d: dict, key: str) -> str:
129
+ """Get a string value from a dict, trying both lower-case and UPPER-CASE keys."""
130
+ return str(d.get(key, "") or d.get(key.upper(), "") or "")
131
+
132
+
133
+ def _get_list(d: dict, key: str) -> List[str]:
134
+ """Get a list-of-strings value, trying both lower-case and UPPER-CASE keys.
135
+
136
+ If the value is a list of dicts (e.g. commands_run entries with {command, result}),
137
+ each dict is serialised to a readable string.
138
+ """
139
+ val = d.get(key) or d.get(key.upper()) or []
140
+ if not isinstance(val, list):
141
+ return [str(val)] if val else []
142
+ result: List[str] = []
143
+ for item in val:
144
+ if isinstance(item, dict):
145
+ # e.g. {"command": "ls", "result": "ok"} -> "`ls` -> ok"
146
+ cmd = item.get("command", item.get("cmd", ""))
147
+ res = item.get("result", item.get("output", ""))
148
+ result.append(f"`{cmd}` -> {res}" if cmd else str(item))
149
+ else:
150
+ result.append(str(item))
151
+ return result
152
+
153
+
154
+ def _extract_agent_status(contract: dict) -> AgentStatusBlock:
155
+ """Build an AgentStatusBlock from the parsed JSON contract dict."""
156
+ agent_status = contract.get("agent_status")
157
+ if not agent_status or not isinstance(agent_status, dict):
158
+ return AgentStatusBlock(
159
+ marker_present=False,
160
+ plan_status="",
161
+ pending_steps="",
162
+ next_action="",
163
+ agent_id="",
164
+ )
165
+
166
+ plan_status = _get_str(agent_status, "plan_status").upper().rstrip(".,;")
167
+ pending_steps = _get_str(agent_status, "pending_steps")
168
+ next_action = _get_str(agent_status, "next_action")
169
+ agent_id = _get_str(agent_status, "agent_id")
170
+
171
+ return AgentStatusBlock(
172
+ marker_present=True,
173
+ plan_status=plan_status,
174
+ pending_steps=pending_steps,
175
+ next_action=next_action,
176
+ agent_id=agent_id,
177
+ )
178
+
179
+
180
+ def _extract_evidence_report(contract: dict) -> EvidenceReportBlock:
181
+ """Build an EvidenceReportBlock from the parsed JSON contract dict."""
182
+ evidence = contract.get("evidence_report")
183
+ if not evidence or not isinstance(evidence, dict):
184
+ return EvidenceReportBlock(
185
+ marker_present=False,
186
+ fields={field: [] for field in EVIDENCE_FIELDS},
187
+ )
188
+
189
+ fields: Dict[str, List[str]] = {}
190
+ for field_name in EVIDENCE_FIELDS:
191
+ key_lower = field_name.lower()
192
+ values = _get_list(evidence, key_lower)
193
+ fields[field_name] = values
194
+
195
+ return EvidenceReportBlock(marker_present=True, fields=fields)
196
+
197
+
198
+ def _extract_consolidation_report(contract: dict) -> ConsolidationReportBlock:
199
+ """Build a ConsolidationReportBlock from the parsed JSON contract dict."""
200
+ consolidation = contract.get("consolidation_report")
201
+ if not consolidation or not isinstance(consolidation, dict):
202
+ return ConsolidationReportBlock(
203
+ marker_present=False,
204
+ ownership_assessment="",
205
+ fields={field: [] for field in CONSOLIDATION_FIELDS},
206
+ )
207
+
208
+ ownership = _get_str(consolidation, "ownership_assessment")
209
+
210
+ fields: Dict[str, List[str]] = {}
211
+ for field_name in CONSOLIDATION_FIELDS:
212
+ key_lower = field_name.lower()
213
+ values = _get_list(consolidation, key_lower)
214
+ fields[field_name] = values
215
+
216
+ return ConsolidationReportBlock(
217
+ marker_present=True,
218
+ ownership_assessment=ownership,
219
+ fields=fields,
220
+ )
221
+
222
+
223
+ # ============================================================================
224
+ # Public parse helpers (operate on agent_output string via parse_contract)
225
+ # ============================================================================
226
+
227
+ def parse_agent_status(agent_output: str, parsed_contract: Optional[dict] = None) -> AgentStatusBlock:
228
+ """Parse agent_status from agent output using the json:contract block."""
229
+ contract = parsed_contract if parsed_contract is not None else parse_contract(agent_output)
230
+ if contract is None:
231
+ return AgentStatusBlock(
232
+ marker_present=False, plan_status="", pending_steps="",
233
+ next_action="", agent_id="",
234
+ )
235
+ return _extract_agent_status(contract)
236
+
237
+
238
+ def parse_evidence_report(agent_output: str, parsed_contract: Optional[dict] = None) -> EvidenceReportBlock:
239
+ """Parse evidence_report from agent output using the json:contract block."""
240
+ contract = parsed_contract if parsed_contract is not None else parse_contract(agent_output)
241
+ if contract is None:
242
+ return EvidenceReportBlock(
243
+ marker_present=False,
244
+ fields={field: [] for field in EVIDENCE_FIELDS},
245
+ )
246
+ return _extract_evidence_report(contract)
247
+
248
+
249
+ def parse_consolidation_report(agent_output: str, parsed_contract: Optional[dict] = None) -> ConsolidationReportBlock:
250
+ """Parse consolidation_report from agent output using the json:contract block."""
251
+ contract = parsed_contract if parsed_contract is not None else parse_contract(agent_output)
252
+ if contract is None:
253
+ return ConsolidationReportBlock(
254
+ marker_present=False, ownership_assessment="",
255
+ fields={field: [] for field in CONSOLIDATION_FIELDS},
256
+ )
257
+ return _extract_consolidation_report(contract)
258
+
259
+
260
+ def _is_resume_agent_id(value: str) -> bool:
261
+ return bool(_AGENT_ID_PATTERN.match(value or ""))
262
+
263
+
264
+ def resolve_agent_id(task_info: dict) -> str:
265
+ """Extract the agent ID from a task_info dict.
266
+
267
+ Falls back to ``task_id`` when ``agent_id`` is absent.
268
+ """
269
+ return str(task_info.get("agent_id", "") or task_info.get("task_id", ""))
270
+
271
+
272
+ def validate_response_contract(
273
+ agent_output: str,
274
+ *,
275
+ task_agent_id: str = "",
276
+ consolidation_required: bool = False,
277
+ parsed_contract: Optional[dict] = None,
278
+ ) -> ResponseContractValidation:
279
+ """Validate deterministic response blocks emitted by an agent.
280
+
281
+ Args:
282
+ agent_output: Raw agent output text.
283
+ task_agent_id: Agent ID from task_info, used as fallback.
284
+ consolidation_required: Whether a CONSOLIDATION_REPORT is required.
285
+ parsed_contract: Pre-parsed dict from parse_contract(). If provided,
286
+ avoids re-parsing agent_output. If None, parse_contract() is
287
+ called internally.
288
+ """
289
+ contract = parsed_contract if parsed_contract is not None else parse_contract(agent_output)
290
+
291
+ if contract is None:
292
+ # No json:contract block found -- everything is missing.
293
+ empty_evidence = EvidenceReportBlock(
294
+ marker_present=False,
295
+ fields={field: [] for field in EVIDENCE_FIELDS},
296
+ )
297
+ empty_consolidation = ConsolidationReportBlock(
298
+ marker_present=False, ownership_assessment="",
299
+ fields={field: [] for field in CONSOLIDATION_FIELDS},
300
+ )
301
+ empty_status = AgentStatusBlock(
302
+ marker_present=False, plan_status="", pending_steps="",
303
+ next_action="", agent_id="",
304
+ )
305
+ missing = ["AGENT_STATUS", "PLAN_STATUS", "PENDING_STEPS", "NEXT_ACTION", "AGENT_ID"]
306
+ recommended_action = "escalate_contract_repair" if not task_agent_id else "resume_same_agent_contract_repair"
307
+ if not _is_resume_agent_id(task_agent_id):
308
+ recommended_action = "escalate_contract_repair"
309
+ return ResponseContractValidation(
310
+ valid=False,
311
+ severity="hard",
312
+ missing=missing,
313
+ invalid=[],
314
+ warnings=[],
315
+ evidence_required=False,
316
+ consolidation_required=consolidation_required,
317
+ recommended_action=recommended_action,
318
+ agent_status=empty_status,
319
+ evidence_report=empty_evidence,
320
+ consolidation_report=empty_consolidation,
321
+ )
322
+
323
+ status = _extract_agent_status(contract)
324
+ evidence = _extract_evidence_report(contract)
325
+ if consolidation_required:
326
+ consolidation = _extract_consolidation_report(contract)
327
+ else:
328
+ consolidation = ConsolidationReportBlock(
329
+ marker_present=False, ownership_assessment="",
330
+ fields={field: [] for field in CONSOLIDATION_FIELDS}
331
+ )
332
+
333
+ missing: List[str] = []
334
+ invalid: List[str] = []
335
+
336
+ if not status.marker_present:
337
+ missing.append("AGENT_STATUS")
338
+
339
+ if not status.plan_status:
340
+ missing.append("PLAN_STATUS")
341
+ elif status.plan_status not in VALID_PLAN_STATUSES:
342
+ invalid.append(f"PLAN_STATUS:{status.plan_status}")
343
+
344
+ if not status.pending_steps:
345
+ missing.append("PENDING_STEPS")
346
+ if not status.next_action:
347
+ missing.append("NEXT_ACTION")
348
+ if not status.agent_id:
349
+ missing.append("AGENT_ID")
350
+
351
+ effective_agent_id = status.agent_id if _is_resume_agent_id(status.agent_id) else task_agent_id
352
+ if not _is_resume_agent_id(effective_agent_id):
353
+ effective_agent_id = ""
354
+ evidence_required = status.plan_status in EVIDENCE_REQUIRED_PLAN_STATUSES
355
+ if evidence_required:
356
+ if not evidence.marker_present:
357
+ missing.append("EVIDENCE_REPORT")
358
+ for field in EVIDENCE_FIELDS:
359
+ if not evidence.fields.get(field, []):
360
+ missing.append(field)
361
+
362
+ if consolidation_required:
363
+ if not consolidation.marker_present:
364
+ missing.append("CONSOLIDATION_REPORT")
365
+ if not consolidation.ownership_assessment:
366
+ missing.append("OWNERSHIP_ASSESSMENT")
367
+ elif consolidation.ownership_assessment not in VALID_OWNERSHIP_ASSESSMENTS:
368
+ invalid.append(f"OWNERSHIP_ASSESSMENT:{consolidation.ownership_assessment}")
369
+ for field in CONSOLIDATION_FIELDS:
370
+ if not consolidation.fields.get(field, []):
371
+ missing.append(field)
372
+
373
+ # ------------------------------------------------------------------
374
+ # Approval request validation (advisory -- warnings only, not blocking)
375
+ # ------------------------------------------------------------------
376
+ warnings: List[str] = []
377
+ if status.plan_status in APPROVAL_REQUEST_STATUSES:
378
+ approval_req = contract.get("approval_request")
379
+ if not approval_req or not isinstance(approval_req, dict):
380
+ warnings.append("APPROVAL_REQUEST_MISSING")
381
+ else:
382
+ for field in APPROVAL_REQUEST_REQUIRED_FIELDS:
383
+ if not approval_req.get(field):
384
+ warnings.append(f"APPROVAL_REQUEST_FIELD_MISSING:{field}")
385
+ risk = str(approval_req.get("risk_level", "")).upper()
386
+ if risk and risk not in VALID_RISK_LEVELS:
387
+ warnings.append(f"APPROVAL_REQUEST_INVALID_RISK_LEVEL:{risk}")
388
+ # Check for approval_id when status is REVIEW
389
+ if status.plan_status == "REVIEW":
390
+ pass # approval_id presence is advisory, not enforced
391
+
392
+ valid = not missing and not invalid
393
+ recommended_action = RECOMMENDED_ACTION_NONE if valid else "resume_same_agent_contract_repair"
394
+ severity = "none" if valid else "hard"
395
+
396
+ # If there is no actionable agent id, repair cannot be routed deterministically.
397
+ if not valid and not effective_agent_id:
398
+ recommended_action = "escalate_contract_repair"
399
+
400
+ return ResponseContractValidation(
401
+ valid=valid,
402
+ severity=severity,
403
+ missing=missing,
404
+ invalid=invalid,
405
+ warnings=warnings,
406
+ evidence_required=evidence_required,
407
+ consolidation_required=consolidation_required,
408
+ recommended_action=recommended_action,
409
+ agent_status=status,
410
+ evidence_report=evidence,
411
+ consolidation_report=consolidation,
412
+ )
413
+
414
+
415
+ def _get_session_id() -> str:
416
+ return get_session_id()
417
+
418
+
419
+ _contract_dir_cache: Dict[str, Path] = {}
420
+
421
+
422
+ def clear_contract_dir_cache() -> None:
423
+ """Clear the cached contract directory path (useful for testing)."""
424
+ _contract_dir_cache.clear()
425
+
426
+
427
+ def _get_contract_dir(session_id: Optional[str] = None) -> Path:
428
+ session_id = session_id or _get_session_id()
429
+ cached = _contract_dir_cache.get(session_id)
430
+ if cached is not None and cached.is_dir():
431
+ return cached
432
+ path = get_session_dir() / "response-contract" / session_id
433
+ path.mkdir(parents=True, exist_ok=True)
434
+ _contract_dir_cache[session_id] = path
435
+ return path
436
+
437
+
438
+ def _read_json(path: Path) -> Optional[dict]:
439
+ try:
440
+ return json.loads(path.read_text())
441
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
442
+ return None
443
+
444
+
445
+ def load_last_validation(session_id: Optional[str] = None) -> Optional[dict]:
446
+ """Load the last response-contract validation result, if any."""
447
+ session_id = session_id or _get_session_id()
448
+ path = _get_contract_dir(session_id) / "last-result.json"
449
+ payload = _read_json(path)
450
+ if not payload:
451
+ return None
452
+ if payload.get("session_id") != session_id:
453
+ return None
454
+ return payload
455
+
456
+
457
+ def save_validation_result(task_info: Dict[str, object], validation: ResponseContractValidation) -> Path:
458
+ """Persist the last validation result for observability and orchestration."""
459
+ session_id = _get_session_id()
460
+ target = _get_contract_dir(session_id) / "last-result.json"
461
+ payload = {
462
+ "timestamp": datetime.now().isoformat(),
463
+ "created_at_epoch": time.time(),
464
+ "session_id": session_id,
465
+ "agent": task_info.get("agent", ""),
466
+ "agent_id": resolve_agent_id(task_info),
467
+ "task_id": task_info.get("task_id", ""),
468
+ "validation": validation.to_dict(),
469
+ }
470
+ target.write_text(json.dumps(payload, indent=2))
471
+ return target
472
+
473
+
474
+ __all__ = [
475
+ "AgentStatusBlock",
476
+ "EvidenceReportBlock",
477
+ "ConsolidationReportBlock",
478
+ "ResponseContractValidation",
479
+ "VALID_PLAN_STATUSES",
480
+ "EVIDENCE_REQUIRED_PLAN_STATUSES",
481
+ "EVIDENCE_FIELDS",
482
+ "VALID_OWNERSHIP_ASSESSMENTS",
483
+ "CONSOLIDATION_FIELDS",
484
+ "RECOMMENDED_ACTION_NONE",
485
+ "APPROVAL_REQUEST_STATUSES",
486
+ "APPROVAL_REQUEST_REQUIRED_FIELDS",
487
+ "VALID_RISK_LEVELS",
488
+ "parse_agent_status",
489
+ "parse_evidence_report",
490
+ "parse_consolidation_report",
491
+ "validate_response_contract",
492
+ "save_validation_result",
493
+ "load_last_validation",
494
+ "resolve_agent_id",
495
+ "clear_contract_dir_cache",
496
+ ]
@@ -0,0 +1,124 @@
1
+ """
2
+ Skill injection verifier -- transcript fingerprint checking.
3
+
4
+ At SubagentStop, verifies that skills declared in the agent's frontmatter
5
+ were actually injected into the agent's context by searching for unique
6
+ fingerprint strings from each SKILL.md.
7
+
8
+ Returns an optional anomaly dict (advisory) when declared skills are missing
9
+ from the transcript, indicating a potential injection gap.
10
+ """
11
+
12
+ import logging
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ # Fingerprint strings per skill: unique phrases from SKILL.md that confirm injection.
19
+ # Each skill maps to a list of candidate fingerprints -- at least one must appear
20
+ # in the transcript for the skill to be considered present.
21
+ SKILL_FINGERPRINTS: Dict[str, List[str]] = {
22
+ "agent-protocol": [
23
+ "json:contract",
24
+ "plan_status",
25
+ "evidence_report",
26
+ ],
27
+ "security-tiers": [
28
+ "T0_READ_ONLY",
29
+ "T3_BLOCKED",
30
+ "Tier Definitions",
31
+ "Hook Enforcement",
32
+ ],
33
+ "investigation": [
34
+ "Start From Injected Context",
35
+ "Pattern Hierarchy",
36
+ "Codebase first",
37
+ ],
38
+ "command-execution": [
39
+ "ONE COMMAND. ONE RESULT. ONE EXIT CODE",
40
+ "NO PIPES. NO CHAINS. NO REDIRECTS",
41
+ "cloud_pipe_validator",
42
+ ],
43
+ "context-updater": [
44
+ "CONTEXT_UPDATE",
45
+ "context-updater",
46
+ ],
47
+ "fast-queries": [
48
+ "fast-queries",
49
+ "triage",
50
+ ],
51
+ "terraform-patterns": [
52
+ "terraform-patterns",
53
+ "Terragrunt",
54
+ ],
55
+ "gitops-patterns": [
56
+ "gitops-patterns",
57
+ "Flux",
58
+ "HelmRelease",
59
+ ],
60
+ "developer-patterns": [
61
+ "developer-patterns",
62
+ ],
63
+ "speckit-workflow": [
64
+ "speckit-workflow",
65
+ "speckit",
66
+ ],
67
+ }
68
+
69
+
70
+ def verify_skill_injection(
71
+ agent_type: str,
72
+ transcript_text: str,
73
+ declared_skills: List[str],
74
+ ) -> Optional[Dict[str, Any]]:
75
+ """Verify that declared skills were injected into the agent transcript.
76
+
77
+ Searches the transcript for fingerprint strings that confirm each skill
78
+ was loaded. Returns an anomaly dict if any declared skill has no
79
+ fingerprint match in the transcript.
80
+
81
+ Args:
82
+ agent_type: The agent type string (e.g. "cloud-troubleshooter").
83
+ transcript_text: The full agent transcript text.
84
+ declared_skills: List of skill names from agent frontmatter.
85
+
86
+ Returns:
87
+ An anomaly dict (type: skill_injection_gap, severity: advisory) if
88
+ any declared skill is missing from the transcript. None if all
89
+ declared skills are present or if the check does not apply.
90
+ """
91
+ if not transcript_text or not declared_skills:
92
+ return None
93
+
94
+ missing_skills: List[str] = []
95
+
96
+ for skill_name in declared_skills:
97
+ fingerprints = SKILL_FINGERPRINTS.get(skill_name)
98
+ if fingerprints is None:
99
+ # No fingerprints defined for this skill -- skip (cannot verify)
100
+ logger.debug(
101
+ "No fingerprints defined for skill '%s', skipping verification",
102
+ skill_name,
103
+ )
104
+ continue
105
+
106
+ # At least one fingerprint must appear in the transcript
107
+ found = any(fp in transcript_text for fp in fingerprints)
108
+ if not found:
109
+ missing_skills.append(skill_name)
110
+
111
+ if not missing_skills:
112
+ return None
113
+
114
+ return {
115
+ "type": "skill_injection_gap",
116
+ "severity": "advisory",
117
+ "agent_type": agent_type,
118
+ "missing_skills": missing_skills,
119
+ "message": (
120
+ f"Agent '{agent_type}' declared {len(declared_skills)} skills but "
121
+ f"{len(missing_skills)} skill(s) have no transcript fingerprint: "
122
+ f"{', '.join(missing_skills)}"
123
+ ),
124
+ }
@@ -0,0 +1,74 @@
1
+ """
2
+ Build task_info dict from Claude Code SubagentStop stdin payload.
3
+
4
+ Provides:
5
+ - build_task_info_from_hook_data(): Map hook payload to task_info format
6
+ """
7
+
8
+ import logging
9
+ import re
10
+ from typing import Any, Dict
11
+
12
+ from .contract_validator import extract_exit_code_from_output, extract_plan_status_from_output
13
+ from .transcript_reader import (
14
+ extract_injected_context_payload_from_transcript,
15
+ extract_task_description_from_transcript,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def build_task_info_from_hook_data(
22
+ hook_data: Dict[str, Any],
23
+ agent_output: str = "",
24
+ ) -> Dict[str, Any]:
25
+ """Build a task_info dict from the Claude Code SubagentStop stdin payload.
26
+
27
+ Claude Code sends these fields for SubagentStop:
28
+ - hook_event_name: "SubagentStop"
29
+ - session_id: str
30
+ - agent_type: str (e.g. "cloud-troubleshooter")
31
+ - agent_id: str
32
+ - transcript_path: str (session-level JSONL)
33
+ - agent_transcript_path: str (subagent JSONL)
34
+ - last_assistant_message: str (final agent response text, no I/O needed)
35
+ - cwd: str
36
+ - stop_hook_active: bool
37
+ - permission_mode: str
38
+
39
+ We map these to the task_info format expected by subagent_stop_hook().
40
+ The exit_code is derived from the agent's AGENT_STATUS block.
41
+ task_description is extracted from the first user message in the transcript.
42
+ tier_real is parsed from the AGENT_STATUS block (not hardcoded T0).
43
+ """
44
+ exit_code = extract_exit_code_from_output(agent_output) if agent_output else 0
45
+ plan_status = extract_plan_status_from_output(agent_output) if agent_output else ""
46
+
47
+ # Extract tier from agent output (e.g. agents report tier in their context)
48
+ # Look for explicit tier references in agent output: T0, T1, T2, T3
49
+ tier_real = "T0"
50
+ if agent_output:
51
+ tier_match = re.search(r"\bT([0-3])\b", agent_output)
52
+ if tier_match:
53
+ tier_real = f"T{tier_match.group(1)}"
54
+
55
+ # Extract real task description from the first user message in the transcript
56
+ transcript_path = hook_data.get("agent_transcript_path", "")
57
+ task_description = extract_task_description_from_transcript(transcript_path)
58
+ injected_context = extract_injected_context_payload_from_transcript(transcript_path)
59
+ agent_type = hook_data.get("agent_type", "") or "unknown"
60
+ if not task_description:
61
+ task_description = f"SubagentStop for {agent_type}"
62
+
63
+ return {
64
+ "task_id": hook_data.get("agent_id", "unknown"),
65
+ "agent_id": hook_data.get("agent_id", "unknown"),
66
+ "agent_transcript_path": transcript_path,
67
+ "description": task_description,
68
+ "agent": agent_type,
69
+ "tier": tier_real,
70
+ "tags": [agent_type] if agent_type != "unknown" else [],
71
+ "exit_code": exit_code,
72
+ "plan_status": plan_status,
73
+ "injected_context": injected_context,
74
+ }