@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,539 @@
1
+ """
2
+ Hook executor for gaia-ops replay testing.
3
+
4
+ Runs hooks as subprocesses with ReplayEvent payloads and compares results
5
+ against expected outcomes. Completely decoupled from log parsing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any, Optional
19
+
20
+ from gaia_simulator.extractor import ReplayEvent
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ReplayResult:
25
+ """Result of replaying a single event against the current hooks."""
26
+
27
+ event: ReplayEvent
28
+ actual_exit_code: int
29
+ actual_stdout: str
30
+ actual_stderr: str
31
+ actual_decision: str # "ALLOW", "BLOCK", "DENY", "ERROR"
32
+ actual_tier: str # parsed from stdout if available
33
+ matched: bool # expected_decision == actual_decision
34
+ regression_type: Optional[str] # None, "allow_to_block", "block_to_allow", "tier_change", "exit_code_change"
35
+ actual_metadata: dict[str, Any] = field(default_factory=dict)
36
+
37
+
38
+ _RE_TIER = re.compile(r"\bT[0-3]\b")
39
+
40
+
41
+ def _parse_decision_from_output(
42
+ exit_code: int, stdout: str
43
+ ) -> tuple[str, str]:
44
+ """Parse the hook decision and tier from stdout/exit_code.
45
+
46
+ Returns:
47
+ (decision, tier) tuple.
48
+ """
49
+ decision = "ALLOW"
50
+ tier = ""
51
+
52
+ if exit_code == 2:
53
+ decision = "BLOCK"
54
+ elif exit_code != 0:
55
+ decision = "ERROR"
56
+
57
+ # Try to parse structured JSON from stdout
58
+ stdout_stripped = stdout.strip()
59
+ if stdout_stripped:
60
+ # Hook output may have multiple lines; find the last JSON line
61
+ for line in reversed(stdout_stripped.splitlines()):
62
+ line = line.strip()
63
+ if not line.startswith("{"):
64
+ continue
65
+ try:
66
+ data = json.loads(line)
67
+ # Check for deny via hookSpecificOutput
68
+ hook_output = data.get("hookSpecificOutput", {})
69
+ perm_decision = hook_output.get("permissionDecision", "")
70
+ if perm_decision == "deny":
71
+ decision = "DENY"
72
+ break
73
+ except json.JSONDecodeError:
74
+ continue
75
+
76
+ return decision, tier
77
+
78
+
79
+ def _extract_tier_from_text(*texts: str) -> str:
80
+ """Return the first security tier found in the provided texts."""
81
+ for text in texts:
82
+ if not text:
83
+ continue
84
+ match = _RE_TIER.search(text)
85
+ if match:
86
+ return match.group(0)
87
+ return ""
88
+
89
+
90
+ def _parse_last_json_line(stdout: str) -> Optional[dict[str, Any]]:
91
+ """Parse the last JSON object emitted on stdout, if any."""
92
+ for line in reversed(stdout.strip().splitlines()):
93
+ stripped = line.strip()
94
+ if not stripped.startswith("{"):
95
+ continue
96
+ try:
97
+ return json.loads(stripped)
98
+ except json.JSONDecodeError:
99
+ continue
100
+ return None
101
+
102
+
103
+ def _classify_regression(
104
+ expected_decision: str,
105
+ actual_decision: str,
106
+ expected_exit_code: int,
107
+ actual_exit_code: int,
108
+ expected_tier: str,
109
+ actual_tier: str,
110
+ expected_metadata: Optional[dict[str, Any]] = None,
111
+ actual_metadata: Optional[dict[str, Any]] = None,
112
+ compare_tier: bool = True,
113
+ ) -> Optional[str]:
114
+ """Classify the type of regression, if any.
115
+
116
+ Returns:
117
+ None if no regression, or a string describing the regression type.
118
+ """
119
+ expected_metadata = expected_metadata or {}
120
+ actual_metadata = actual_metadata or {}
121
+
122
+ if expected_decision == actual_decision and expected_exit_code == actual_exit_code:
123
+ if compare_tier and expected_tier and actual_tier and expected_tier != actual_tier:
124
+ return "tier_change"
125
+ for key, expected_value in expected_metadata.items():
126
+ if key not in actual_metadata:
127
+ return f"{key}_missing"
128
+ if actual_metadata[key] != expected_value:
129
+ return f"{key}_change"
130
+ return None
131
+
132
+ if expected_decision == "ALLOW" and actual_decision == "BLOCK":
133
+ return "allow_to_block"
134
+ if expected_decision == "ALLOW" and actual_decision == "DENY":
135
+ return "allow_to_t3"
136
+ if expected_decision == "BLOCK" and actual_decision == "ALLOW":
137
+ return "block_to_allow"
138
+ if expected_decision == "DENY" and actual_decision == "ALLOW":
139
+ return "deny_to_allow"
140
+ if expected_exit_code != actual_exit_code:
141
+ return "exit_code_change"
142
+
143
+ return "decision_change"
144
+
145
+
146
+ class HookRunner:
147
+ """Executes hooks as subprocesses for replay testing.
148
+
149
+ Creates an isolated temporary project directory for each batch run,
150
+ mimicking the .claude/ directory structure that hooks expect.
151
+ """
152
+
153
+ def __init__(self, hooks_dir: Path, project_root: Optional[Path] = None):
154
+ """Initialize the runner.
155
+
156
+ Args:
157
+ hooks_dir: Path to the directory containing hook .py files.
158
+ project_root: Optional path to use as the simulated project root.
159
+ If None, a temporary directory is created per batch.
160
+ """
161
+ self.hooks_dir = hooks_dir
162
+ self.project_root = project_root
163
+ self._timeout = 30
164
+
165
+ def _state_file_path(self, work_dir: Path) -> Path:
166
+ """Return the hook state file path for a replay work directory."""
167
+ return work_dir / ".claude" / ".hooks_state.json"
168
+
169
+ def _load_hook_state(self, work_dir: Path) -> dict[str, Any]:
170
+ """Load hook state written by pre_tool_use, if present."""
171
+ path = self._state_file_path(work_dir)
172
+ if not path.exists():
173
+ return {}
174
+ try:
175
+ return json.loads(path.read_text())
176
+ except (OSError, json.JSONDecodeError):
177
+ return {}
178
+
179
+ def _prime_post_tool_use_state(self, event: ReplayEvent, work_dir: Path) -> None:
180
+ """Seed the pre-hook state so post_tool_use can replay faithfully."""
181
+ tool_input = event.stdin_payload.get("tool_input", {})
182
+ command = ""
183
+ if isinstance(tool_input, dict):
184
+ command = str(tool_input.get("command", ""))
185
+
186
+ state = {
187
+ "tool_name": event.tool_name,
188
+ "command": command,
189
+ "tier": event.expected_tier or "unknown",
190
+ "start_time": "2026-01-01T00:00:00",
191
+ "start_time_epoch": 0.0,
192
+ "session_id": event.stdin_payload.get("session_id", "replay"),
193
+ "pre_hook_result": "allowed",
194
+ "metadata": {},
195
+ }
196
+ self._state_file_path(work_dir).write_text(json.dumps(state))
197
+
198
+ def _read_latest_audit_record(self, work_dir: Path) -> dict[str, Any]:
199
+ """Read the most recent audit record emitted during replay, if any."""
200
+ logs_dir = work_dir / ".claude" / "logs"
201
+ audit_files = sorted(logs_dir.glob("audit-*.jsonl"))
202
+ if not audit_files:
203
+ return {}
204
+ lines = audit_files[-1].read_text(encoding="utf-8", errors="replace").splitlines()
205
+ for line in reversed(lines):
206
+ if not line.strip():
207
+ continue
208
+ try:
209
+ return json.loads(line)
210
+ except json.JSONDecodeError:
211
+ continue
212
+ return {}
213
+
214
+ def _parse_pre_tool_use_result(
215
+ self,
216
+ exit_code: int,
217
+ stdout: str,
218
+ stderr: str,
219
+ work_dir: Path,
220
+ ) -> tuple[str, str, dict[str, Any]]:
221
+ """Parse pre_tool_use results, including tier from hook state/log artifacts."""
222
+ decision, tier = _parse_decision_from_output(exit_code, stdout)
223
+ payload = _parse_last_json_line(stdout) or {}
224
+ hook_output = payload.get("hookSpecificOutput", {}) if isinstance(payload, dict) else {}
225
+
226
+ state = self._load_hook_state(work_dir)
227
+ if not tier:
228
+ tier = str(state.get("tier", "") or "")
229
+ if not tier:
230
+ tier = _extract_tier_from_text(
231
+ str(hook_output.get("permissionDecisionReason", "")),
232
+ stdout,
233
+ stderr,
234
+ )
235
+
236
+ actual_metadata: dict[str, Any] = {}
237
+ if "updatedInput" in hook_output:
238
+ actual_metadata["updated_input"] = hook_output["updatedInput"]
239
+ if "permissionDecisionReason" in hook_output:
240
+ actual_metadata["permission_reason"] = hook_output["permissionDecisionReason"]
241
+ return decision, tier, actual_metadata
242
+
243
+ def _parse_post_tool_use_result(
244
+ self,
245
+ exit_code: int,
246
+ stdout: str,
247
+ work_dir: Path,
248
+ ) -> tuple[str, str, dict[str, Any]]:
249
+ """Parse post_tool_use results using the audit record it just emitted."""
250
+ decision = "PASS" if exit_code == 0 else "ERROR"
251
+ audit_record = self._read_latest_audit_record(work_dir)
252
+ actual_tier = str(audit_record.get("tier", "") or "")
253
+ actual_metadata = {}
254
+ if audit_record:
255
+ actual_metadata["tool_exit_code"] = audit_record.get("exit_code")
256
+ actual_metadata["duration_ms"] = audit_record.get("duration_ms")
257
+ return decision, actual_tier, actual_metadata
258
+
259
+ def _parse_stop_hook_result(
260
+ self,
261
+ exit_code: int,
262
+ stdout: str,
263
+ ) -> tuple[str, str, dict[str, Any]]:
264
+ """Parse stop_hook results from its JSON stdout payload."""
265
+ decision = "PASS" if exit_code == 0 else "ERROR"
266
+ payload = _parse_last_json_line(stdout) or {}
267
+ actual_metadata: dict[str, Any] = {}
268
+ if payload:
269
+ for key in ("quality_sufficient", "score", "recommendation"):
270
+ if key in payload:
271
+ actual_metadata[key] = payload[key]
272
+ return decision, "", actual_metadata
273
+
274
+ def _parse_result(
275
+ self,
276
+ event: ReplayEvent,
277
+ exit_code: int,
278
+ stdout: str,
279
+ stderr: str,
280
+ work_dir: Path,
281
+ ) -> tuple[str, str, dict[str, Any]]:
282
+ """Dispatch hook-specific result parsing."""
283
+ if event.hook_name == "pre_tool_use":
284
+ return self._parse_pre_tool_use_result(exit_code, stdout, stderr, work_dir)
285
+ if event.hook_name == "post_tool_use":
286
+ return self._parse_post_tool_use_result(exit_code, stdout, work_dir)
287
+ if event.hook_name == "stop_hook":
288
+ return self._parse_stop_hook_result(exit_code, stdout)
289
+ return ("PASS" if exit_code == 0 else "ERROR", "", {})
290
+
291
+ def _setup_project_dir(self, base_dir: Path) -> Path:
292
+ """Create a minimal .claude/ directory structure for hooks.
293
+
294
+ Args:
295
+ base_dir: Directory to set up as the project root.
296
+
297
+ Returns:
298
+ The base_dir path.
299
+ """
300
+ claude_dir = base_dir / ".claude"
301
+ claude_dir.mkdir(parents=True, exist_ok=True)
302
+
303
+ # Logs directory
304
+ (claude_dir / "logs").mkdir(exist_ok=True)
305
+
306
+ # Session directory
307
+ session_dir = claude_dir / "session" / "active"
308
+ session_dir.mkdir(parents=True, exist_ok=True)
309
+
310
+ # Project context directory
311
+ pc_dir = claude_dir / "project-context"
312
+ pc_dir.mkdir(parents=True, exist_ok=True)
313
+
314
+ # Minimal project-context.json
315
+ minimal_context = {
316
+ "metadata": {
317
+ "version": "2.0",
318
+ "last_updated": "2026-01-01T00:00:00Z",
319
+ "scan_config": {
320
+ "last_scan": "2026-01-01T00:00:00Z",
321
+ "scanner_version": "0.1.0",
322
+ "staleness_hours": 24,
323
+ },
324
+ },
325
+ "paths": {},
326
+ "sections": {
327
+ "project_identity": {
328
+ "name": "replay-test",
329
+ "type": "application",
330
+ },
331
+ },
332
+ }
333
+ (pc_dir / "project-context.json").write_text(
334
+ json.dumps(minimal_context, indent=2)
335
+ )
336
+
337
+ # Workflow episodic memory dir
338
+ wem_dir = pc_dir / "workflow-episodic-memory"
339
+ wem_dir.mkdir(parents=True, exist_ok=True)
340
+ (wem_dir / "signals").mkdir(exist_ok=True)
341
+
342
+ # Config, memory, metrics directories
343
+ (claude_dir / "config").mkdir(exist_ok=True)
344
+ (claude_dir / "memory").mkdir(exist_ok=True)
345
+ (claude_dir / "metrics").mkdir(exist_ok=True)
346
+
347
+ # Settings.json
348
+ settings = {
349
+ "permissions": {"allow": ["Bash(*)"], "deny": []},
350
+ }
351
+ (claude_dir / "settings.json").write_text(json.dumps(settings, indent=2))
352
+
353
+ return base_dir
354
+
355
+ # Tools that the orchestrator is allowed to use directly.
356
+ # Payloads for these tools should NOT get agent_id injected, because
357
+ # they are orchestrator-level operations (dispatch, communication).
358
+ _ORCHESTRATOR_TOOLS = frozenset({
359
+ "agent", "task", "sendmessage", "skill",
360
+ "taskcreate", "taskupdate", "tasklist", "taskget",
361
+ "toolsearch", "websearch", "webfetch", "askuserquestion",
362
+ "stop", # stop_hook payloads are not subject to delegate mode
363
+ })
364
+
365
+ def _prepare_payload(self, event: ReplayEvent) -> str:
366
+ """Serialize the event payload for the hook subprocess.
367
+
368
+ Injects ``agent_id`` into tool-call payloads that lack one, so
369
+ delegate mode recognises them as subagent context instead of
370
+ blocking them as orchestrator calls. Agent/SendMessage/Task
371
+ payloads are left untouched since the orchestrator context is
372
+ correct for those.
373
+
374
+ Args:
375
+ event: The ReplayEvent being replayed.
376
+
377
+ Returns:
378
+ JSON string to feed to the hook subprocess via stdin.
379
+ """
380
+ payload = event.stdin_payload
381
+ tool_name = (payload.get("tool_name") or event.tool_name or "").lower()
382
+
383
+ if not payload.get("agent_id") and tool_name not in self._ORCHESTRATOR_TOOLS:
384
+ payload = {**payload, "agent_id": "replay-simulator"}
385
+
386
+ return json.dumps(payload)
387
+
388
+ def _resolve_hook_script(self, hook_name: str) -> Path:
389
+ """Resolve hook name to script path.
390
+
391
+ Args:
392
+ hook_name: Hook name like "pre_tool_use" or "subagent_stop".
393
+
394
+ Returns:
395
+ Path to the hook script.
396
+ """
397
+ script_name = f"{hook_name}.py"
398
+ return self.hooks_dir / script_name
399
+
400
+ def run(self, event: ReplayEvent, project_dir: Optional[Path] = None) -> ReplayResult:
401
+ """Run the hook with the event's stdin_payload and compare results.
402
+
403
+ Args:
404
+ event: The ReplayEvent to replay.
405
+ project_dir: Optional project directory to use. If None, uses
406
+ self.project_root or creates a temporary one.
407
+
408
+ Returns:
409
+ ReplayResult with actual vs expected comparison.
410
+ """
411
+ work_dir = project_dir or self.project_root
412
+ if work_dir is None:
413
+ tmp = tempfile.mkdtemp(prefix="replay_")
414
+ work_dir = Path(tmp)
415
+ self._setup_project_dir(work_dir)
416
+
417
+ script_path = self._resolve_hook_script(event.hook_name)
418
+ if not script_path.exists():
419
+ return ReplayResult(
420
+ event=event,
421
+ actual_exit_code=-1,
422
+ actual_stdout="",
423
+ actual_stderr=f"Hook script not found: {script_path}",
424
+ actual_decision="ERROR",
425
+ actual_tier="",
426
+ matched=False,
427
+ regression_type="missing_hook",
428
+ )
429
+
430
+ env = os.environ.copy()
431
+ env.pop("CLAUDE_PLUGIN_ROOT", None)
432
+
433
+ if event.hook_name == "post_tool_use":
434
+ self._prime_post_tool_use_state(event, work_dir)
435
+
436
+ try:
437
+ result = subprocess.run(
438
+ [sys.executable, str(script_path)],
439
+ input=self._prepare_payload(event),
440
+ capture_output=True,
441
+ text=True,
442
+ env=env,
443
+ timeout=self._timeout,
444
+ cwd=str(work_dir),
445
+ )
446
+ except subprocess.TimeoutExpired:
447
+ return ReplayResult(
448
+ event=event,
449
+ actual_exit_code=-1,
450
+ actual_stdout="",
451
+ actual_stderr="Timeout",
452
+ actual_decision="ERROR",
453
+ actual_tier="",
454
+ matched=False,
455
+ regression_type="timeout",
456
+ )
457
+ except OSError as exc:
458
+ return ReplayResult(
459
+ event=event,
460
+ actual_exit_code=-1,
461
+ actual_stdout="",
462
+ actual_stderr=str(exc),
463
+ actual_decision="ERROR",
464
+ actual_tier="",
465
+ matched=False,
466
+ regression_type="os_error",
467
+ )
468
+
469
+ actual_decision, actual_tier, actual_metadata = self._parse_result(
470
+ event,
471
+ result.returncode,
472
+ result.stdout,
473
+ result.stderr,
474
+ work_dir,
475
+ )
476
+
477
+ regression = _classify_regression(
478
+ event.expected_decision,
479
+ actual_decision,
480
+ event.expected_exit_code,
481
+ result.returncode,
482
+ event.expected_tier,
483
+ actual_tier,
484
+ expected_metadata=event.expected_metadata,
485
+ actual_metadata=actual_metadata,
486
+ compare_tier=event.compare_tier,
487
+ )
488
+
489
+ matched = regression is None
490
+
491
+ return ReplayResult(
492
+ event=event,
493
+ actual_exit_code=result.returncode,
494
+ actual_stdout=result.stdout,
495
+ actual_stderr=result.stderr,
496
+ actual_decision=actual_decision,
497
+ actual_tier=actual_tier,
498
+ matched=matched,
499
+ regression_type=regression,
500
+ actual_metadata=actual_metadata,
501
+ )
502
+
503
+ def run_batch(
504
+ self,
505
+ events: list[ReplayEvent],
506
+ progress_callback=None,
507
+ ) -> list[ReplayResult]:
508
+ """Run all events and return all results.
509
+
510
+ Creates a single isolated project directory for the batch to
511
+ share session state across sequential hook calls.
512
+
513
+ Args:
514
+ events: List of ReplayEvents to replay.
515
+ progress_callback: Optional callable(current, total) for progress.
516
+
517
+ Returns:
518
+ List of ReplayResult instances in the same order as events.
519
+ """
520
+ results: list[ReplayResult] = []
521
+
522
+ # Create a shared project directory for the batch
523
+ if self.project_root:
524
+ work_dir = self.project_root
525
+ else:
526
+ tmp = tempfile.mkdtemp(prefix="replay_batch_")
527
+ work_dir = Path(tmp)
528
+
529
+ self._setup_project_dir(work_dir)
530
+
531
+ total = len(events)
532
+ for idx, event in enumerate(events):
533
+ result = self.run(event, project_dir=work_dir)
534
+ results.append(result)
535
+
536
+ if progress_callback:
537
+ progress_callback(idx + 1, total)
538
+
539
+ return results