@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,283 @@
1
+ """
2
+ Task tool validator.
3
+
4
+ Validates Task tool invocations:
5
+ - Agent existence verification
6
+ - Context provisioning enforcement
7
+ - T3 operation detection for user approval workflow
8
+ """
9
+
10
+ import logging
11
+ import re
12
+ from typing import Dict, Any, List, Optional, Tuple
13
+ from dataclasses import dataclass
14
+
15
+ from ..security.tiers import SecurityTier
16
+ from ..security.mutative_verbs import (
17
+ detect_mutative_command,
18
+ MutativeResult,
19
+ CLI_FAMILY_LOOKUP,
20
+ COMMAND_ALIASES,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Available agents for Task invocation — both bare and plugin-namespaced forms
26
+ _BASE_AGENTS = [
27
+ "terraform-architect",
28
+ "gitops-operator",
29
+ "cloud-troubleshooter",
30
+ "devops-developer",
31
+ "gaia-system",
32
+ "Explore",
33
+ "Plan",
34
+ "speckit-planner",
35
+ "claude-code-guide",
36
+ ]
37
+ # Support both "cloud-troubleshooter" and "gaia-ops:cloud-troubleshooter"
38
+ AVAILABLE_AGENTS = _BASE_AGENTS + [f"gaia-ops:{a}" for a in _BASE_AGENTS]
39
+
40
+ # Meta-agents that don't require context_provider.
41
+ # speckit-planner is a project agent that DOES receive context, so it is NOT a meta-agent.
42
+ META_AGENTS = ["gaia-system", "Explore", "Plan", "claude-code-guide"]
43
+
44
+ # T3_KEYWORDS is test-only: used by tests and cross-layer consistency checks
45
+ # to verify that these commands are classified as T3 by the verb detector.
46
+ # NOT used at runtime -- detection is handled entirely by detect_mutative_command().
47
+ T3_KEYWORDS = [
48
+ "git commit",
49
+ "git push",
50
+ "terraform apply",
51
+ "terragrunt apply",
52
+ "terragrunt run-all apply",
53
+ "kubectl apply",
54
+ "kubectl delete",
55
+ "kubectl create",
56
+ "kubectl rollout restart",
57
+ "kubectl scale",
58
+ "kubectl set image",
59
+ "git push origin main",
60
+ "git push origin master",
61
+ "helm install",
62
+ "helm upgrade",
63
+ "flux reconcile",
64
+ "npm publish",
65
+ "docker push",
66
+ "gcloud sql import",
67
+ "gcloud storage cp",
68
+ "gcloud storage rsync",
69
+ ]
70
+
71
+
72
+ _EMBEDDED_COMMAND_QUOTE_CHARS = "\"'`"
73
+
74
+
75
+ def _sanitize_candidate_fragment(fragment: str) -> str:
76
+ """Normalize a prose-embedded command fragment for verb detection.
77
+
78
+ Task prompts often mention commands inside backticks or quotes:
79
+ - Please run `terraform apply` in prod
80
+ - Need to execute "terraform apply" in prod
81
+
82
+ The detector only needs the command skeleton, so strip quote delimiters and
83
+ collapse whitespace before handing the fragment to the dangerous verb
84
+ classifier.
85
+ """
86
+ if not fragment:
87
+ return ""
88
+ cleaned = fragment.translate(str.maketrans({char: " " for char in _EMBEDDED_COMMAND_QUOTE_CHARS}))
89
+ cleaned = re.sub(r"\s+", " ", cleaned).strip()
90
+ return cleaned.rstrip(".,;:!?")
91
+
92
+
93
+ def _extract_command_candidates(text: str) -> List[str]:
94
+ """Extract command-like lines from free-form text for verb detection.
95
+
96
+ Looks for lines that start with known CLI prefixes or contain command-like
97
+ patterns (e.g., "git push", "terraform apply").
98
+
99
+ Args:
100
+ text: Free-form text (prompt or description).
101
+
102
+ Returns:
103
+ List of candidate command strings to scan.
104
+ """
105
+ if not text:
106
+ return []
107
+
108
+ candidates: List[str] = []
109
+ # Derive CLI prefixes from the canonical CLI_FAMILY_LOOKUP and COMMAND_ALIASES
110
+ cli_prefixes = tuple(
111
+ f"{cli} " for cli in sorted(
112
+ set(CLI_FAMILY_LOOKUP.keys()) | set(COMMAND_ALIASES.keys()),
113
+ key=len,
114
+ reverse=True,
115
+ )
116
+ )
117
+
118
+ text_lower = text.lower()
119
+
120
+ # Strategy 1: Scan the full text for known CLI command patterns
121
+ for prefix in cli_prefixes:
122
+ idx = 0
123
+ while True:
124
+ pos = text_lower.find(prefix, idx)
125
+ if pos == -1:
126
+ break
127
+ # Only match at word boundaries (start of string or preceded by whitespace/punctuation)
128
+ if pos > 0 and text_lower[pos - 1].isalnum():
129
+ idx = pos + len(prefix)
130
+ continue
131
+ # Extract from the prefix to end of line (or next sentence boundary)
132
+ end = text.find("\n", pos)
133
+ if end == -1:
134
+ end = len(text)
135
+ fragment = text[pos:end].strip()
136
+ # Trim trailing punctuation/quotes that are part of prose
137
+ fragment = fragment.rstrip(".,;:!?\"')")
138
+ fragment = _sanitize_candidate_fragment(fragment)
139
+ if fragment:
140
+ candidates.append(fragment)
141
+ idx = pos + len(prefix)
142
+
143
+ return candidates
144
+
145
+
146
+ def _scan_text_for_t3(text: str) -> Tuple[bool, str, Optional[MutativeResult]]:
147
+ """Scan free-form text for T3 (dangerous) command intent using the verb detector.
148
+
149
+ Args:
150
+ text: Combined prompt/description text.
151
+
152
+ Returns:
153
+ (is_t3, matched_command, danger_result) tuple.
154
+ """
155
+ candidates = _extract_command_candidates(text)
156
+
157
+ for candidate in candidates:
158
+ result = detect_mutative_command(candidate)
159
+ if result.is_mutative:
160
+ return True, candidate, result
161
+
162
+ return False, "", None
163
+
164
+ __all__ = [
165
+ "TaskValidator",
166
+ "TaskValidationResult",
167
+ "validate_task_invocation",
168
+ "AVAILABLE_AGENTS",
169
+ "META_AGENTS",
170
+ "T3_KEYWORDS",
171
+ ]
172
+
173
+
174
+ @dataclass
175
+ class TaskValidationResult:
176
+ """Result of Task tool validation."""
177
+ allowed: bool
178
+ tier: SecurityTier
179
+ reason: str
180
+ agent_name: str = ""
181
+ has_context: bool = False
182
+ is_t3_operation: bool = False
183
+
184
+
185
+ class TaskValidator:
186
+ """Validator for Task tool invocations."""
187
+
188
+ def __init__(self, available_agents: Optional[List[str]] = None):
189
+ """
190
+ Initialize validator.
191
+
192
+ Args:
193
+ available_agents: Override available agents list
194
+ """
195
+ self.available_agents = available_agents or AVAILABLE_AGENTS
196
+
197
+ def validate(self, parameters: Dict[str, Any]) -> TaskValidationResult:
198
+ """
199
+ Validate Task tool invocation.
200
+
201
+ Args:
202
+ parameters: Task tool parameters
203
+
204
+ Returns:
205
+ TaskValidationResult with validation details
206
+ """
207
+ agent_name = parameters.get("subagent_type", "unknown")
208
+ prompt = parameters.get("prompt", "")
209
+ description = parameters.get("description", "")
210
+
211
+ # additionalContext means prompt is never mutated, so T3 detection
212
+ # runs directly against the original user prompt.
213
+ user_task_for_t3_check = prompt
214
+
215
+ logger.info(f"Task tool validation for agent: {agent_name}")
216
+
217
+ # Check agent exists
218
+ if agent_name not in self.available_agents:
219
+ error_msg = f"Unknown agent: '{agent_name}'\n\n"
220
+ error_msg += f"Available agents:\n"
221
+ for agent in sorted(self.available_agents):
222
+ error_msg += f" - {agent}\n"
223
+ error_msg += "\nRefer to the Surface Routing Recommendation for agent selection.\n"
224
+ error_msg += f"\nCorrect usage: Task(subagent_type=\"<agent-name>\", ...)"
225
+
226
+ return TaskValidationResult(
227
+ allowed=False,
228
+ tier=SecurityTier.T3_BLOCKED,
229
+ reason=error_msg,
230
+ agent_name=agent_name,
231
+ )
232
+
233
+ # Context is injected via additionalContext by the adapter, not by
234
+ # mutating the prompt. The validator cannot check additionalContext
235
+ # (it only sees parameters), so we determine context status by agent type.
236
+ # Meta-agents never receive context by design.
237
+ has_context = agent_name not in META_AGENTS
238
+
239
+ # Check for T3 operations (use original user task to avoid false positives from context)
240
+ is_t3 = self._is_t3_operation(user_task_for_t3_check, description)
241
+
242
+ logger.info(
243
+ f"Task invocation validated: {agent_name} "
244
+ f"(T3={is_t3}, context={has_context})"
245
+ )
246
+
247
+ tier = SecurityTier.T3_BLOCKED if is_t3 else SecurityTier.T0_READ_ONLY
248
+ reason = (
249
+ f"Task invocation allowed for {agent_name}; T3 execution still requires "
250
+ f"nonce-based approval at Bash time"
251
+ if is_t3
252
+ else f"Task invocation allowed for {agent_name}"
253
+ )
254
+
255
+ return TaskValidationResult(
256
+ allowed=True,
257
+ tier=tier,
258
+ reason=reason,
259
+ agent_name=agent_name,
260
+ has_context=has_context,
261
+ is_t3_operation=is_t3,
262
+ )
263
+
264
+ def _is_t3_operation(self, prompt: str, description: str) -> bool:
265
+ """Check if this is a T3 (destructive) operation using the verb detector."""
266
+ combined = f"{description} {prompt}"
267
+ is_t3, _, _ = _scan_text_for_t3(combined)
268
+ return is_t3
269
+
270
+
271
+
272
+ def validate_task_invocation(parameters: Dict[str, Any]) -> TaskValidationResult:
273
+ """
274
+ Validate Task tool invocation (convenience function).
275
+
276
+ Args:
277
+ parameters: Task tool parameters
278
+
279
+ Returns:
280
+ TaskValidationResult
281
+ """
282
+ validator = TaskValidator()
283
+ return validator.validate(parameters)
@@ -0,0 +1,23 @@
1
+ """
2
+ Validation Module: Commit message validation for bash_validator
3
+
4
+ This module provides commit message validation that is exclusively used
5
+ by hooks/modules/tools/bash_validator.py to enforce git commit standards.
6
+
7
+ Note: This is an internal module. Do not import directly in agent code.
8
+ Commit validation is automatically enforced via bash_validator.py.
9
+ """
10
+
11
+ from .commit_validator import (
12
+ CommitMessageValidator,
13
+ ValidationResult,
14
+ validate_commit_message,
15
+ safe_validate_before_commit,
16
+ )
17
+
18
+ __all__ = [
19
+ "CommitMessageValidator",
20
+ "ValidationResult",
21
+ "validate_commit_message",
22
+ "safe_validate_before_commit",
23
+ ]
@@ -0,0 +1,380 @@
1
+ """
2
+ Git Commit Message Validator
3
+
4
+ Validates commit messages against project standards before execution.
5
+ This prevents commits with forbidden footers or incorrect format from
6
+ being pushed to the repository.
7
+
8
+ Usage:
9
+ from commit_validator import CommitMessageValidator
10
+
11
+ validator = CommitMessageValidator()
12
+ validation = validator.validate(commit_message)
13
+
14
+ if not validation.valid:
15
+ for error in validation.errors:
16
+ print(f"Error: {error['message']}")
17
+ # Do not proceed with commit
18
+ else:
19
+ # Safe to commit
20
+ git commit -m "$commit_message"
21
+ """
22
+
23
+ import json
24
+ import os
25
+ import re
26
+ from typing import Dict, List, Any, Optional
27
+ from datetime import datetime
28
+ from dataclasses import dataclass
29
+
30
+
31
+ @dataclass
32
+ class ValidationResult:
33
+ """Result of commit message validation."""
34
+ valid: bool
35
+ errors: List[Dict[str, str]]
36
+ warnings: List[Dict[str, str]] = None
37
+
38
+ def __post_init__(self):
39
+ if self.warnings is None:
40
+ self.warnings = []
41
+
42
+
43
+ class CommitMessageValidator:
44
+ """
45
+ Validates git commit messages against project standards.
46
+
47
+ Standards are defined in .claude/config/git_standards.json
48
+ """
49
+
50
+ def __init__(self, config_path: Optional[str] = None):
51
+ """
52
+ Initialize validator with configuration.
53
+
54
+ Args:
55
+ config_path: Optional path to git_standards.json
56
+ If None, uses default location
57
+ """
58
+ if config_path is None:
59
+ # Default path relative to this file
60
+ # From hooks/modules/validation/ go up to gaia-ops root
61
+ # __file__ -> hooks/modules/validation/commit_validator.py
62
+ # dirname(dirname(dirname(dirname(__file__)))) -> gaia-ops root
63
+ base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
64
+ config_path = os.path.join(base_path, 'config', 'git_standards.json')
65
+ else:
66
+ # If config_path provided, derive base_path from it
67
+ base_path = os.path.dirname(os.path.dirname(config_path))
68
+
69
+ self.base_path = base_path
70
+ self.config_path = config_path
71
+ self.config = self._load_config()
72
+ self.standards = self.config.get('commit_message', {})
73
+ self.enforcement = self.config.get('enforcement', {})
74
+
75
+ def _load_config(self) -> Dict[str, Any]:
76
+ """Load git standards configuration from JSON file."""
77
+ if not os.path.exists(self.config_path):
78
+ raise FileNotFoundError(
79
+ f"Git standards configuration not found at: {self.config_path}"
80
+ )
81
+
82
+ with open(self.config_path, 'r') as f:
83
+ return json.load(f)
84
+
85
+ def validate(self, message: str) -> ValidationResult:
86
+ """
87
+ Validate a commit message against all standards.
88
+
89
+ Args:
90
+ message: The commit message to validate
91
+
92
+ Returns:
93
+ ValidationResult with valid status and any errors/warnings
94
+ """
95
+ errors = []
96
+ warnings = []
97
+
98
+ # 1. Check for forbidden footers (CRITICAL)
99
+ footer_errors = self._check_forbidden_footers(message)
100
+ errors.extend(footer_errors)
101
+
102
+ # 2. Check conventional commits format
103
+ format_errors = self._check_conventional_format(message)
104
+ errors.extend(format_errors)
105
+
106
+ # 3. Check subject line rules
107
+ subject_errors = self._check_subject_rules(message)
108
+ errors.extend(subject_errors)
109
+
110
+ # 4. Check body rules (warnings only)
111
+ body_warnings = self._check_body_rules(message)
112
+ warnings.extend(body_warnings)
113
+
114
+ # Log violations if configured
115
+ if errors and self.enforcement.get('log_violations', False):
116
+ self._log_violation(message, errors)
117
+
118
+ return ValidationResult(
119
+ valid=len(errors) == 0,
120
+ errors=errors,
121
+ warnings=warnings
122
+ )
123
+
124
+ def _check_forbidden_footers(self, message: str) -> List[Dict[str, str]]:
125
+ """Check for forbidden footers in commit message."""
126
+ errors = []
127
+ forbidden = self.standards.get('footer_forbidden', [])
128
+
129
+ for forbidden_text in forbidden:
130
+ if forbidden_text.lower() in message.lower():
131
+ errors.append({
132
+ 'type': 'FORBIDDEN_FOOTER',
133
+ 'message': f"Commit message contains forbidden footer: '{forbidden_text}'",
134
+ 'fix': f"Remove all occurrences of '{forbidden_text}'",
135
+ 'severity': 'error'
136
+ })
137
+
138
+ return errors
139
+
140
+ def _check_conventional_format(self, message: str) -> List[Dict[str, str]]:
141
+ """Check if message follows Conventional Commits format."""
142
+ errors = []
143
+
144
+ # Get first line (subject)
145
+ lines = message.split('\n')
146
+ subject = lines[0].strip()
147
+
148
+ # Pattern: type(scope)?: description
149
+ # Examples: feat: add feature, fix(api): correct bug
150
+ allowed_types = '|'.join(self.standards.get('type_allowed', []))
151
+ pattern = rf'^({allowed_types})(\(.+?\))?: .+$'
152
+
153
+ if not re.match(pattern, subject):
154
+ errors.append({
155
+ 'type': 'INVALID_FORMAT',
156
+ 'message': 'Commit message does not follow Conventional Commits format',
157
+ 'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(self.standards.get('type_allowed', []))}",
158
+ 'severity': 'error',
159
+ 'examples': self.standards.get('examples_valid', [])
160
+ })
161
+
162
+ return errors
163
+
164
+ def _check_subject_rules(self, message: str) -> List[Dict[str, str]]:
165
+ """Check subject line specific rules."""
166
+ errors = []
167
+
168
+ lines = message.split('\n')
169
+ subject = lines[0].strip()
170
+
171
+ # Extract description part (after type and scope)
172
+ # Example: "feat(scope): description" -> "description"
173
+ match = re.match(r'^[a-z]+(\(.+?\))?: (.+)$', subject)
174
+ if match:
175
+ description = match.group(2)
176
+
177
+ # Check max length
178
+ max_length = self.standards.get('subject_max_length', 72)
179
+ if len(subject) > max_length:
180
+ errors.append({
181
+ 'type': 'SUBJECT_TOO_LONG',
182
+ 'message': f'Subject line exceeds {max_length} characters (current: {len(subject)})',
183
+ 'fix': f'Shorten subject to {max_length} characters or less',
184
+ 'severity': 'error'
185
+ })
186
+
187
+ # Check for period at end
188
+ rules = self.standards.get('subject_rules', {})
189
+ if rules.get('no_period_at_end', True) and description.endswith('.'):
190
+ errors.append({
191
+ 'type': 'SUBJECT_ENDS_WITH_PERIOD',
192
+ 'message': 'Subject line should not end with a period',
193
+ 'fix': 'Remove the period at the end of the subject',
194
+ 'severity': 'error'
195
+ })
196
+
197
+ # Check for emojis in subject line
198
+ if rules.get('no_emoji', False):
199
+ emoji_pattern = re.compile(
200
+ "["
201
+ "\U0001F600-\U0001F64F" # emoticons
202
+ "\U0001F300-\U0001F5FF" # symbols & pictographs
203
+ "\U0001F680-\U0001F6FF" # transport & map symbols
204
+ "\U0001F700-\U0001F77F" # alchemical symbols
205
+ "\U0001F780-\U0001F7FF" # Geometric Shapes Extended
206
+ "\U0001F800-\U0001F8FF" # Supplemental Arrows-C
207
+ "\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
208
+ "\U0001FA00-\U0001FA6F" # Chess Symbols
209
+ "\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A
210
+ "\U00002702-\U000027B0" # Dingbats
211
+ "\U000024C2-\U0001F251" # Enclosed characters
212
+ "]+", flags=re.UNICODE
213
+ )
214
+
215
+ if emoji_pattern.search(subject):
216
+ errors.append({
217
+ 'type': 'SUBJECT_CONTAINS_EMOJI',
218
+ 'message': 'Subject line contains emojis which are not allowed',
219
+ 'fix': 'Remove all emojis from the subject line',
220
+ 'severity': 'error'
221
+ })
222
+
223
+ return errors
224
+
225
+ def _check_body_rules(self, message: str) -> List[Dict[str, str]]:
226
+ """Check body rules (returns warnings, not errors)."""
227
+ warnings = []
228
+
229
+ lines = message.split('\n')
230
+
231
+ # Check if there's a body (more than just subject)
232
+ if len(lines) <= 1:
233
+ return warnings
234
+
235
+ # Check blank line after subject
236
+ if len(lines) > 1 and lines[1].strip() != '':
237
+ warnings.append({
238
+ 'type': 'MISSING_BLANK_LINE',
239
+ 'message': 'Missing blank line between subject and body',
240
+ 'fix': 'Add a blank line after the subject line',
241
+ 'severity': 'warning'
242
+ })
243
+
244
+ # Check body line length
245
+ max_length = self.standards.get('body_max_line_length', 72)
246
+ for i, line in enumerate(lines[2:], start=3): # Skip subject and blank line
247
+ if len(line) > max_length and not line.startswith('http'):
248
+ warnings.append({
249
+ 'type': 'BODY_LINE_TOO_LONG',
250
+ 'message': f'Body line {i} exceeds {max_length} characters',
251
+ 'fix': f'Wrap line to {max_length} characters',
252
+ 'severity': 'warning'
253
+ })
254
+
255
+ return warnings
256
+
257
+ def _log_violation(self, message: str, errors: List[Dict[str, str]]):
258
+ """Log commit message violation for audit trail."""
259
+ log_path = self.enforcement.get('log_path', '.claude/logs/commit-violations.jsonl')
260
+
261
+ # If log_path is relative, resolve from base_path (not cwd)
262
+ if not os.path.isabs(log_path):
263
+ # Remove leading ./ if present
264
+ log_path = log_path.lstrip('./')
265
+ # If starts with 'claude/', remove it since base_path already points to .claude/
266
+ if log_path.startswith('claude/'):
267
+ log_path = log_path[7:] # Remove 'claude/' prefix
268
+ log_path = os.path.join(self.base_path, log_path)
269
+
270
+ # Ensure log directory exists
271
+ log_dir = os.path.dirname(log_path)
272
+ if log_dir and not os.path.exists(log_dir):
273
+ os.makedirs(log_dir, exist_ok=True)
274
+
275
+ log_entry = {
276
+ 'timestamp': datetime.now().isoformat(),
277
+ 'message': message[:100] + ('...' if len(message) > 100 else ''),
278
+ 'errors': errors,
279
+ 'error_count': len(errors)
280
+ }
281
+
282
+ with open(log_path, 'a') as f:
283
+ f.write(json.dumps(log_entry) + '\n')
284
+
285
+ def get_examples(self) -> Dict[str, List[str]]:
286
+ """Get example commit messages (valid and invalid)."""
287
+ return {
288
+ 'valid': self.standards.get('examples_valid', []),
289
+ 'invalid': self.standards.get('examples_invalid', [])
290
+ }
291
+
292
+ def get_allowed_types(self) -> List[str]:
293
+ """Get list of allowed commit types."""
294
+ return self.standards.get('type_allowed', [])
295
+
296
+ def format_error_message(self, validation: ValidationResult) -> str:
297
+ """
298
+ Format validation errors into human-readable message.
299
+
300
+ Args:
301
+ validation: ValidationResult from validate()
302
+
303
+ Returns:
304
+ Formatted error message string
305
+ """
306
+ if validation.valid:
307
+ return "[OK] Commit message is valid"
308
+
309
+ lines = ["[ERROR] Commit message validation failed:\n"]
310
+
311
+ for error in validation.errors:
312
+ lines.append(f" [{error['type']}]")
313
+ lines.append(f" {error['message']}")
314
+ lines.append(f" Fix: {error['fix']}")
315
+
316
+ if 'examples' in error:
317
+ lines.append(f" Examples:")
318
+ for example in error['examples'][:3]:
319
+ lines.append(f" - {example}")
320
+
321
+ lines.append("")
322
+
323
+ if validation.warnings:
324
+ lines.append("[WARNING] Warnings:")
325
+ for warning in validation.warnings:
326
+ lines.append(f" - {warning['message']}")
327
+ lines.append("")
328
+
329
+ return "\n".join(lines)
330
+
331
+
332
+ # Convenience function for quick validation
333
+ def validate_commit_message(message: str) -> ValidationResult:
334
+ """
335
+ Quick validation function.
336
+
337
+ Args:
338
+ message: Commit message to validate
339
+
340
+ Returns:
341
+ ValidationResult
342
+
343
+ Example:
344
+ validation = validate_commit_message("feat: add new feature")
345
+ if not validation.valid:
346
+ print("Invalid commit message")
347
+ """
348
+ validator = CommitMessageValidator()
349
+ return validator.validate(message)
350
+
351
+
352
+ # Function for use in git commit workflow
353
+ def safe_validate_before_commit(message: str) -> bool:
354
+ """
355
+ Validate commit message and print errors if invalid.
356
+
357
+ This is the primary function that agents should call before git commit.
358
+
359
+ Args:
360
+ message: Commit message to validate
361
+
362
+ Returns:
363
+ True if valid, False if invalid (with errors printed)
364
+
365
+ Example:
366
+ if not safe_validate_before_commit(commit_message):
367
+ return {"status": "failed", "reason": "commit_validation_failed"}
368
+
369
+ # Safe to commit
370
+ Bash(f'git commit -m "{commit_message}"')
371
+ """
372
+ validator = CommitMessageValidator()
373
+ validation = validator.validate(message)
374
+
375
+ if not validation.valid:
376
+ error_message = validator.format_error_message(validation)
377
+ print(error_message)
378
+ return False
379
+
380
+ return True