@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
@@ -125,29 +125,28 @@ def get_memory_dir(subdir: Optional[str] = None) -> Path:
125
125
  return memory_dir
126
126
 
127
127
 
128
- def get_session_dir() -> Path:
128
+ def get_events_dir() -> Path:
129
129
  """
130
- Get the active session directory, creating it if necessary.
130
+ Get the events directory, creating it if necessary.
131
131
 
132
132
  Returns:
133
- Path to .claude/session/active/
133
+ Path to .claude/events/
134
134
  """
135
- session_dir = get_plugin_data_dir() / "session" / "active"
136
- session_dir.mkdir(parents=True, exist_ok=True)
137
- return session_dir
135
+ events_dir = get_plugin_data_dir() / "events"
136
+ events_dir.mkdir(parents=True, exist_ok=True)
137
+ return events_dir
138
138
 
139
139
 
140
- def get_hooks_config_dir() -> Path:
140
+ def get_session_dir() -> Path:
141
141
  """
142
- Get the hooks config directory.
142
+ Get the active session directory, creating it if necessary.
143
143
 
144
144
  Returns:
145
- Path to hooks/config/
145
+ Path to .claude/session/active/
146
146
  """
147
- # Config lives alongside the hooks modules
148
- hooks_dir = Path(__file__).parent.parent.parent
149
- config_dir = hooks_dir / "config"
150
- return config_dir
147
+ session_dir = get_plugin_data_dir() / "session" / "active"
148
+ session_dir.mkdir(parents=True, exist_ok=True)
149
+ return session_dir
151
150
 
152
151
 
153
152
  def clear_path_cache():
@@ -6,9 +6,11 @@ Ops mode: T3 operations block with nonce for orchestrator agent approval flow.
6
6
 
7
7
  Detection order:
8
8
  1. plugin-registry.json in plugin data directory
9
- 2. GAIA_PLUGIN_MODE env var fallback
10
- 3. Default: "security" (most restrictive)
9
+ 2. NPM package name detection (gaia-ops vs gaia-security)
10
+ 3. GAIA_PLUGIN_MODE env var fallback
11
+ 4. Default: "security" (most restrictive)
11
12
  """
13
+ from __future__ import annotations
12
14
 
13
15
  import json
14
16
  import logging
@@ -21,6 +23,54 @@ logger = logging.getLogger(__name__)
21
23
  VALID_MODES = ("security", "ops")
22
24
  DEFAULT_MODE = "security"
23
25
 
26
+ # Map NPM package names to plugin modes
27
+ _NPM_PACKAGE_MODE = {
28
+ "gaia-ops": "ops",
29
+ "gaia-security": "security",
30
+ }
31
+
32
+
33
+ def _detect_mode_from_npm_package() -> str | None:
34
+ """Detect plugin mode from the NPM package name.
35
+
36
+ When installed via npm, this module lives at a path like:
37
+ .../node_modules/@jaguilar87/gaia-ops/hooks/modules/core/plugin_mode.py
38
+
39
+ The package directory name (gaia-ops or gaia-security) determines the mode.
40
+ Also checks .claude/ symlinks as a secondary signal for npm installs.
41
+
42
+ Returns the mode string or None if not detectable.
43
+ """
44
+ # Primary: check our own file path for node_modules package name
45
+ module_path = Path(__file__).resolve()
46
+ parts = module_path.parts
47
+ for i, part in enumerate(parts):
48
+ if part == "node_modules" and i + 2 < len(parts):
49
+ # Could be @scope/package-name or just package-name
50
+ pkg_name = parts[i + 1]
51
+ if pkg_name.startswith("@") and i + 2 < len(parts):
52
+ pkg_name = parts[i + 2]
53
+ mode = _NPM_PACKAGE_MODE.get(pkg_name)
54
+ if mode:
55
+ logger.debug("Detected mode '%s' from npm package path: %s", mode, pkg_name)
56
+ return mode
57
+
58
+ # Secondary: check if .claude/agents symlink points to a gaia package
59
+ try:
60
+ from .paths import find_claude_dir
61
+ claude_dir = find_claude_dir()
62
+ agents_link = claude_dir / "agents"
63
+ if agents_link.is_symlink():
64
+ target = str(agents_link.resolve())
65
+ for pkg_name, mode in _NPM_PACKAGE_MODE.items():
66
+ if pkg_name in target:
67
+ logger.debug("Detected mode '%s' from .claude/agents symlink target", mode)
68
+ return mode
69
+ except Exception:
70
+ pass
71
+
72
+ return None
73
+
24
74
 
25
75
  @lru_cache(maxsize=1)
26
76
  def get_plugin_mode() -> str:
@@ -42,12 +92,32 @@ def get_plugin_mode() -> str:
42
92
  except Exception as e:
43
93
  logger.debug("Registry check failed (non-fatal): %s", e)
44
94
 
45
- # 2. Env var fallback
95
+ # 2. CLAUDE_PLUGIN_ROOT + plugin.json (--plugin-dir mode)
96
+ plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
97
+ if plugin_root:
98
+ try:
99
+ pjson = Path(plugin_root) / ".claude-plugin" / "plugin.json"
100
+ if pjson.exists():
101
+ pdata = json.loads(pjson.read_text())
102
+ pname = pdata.get("name", "")
103
+ mode = _NPM_PACKAGE_MODE.get(pname)
104
+ if mode:
105
+ logger.debug("Detected mode '%s' from plugin.json name: %s", mode, pname)
106
+ return mode
107
+ except Exception as e:
108
+ logger.debug("Plugin.json check failed (non-fatal): %s", e)
109
+
110
+ # 3. NPM package name detection
111
+ npm_mode = _detect_mode_from_npm_package()
112
+ if npm_mode:
113
+ return npm_mode
114
+
115
+ # 4. Env var fallback
46
116
  mode = os.environ.get("GAIA_PLUGIN_MODE", "").lower()
47
117
  if mode in VALID_MODES:
48
118
  return mode
49
119
 
50
- # 3. Default: security (most restrictive)
120
+ # 5. Default: security (most restrictive)
51
121
  return DEFAULT_MODE
52
122
 
53
123
 
@@ -1,8 +1,9 @@
1
1
  """First-time plugin setup for SessionStart hook.
2
2
 
3
3
  Detects first run via marker file in CLAUDE_PLUGIN_DATA.
4
- On first run, creates .claude/settings.json in the project with base permissions.
4
+ On first run, merges gaia permissions into .claude/settings.local.json.
5
5
  """
6
+ from __future__ import annotations
6
7
 
7
8
  import json
8
9
  import logging
@@ -16,6 +17,192 @@ logger = logging.getLogger(__name__)
16
17
 
17
18
  MARKER_FILE = ".plugin-initialized"
18
19
 
20
+ # ---------------------------------------------------------------------------
21
+ # Deny list — shared across all modes. Aligned with blocked_commands.py
22
+ # (hook-level enforcement) for dual-barrier security. These rules are
23
+ # merged into settings.local.json so Claude Code's native permission system
24
+ # blocks the commands BEFORE they even reach the hook layer.
25
+ # ---------------------------------------------------------------------------
26
+ _DENY_RULES = [
27
+ # AWS — networking / data infrastructure (irreversible)
28
+ "Bash(aws ec2 delete-vpc:*)",
29
+ "Bash(aws ec2 delete-subnet:*)",
30
+ "Bash(aws ec2 delete-internet-gateway:*)",
31
+ "Bash(aws ec2 delete-route-table:*)",
32
+ "Bash(aws ec2 delete-route:*)",
33
+ "Bash(aws ec2 terminate-instances:*)",
34
+ "Bash(aws rds delete-db-instance:*)",
35
+ "Bash(aws rds delete-db-cluster:*)",
36
+ "Bash(aws dynamodb delete-table:*)",
37
+ "Bash(aws s3 rb:*)",
38
+ "Bash(aws s3api delete-bucket:*)",
39
+ "Bash(aws elasticache delete-cache-cluster:*)",
40
+ "Bash(aws elasticache delete-replication-group:*)",
41
+ "Bash(aws eks delete-cluster:*)",
42
+ # AWS — KMS / Organizations / Route53
43
+ "Bash(aws kms schedule-key-deletion:*)",
44
+ "Bash(aws organizations delete-organization:*)",
45
+ "Bash(aws route53 delete-hosted-zone:*)",
46
+ # AWS — IAM (mutative but denied at settings level too)
47
+ "Bash(aws iam delete-user:*)",
48
+ "Bash(aws iam delete-role:*)",
49
+ "Bash(aws iam delete-access-key:*)",
50
+ "Bash(aws iam delete-group:*)",
51
+ "Bash(aws iam delete-instance-profile:*)",
52
+ "Bash(aws iam delete-policy:*)",
53
+ "Bash(aws iam delete-role-policy:*)",
54
+ "Bash(aws iam delete-user-policy:*)",
55
+ "Bash(aws iam delete-group-policy:*)",
56
+ "Bash(aws iam detach-user-policy:*)",
57
+ "Bash(aws iam detach-role-policy:*)",
58
+ "Bash(aws iam detach-group-policy:*)",
59
+ "Bash(aws iam remove-user-from-group:*)",
60
+ # AWS — other destructive
61
+ "Bash(aws backup delete:*::*)",
62
+ "Bash(aws cloudformation delete-stack:*)",
63
+ "Bash(aws dynamodb delete-item:*)",
64
+ "Bash(aws ec2 delete-key-pair:*)",
65
+ "Bash(aws ec2 delete-snapshot:*)",
66
+ "Bash(aws ec2 delete-volume:*)",
67
+ "Bash(aws ec2 delete-security-group:*)",
68
+ "Bash(aws ec2 delete-network-interface:*)",
69
+ "Bash(aws lambda delete-function:*)",
70
+ "Bash(aws rds delete-db-cluster-parameter-group:*)",
71
+ "Bash(aws rds delete-db-parameter-group:*)",
72
+ "Bash(aws s3api delete-objects:*)",
73
+ "Bash(aws sns delete-topic:*)",
74
+ "Bash(aws sqs delete-queue:*)",
75
+ "Bash(aws eks delete-nodegroup:*)",
76
+ "Bash(aws eks delete-addon:*)",
77
+ # Azure — resource group / networking / data (irreversible)
78
+ "Bash(az group delete:*)",
79
+ "Bash(az network vnet delete:*)",
80
+ "Bash(az network vnet subnet delete:*)",
81
+ "Bash(az network nsg delete:*)",
82
+ "Bash(az network public-ip delete:*)",
83
+ "Bash(az network application-gateway delete:*)",
84
+ "Bash(az network lb delete:*)",
85
+ "Bash(az network dns zone delete:*)",
86
+ "Bash(az network private-dns zone delete:*)",
87
+ "Bash(az vm delete:*)",
88
+ "Bash(az vmss delete:*)",
89
+ "Bash(az disk delete:*)",
90
+ "Bash(az snapshot delete:*)",
91
+ "Bash(az image delete:*)",
92
+ # Azure — databases / storage
93
+ "Bash(az sql server delete:*)",
94
+ "Bash(az sql db delete:*)",
95
+ "Bash(az cosmosdb delete:*)",
96
+ "Bash(az redis delete:*)",
97
+ "Bash(az storage account delete:*)",
98
+ "Bash(az storage container delete:*)",
99
+ "Bash(az storage blob delete-batch:*)",
100
+ # Azure — AKS / container
101
+ "Bash(az aks delete:*)",
102
+ "Bash(az aks nodepool delete:*)",
103
+ "Bash(az acr delete:*)",
104
+ # Azure — IAM / key vault / functions
105
+ "Bash(az role assignment delete:*)",
106
+ "Bash(az role definition delete:*)",
107
+ "Bash(az ad app delete:*)",
108
+ "Bash(az ad sp delete:*)",
109
+ "Bash(az keyvault delete:*)",
110
+ "Bash(az keyvault key delete:*)",
111
+ "Bash(az keyvault secret delete:*)",
112
+ "Bash(az functionapp delete:*)",
113
+ "Bash(az webapp delete:*)",
114
+ # Azure — messaging / monitoring
115
+ "Bash(az servicebus namespace delete:*)",
116
+ "Bash(az servicebus queue delete:*)",
117
+ "Bash(az servicebus topic delete:*)",
118
+ "Bash(az eventhubs namespace delete:*)",
119
+ "Bash(az eventhubs eventhub delete:*)",
120
+ "Bash(az monitor action-group delete:*)",
121
+ # GCP — project / cluster / database (irreversible)
122
+ "Bash(gcloud projects delete:*)",
123
+ "Bash(gcloud container clusters delete:*)",
124
+ "Bash(gcloud container node-pools delete:*)",
125
+ "Bash(gcloud sql instances delete:*)",
126
+ "Bash(gcloud sql databases delete:*)",
127
+ "Bash(gcloud services disable:*)",
128
+ "Bash(gsutil rb:*)",
129
+ "Bash(gsutil rm -r:*)",
130
+ # GCP — compute / IAM / storage
131
+ "Bash(gcloud compute firewall-rules delete:*)",
132
+ "Bash(gcloud compute instances delete:*)",
133
+ "Bash(gcloud compute networks delete:*)",
134
+ "Bash(gcloud compute disks delete:*)",
135
+ "Bash(gcloud compute images delete:*)",
136
+ "Bash(gcloud compute snapshots delete:*)",
137
+ "Bash(gcloud iam roles delete:*)",
138
+ "Bash(gcloud storage rm:*)",
139
+ # Kubernetes — critical cluster operations
140
+ "Bash(kubectl delete namespace:*)",
141
+ "Bash(kubectl delete node:*)",
142
+ "Bash(kubectl delete cluster:*)",
143
+ "Bash(kubectl delete pv:*)",
144
+ "Bash(kubectl delete persistentvolume:*)",
145
+ "Bash(kubectl delete pvc:*)",
146
+ "Bash(kubectl delete persistentvolumeclaim:*)",
147
+ "Bash(kubectl delete crd:*)",
148
+ "Bash(kubectl delete customresourcedefinition:*)",
149
+ "Bash(kubectl delete mutatingwebhookconfiguration:*)",
150
+ "Bash(kubectl delete validatingwebhookconfiguration:*)",
151
+ "Bash(kubectl delete clusterrole:*)",
152
+ "Bash(kubectl delete clusterrolebinding:*)",
153
+ "Bash(kubectl drain:*)",
154
+ # Flux
155
+ "Bash(flux delete:*)",
156
+ # Git — force push (history rewrite)
157
+ "Bash(git push --force:*)",
158
+ "Bash(git push -f:*)",
159
+ "Bash(git push origin --force:*)",
160
+ "Bash(git push origin -f:*)",
161
+ # Disk / filesystem destruction
162
+ "Bash(dd:*)",
163
+ "Bash(fdisk:*)",
164
+ "Bash(mkfs:*)",
165
+ "Bash(mkfs.ext4:*)",
166
+ "Bash(mkfs.ext3:*)",
167
+ "Bash(mkfs.fat:*)",
168
+ "Bash(mkfs.ntfs:*)",
169
+ # -------------------------------------------------------------------
170
+ # Generic wildcard rules — catch ALL present and future services.
171
+ # These complement the granular rules above; if a new cloud service
172
+ # is added, these patterns block its delete operations automatically.
173
+ # -------------------------------------------------------------------
174
+ # AWS — any "delete-*" subcommand across all services
175
+ "Bash(aws * delete-*:*)",
176
+ "Bash(aws * terminate-*:*)",
177
+ # Azure — any "delete" subcommand across all services
178
+ "Bash(az * delete:*)",
179
+ # GCP — any "delete" subcommand across all services
180
+ "Bash(gcloud * delete:*)",
181
+ "Bash(gsutil rb:*)",
182
+ "Bash(gsutil rm:*)",
183
+ "Bash(gcloud storage rm:*)",
184
+ # Kubernetes — all delete and drain operations
185
+ "Bash(kubectl delete:*)",
186
+ "Bash(kubectl drain:*)",
187
+ # Terraform / Terragrunt — destroy
188
+ "Bash(terraform destroy:*)",
189
+ "Bash(terragrunt destroy:*)",
190
+ "Bash(terragrunt run-all destroy:*)",
191
+ # Helm — uninstall
192
+ "Bash(helm uninstall:*)",
193
+ "Bash(helm delete:*)",
194
+ # Flux — uninstall
195
+ "Bash(flux uninstall:*)",
196
+ # Docker — bulk prune
197
+ "Bash(docker system prune:*)",
198
+ "Bash(docker volume prune:*)",
199
+ # Git — destructive history operations
200
+ "Bash(git reset --hard:*)",
201
+ # Repo deletion
202
+ "Bash(gh repo delete:*)",
203
+ "Bash(glab project delete:*)",
204
+ ]
205
+
19
206
  # Base permissions for security-only mode
20
207
  SECURITY_PERMISSIONS = {
21
208
  "permissions": {
@@ -34,7 +221,7 @@ SECURITY_PERMISSIONS = {
34
221
  "WebSearch",
35
222
  "NotebookEdit",
36
223
  ],
37
- "deny": [],
224
+ "deny": _DENY_RULES,
38
225
  "ask": [],
39
226
  }
40
227
  }
@@ -63,7 +250,7 @@ OPS_PERMISSIONS = {
63
250
  "Edit(/tmp/*)",
64
251
  "Write(/tmp/*)",
65
252
  ],
66
- "deny": [],
253
+ "deny": _DENY_RULES,
67
254
  "ask": [],
68
255
  }
69
256
  }
@@ -128,18 +315,25 @@ def setup_project_permissions() -> bool:
128
315
  existing["permissions"]["deny"] = merged_deny
129
316
  existing["permissions"].setdefault("ask", [])
130
317
 
318
+ # Add env vars (smart merge: add if not present, don't overwrite)
319
+ env = existing.setdefault("env", {})
320
+ if "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" not in env:
321
+ env["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] = "1"
322
+
131
323
  claude_dir.mkdir(parents=True, exist_ok=True)
132
324
  settings_path.write_text(json.dumps(existing, indent=2) + "\n")
133
- logger.info("Merged gaia %s permissions into %s", mode, settings_path)
325
+ logger.info("Merged gaia %s permissions and env into %s", mode, settings_path)
134
326
  return True
135
327
 
136
328
 
137
329
  def ensure_plugin_registry() -> None:
138
- """Create plugin-registry.json from CLAUDE_PLUGIN_ROOT if missing.
330
+ """Create plugin-registry.json if missing.
139
331
 
140
- In plugin mode, CLAUDE_PLUGIN_ROOT looks like:
141
- .../cache/marketplace/gaia-ops/4.4.0-rc.2
142
- We extract the plugin name and version from the path.
332
+ Detection strategies (in order):
333
+ 1. CLAUDE_PLUGIN_ROOT env var (plugin marketplace mode):
334
+ Path looks like .../cache/marketplace/gaia-ops/4.4.0-rc.2
335
+ 2. NPM package detection: resolve package name and version from
336
+ node_modules path and package.json
143
337
  """
144
338
  import os
145
339
  data_dir = get_plugin_data_dir()
@@ -147,37 +341,215 @@ def ensure_plugin_registry() -> None:
147
341
  if registry_path.exists():
148
342
  return
149
343
 
150
- plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
151
- if not plugin_root:
152
- return
344
+ plugin_name = None
345
+ plugin_version = None
346
+ source = None
153
347
 
154
- parts = Path(plugin_root).parts
155
- if len(parts) < 2:
348
+ # Strategy 1: CLAUDE_PLUGIN_ROOT (plugin marketplace or --plugin-dir)
349
+ plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
350
+ if plugin_root:
351
+ root_path = Path(plugin_root)
352
+ # First, try to read .claude-plugin/plugin.json (most reliable)
353
+ plugin_json = root_path / ".claude-plugin" / "plugin.json"
354
+ if plugin_json.exists():
355
+ try:
356
+ pdata = json.loads(plugin_json.read_text())
357
+ plugin_name = pdata.get("name")
358
+ plugin_version = pdata.get("version")
359
+ source = "plugin-mode"
360
+ except (json.JSONDecodeError, OSError):
361
+ pass
362
+ # Fallback: parse path (marketplace layout: .../name/version)
363
+ if not plugin_name:
364
+ parts = root_path.parts
365
+ if len(parts) >= 2:
366
+ plugin_name = parts[-2]
367
+ plugin_version = parts[-1]
368
+ source = "plugin-mode"
369
+
370
+ # Strategy 2: NPM package detection
371
+ if not plugin_name:
372
+ npm_info = _detect_npm_package_info()
373
+ if npm_info:
374
+ plugin_name, plugin_version = npm_info
375
+ source = "npm-mode"
376
+
377
+ if not plugin_name:
156
378
  return
157
379
 
158
- plugin_name = parts[-2] # e.g. "gaia-ops" or "gaia-security"
159
- plugin_version = parts[-1] # e.g. "4.4.0-rc.2"
160
-
161
380
  registry = {
162
- "installed": [{"name": plugin_name, "version": plugin_version}],
163
- "source": "plugin-mode",
381
+ "installed": [{"name": plugin_name, "version": plugin_version or "unknown"}],
382
+ "source": source,
164
383
  }
384
+ data_dir.mkdir(parents=True, exist_ok=True)
165
385
  registry_path.write_text(json.dumps(registry, indent=2) + "\n")
166
- logger.info("Created plugin-registry.json: %s@%s", plugin_name, plugin_version)
386
+ logger.info("Created plugin-registry.json: %s@%s (source: %s)", plugin_name, plugin_version, source)
387
+
388
+
389
+ def _detect_npm_package_info() -> tuple[str, str | None] | None:
390
+ """Detect plugin name and version from NPM package path.
391
+
392
+ When installed via npm, this module lives at:
393
+ .../node_modules/@jaguilar87/gaia-ops/hooks/modules/core/plugin_setup.py
394
+
395
+ Returns (plugin_name, version) or None.
396
+ """
397
+ module_path = Path(__file__).resolve()
398
+ parts = module_path.parts
399
+
400
+ # Find node_modules in path and extract package name
401
+ pkg_name = None
402
+ pkg_root = None
403
+ for i, part in enumerate(parts):
404
+ if part == "node_modules" and i + 1 < len(parts):
405
+ next_part = parts[i + 1]
406
+ if next_part.startswith("@") and i + 2 < len(parts):
407
+ # Scoped package: @scope/name
408
+ pkg_name = parts[i + 2]
409
+ pkg_root = Path(*parts[:i + 3])
410
+ else:
411
+ pkg_name = next_part
412
+ pkg_root = Path(*parts[:i + 2])
413
+ break
414
+
415
+ if not pkg_name or pkg_name not in ("gaia-ops", "gaia-security"):
416
+ return None
417
+
418
+ # Try to read version from package.json
419
+ version = None
420
+ if pkg_root:
421
+ pkg_json = Path("/") / pkg_root / "package.json"
422
+ try:
423
+ if pkg_json.exists():
424
+ data = json.loads(pkg_json.read_text())
425
+ version = data.get("version")
426
+ except Exception:
427
+ pass
428
+
429
+ return (pkg_name, version)
430
+
431
+
432
+ def setup_project_hooks() -> bool:
433
+ """Merge hooks from hooks.json into .claude/settings.local.json.
434
+
435
+ In npm mode, Claude Code reads hooks from settings files, not hooks.json.
436
+ This converts ${CLAUDE_PLUGIN_ROOT}/hooks/<script> paths to .claude/hooks/<script>
437
+ and merges them into settings.local.json with deduplication by command string.
438
+
439
+ Returns True if settings were modified.
440
+ """
441
+ import re
442
+
443
+ claude_dir = Path.cwd() / ".claude"
444
+ settings_path = claude_dir / "settings.local.json"
445
+
446
+ # Find hooks.json — try package root (npm) or plugin root
447
+ hooks_json_path = None
448
+ # Strategy 1: relative to this module (npm layout)
449
+ module_dir = Path(__file__).resolve().parent.parent.parent
450
+ candidate = module_dir / "hooks.json"
451
+ if candidate.is_file():
452
+ hooks_json_path = candidate
453
+ else:
454
+ # Strategy 2: .claude/hooks/hooks.json (symlinked)
455
+ candidate2 = claude_dir / "hooks" / "hooks.json"
456
+ if candidate2.is_file():
457
+ hooks_json_path = candidate2
458
+
459
+ if not hooks_json_path:
460
+ logger.info("hooks.json not found, skipping hooks merge")
461
+ return False
462
+
463
+ try:
464
+ hooks_data = json.loads(hooks_json_path.read_text())
465
+ except (json.JSONDecodeError, OSError):
466
+ logger.warning("hooks.json is invalid, skipping hooks merge")
467
+ return False
468
+
469
+ # Unwrap outer "hooks" key if present
470
+ source_hooks = hooks_data.get("hooks", hooks_data)
471
+
472
+ # Convert ${CLAUDE_PLUGIN_ROOT}/hooks/<script> to .claude/hooks/<script>
473
+ def convert_command(cmd: str) -> str:
474
+ return re.sub(r'\$\{CLAUDE_PLUGIN_ROOT\}/hooks/', '.claude/hooks/', cmd)
475
+
476
+ converted_hooks: dict = {}
477
+ for event, entries in source_hooks.items():
478
+ converted_hooks[event] = []
479
+ for entry in entries:
480
+ new_entry = dict(entry)
481
+ if "hooks" in new_entry:
482
+ new_entry["hooks"] = [
483
+ {**h, "command": convert_command(h["command"])} if "command" in h else h
484
+ for h in new_entry["hooks"]
485
+ ]
486
+ converted_hooks[event].append(new_entry)
487
+
488
+ # Load existing settings.local.json
489
+ existing: dict = {}
490
+ if settings_path.exists():
491
+ try:
492
+ existing = json.loads(settings_path.read_text())
493
+ except (json.JSONDecodeError, OSError):
494
+ existing = {}
495
+
496
+ # Smart merge: deduplicate by command string
497
+ existing_hooks = existing.get("hooks", {})
498
+ changed = False
499
+
500
+ for event, new_entries in converted_hooks.items():
501
+ if event not in existing_hooks:
502
+ existing_hooks[event] = new_entries
503
+ changed = True
504
+ continue
505
+
506
+ # Collect existing command strings
507
+ existing_commands: set = set()
508
+ for entry in existing_hooks[event]:
509
+ for h in entry.get("hooks", []):
510
+ if "command" in h:
511
+ existing_commands.add(h["command"])
512
+
513
+ # Add entries whose commands are not already present
514
+ for new_entry in new_entries:
515
+ new_commands = [h["command"] for h in new_entry.get("hooks", []) if "command" in h]
516
+ all_present = len(new_commands) > 0 and all(c in existing_commands for c in new_commands)
517
+ if not all_present:
518
+ existing_hooks[event].append(new_entry)
519
+ changed = True
520
+
521
+ if not changed:
522
+ logger.info("settings.local.json hooks already up to date")
523
+ return False
524
+
525
+ existing["hooks"] = existing_hooks
526
+ claude_dir.mkdir(parents=True, exist_ok=True)
527
+ settings_path.write_text(json.dumps(existing, indent=2) + "\n")
528
+ logger.info("Merged hooks into %s", settings_path)
529
+ return True
167
530
 
168
531
 
169
- def run_first_time_setup() -> str | None:
170
- """Run setup. Returns a reload message if permissions were written."""
171
- # Always ensure registry and permissions exist (even on subsequent runs)
532
+ def run_first_time_setup(mark_done: bool = True) -> str | None:
533
+ """Run setup. Returns a reload message if permissions were written.
534
+
535
+ Args:
536
+ mark_done: If True, mark the plugin as initialized after setup.
537
+ Set to False when the caller wants to defer marking
538
+ (e.g., UserPromptSubmit marks after showing the welcome).
539
+ """
540
+ # Always ensure registry, permissions, and hooks exist (even on subsequent runs)
172
541
  ensure_plugin_registry()
173
542
  reload_needed = setup_project_permissions()
543
+ hooks_changed = setup_project_hooks()
544
+ reload_needed = reload_needed or hooks_changed
174
545
 
175
546
  if not is_first_run():
176
547
  if reload_needed:
177
548
  return "Permissions updated. Run /reload-plugins to activate."
178
549
  return None
179
550
 
180
- mark_initialized()
551
+ if mark_done:
552
+ mark_initialized()
181
553
 
182
554
  if reload_needed:
183
555
  mode = get_plugin_mode()
@@ -0,0 +1 @@
1
+ """Event context system for cross-session operational event logging."""