@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,920 @@
1
+ """
2
+ Integration tests for the full scan pipeline (M6: T034-T038).
3
+
4
+ Tests the ScanOrchestrator end-to-end with realistic project fixtures,
5
+ verifying all 6 scanners produce correct v2 sections and agent-enriched
6
+ data is preserved. Backward-compat sections are no longer produced.
7
+
8
+ Tasks:
9
+ T034: Full scan on mock DevOps project
10
+ T035: Scan on minimal project
11
+ T036: Idempotency test
12
+ T037: Scanner failure isolation
13
+ """
14
+
15
+ import copy
16
+ import json
17
+ import textwrap
18
+ import time
19
+ from pathlib import Path
20
+ from typing import Any, Dict
21
+ from unittest.mock import patch
22
+
23
+ import pytest
24
+
25
+ from tools.scan.config import ScanConfig
26
+ from tools.scan.merge import AGENT_ENRICHED_SECTIONS
27
+ from tools.scan.orchestrator import ScanOrchestrator, ScanOutput
28
+ from tools.scan.registry import ScannerRegistry
29
+ from tools.scan.scanners.base import BaseScanner, ScanResult
30
+ from tools.scan.tests.conftest import create_git_dir
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Fixtures
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ @pytest.fixture
39
+ def full_devops_project(tmp_path: Path) -> Path:
40
+ """Create a realistic multi-language DevOps project fixture.
41
+
42
+ Includes:
43
+ - package.json (Node.js with NestJS deps)
44
+ - pyproject.toml (Python)
45
+ - .tf files with GCP provider
46
+ - Dockerfile, docker-compose.yml
47
+ - .github/workflows/ directory
48
+ - .git/ directory with GitHub remote
49
+ - K8s manifests
50
+ - Helm Chart.yaml
51
+ - Flux kustomization
52
+ """
53
+ # -- Node.js / NestJS application --
54
+ pkg = {
55
+ "name": "devops-integration-app",
56
+ "version": "2.0.0",
57
+ "description": "Integration test DevOps application",
58
+ "dependencies": {
59
+ "express": "^4.18.0",
60
+ "@nestjs/core": "^10.0.0",
61
+ "@nestjs/common": "^10.0.0",
62
+ },
63
+ "devDependencies": {
64
+ "typescript": "^5.0.0",
65
+ },
66
+ }
67
+ (tmp_path / "package.json").write_text(json.dumps(pkg, indent=2))
68
+ (tmp_path / "package-lock.json").write_text("{}")
69
+ (tmp_path / "tsconfig.json").write_text('{"compilerOptions": {"strict": true}}')
70
+
71
+ # -- Python project --
72
+ pyproject = textwrap.dedent("""\
73
+ [project]
74
+ name = "devops-backend"
75
+ version = "1.0.0"
76
+ description = "Backend service"
77
+ dependencies = [
78
+ "fastapi>=0.100.0",
79
+ ]
80
+ """)
81
+ backend_dir = tmp_path / "backend"
82
+ backend_dir.mkdir()
83
+ (backend_dir / "pyproject.toml").write_text(pyproject)
84
+ (backend_dir / "requirements.txt").write_text("fastapi>=0.100.0\nuvicorn>=0.23.0\n")
85
+
86
+ # -- Terraform with GCP provider --
87
+ tf_dir = tmp_path / "terraform"
88
+ tf_dir.mkdir()
89
+ (tf_dir / "main.tf").write_text(textwrap.dedent("""\
90
+ provider "google" {
91
+ project = "my-gcp-project"
92
+ region = "us-central1"
93
+ }
94
+
95
+ resource "google_compute_instance" "default" {
96
+ name = "test"
97
+ machine_type = "e2-medium"
98
+ }
99
+ """))
100
+ (tf_dir / "variables.tf").write_text(textwrap.dedent("""\
101
+ variable "project_id" {
102
+ type = string
103
+ }
104
+ """))
105
+
106
+ # -- Container files --
107
+ (tmp_path / "Dockerfile").write_text("FROM node:20-alpine\nWORKDIR /app\nCOPY . .\n")
108
+ (tmp_path / "docker-compose.yml").write_text(textwrap.dedent("""\
109
+ version: "3.8"
110
+ services:
111
+ app:
112
+ build: .
113
+ ports:
114
+ - "3000:3000"
115
+ """))
116
+
117
+ # -- GitHub Actions CI --
118
+ workflows_dir = tmp_path / ".github" / "workflows"
119
+ workflows_dir.mkdir(parents=True)
120
+ (workflows_dir / "ci.yml").write_text(textwrap.dedent("""\
121
+ name: CI
122
+ on: [push]
123
+ jobs:
124
+ test:
125
+ runs-on: ubuntu-latest
126
+ steps:
127
+ - uses: actions/checkout@v4
128
+ """))
129
+
130
+ # -- Git directory with GitHub remote --
131
+ create_git_dir(
132
+ root=tmp_path,
133
+ remote_url="git@github.com:example/devops-integration-app.git",
134
+ default_branch="main",
135
+ extra_remotes={
136
+ "upstream": "https://github.com/upstream/devops-integration-app.git",
137
+ },
138
+ branches=["develop", "feature/scan-integration"],
139
+ )
140
+
141
+ # -- Kubernetes manifests --
142
+ k8s_dir = tmp_path / "k8s"
143
+ k8s_dir.mkdir()
144
+ (k8s_dir / "deployment.yaml").write_text(textwrap.dedent("""\
145
+ apiVersion: apps/v1
146
+ kind: Deployment
147
+ metadata:
148
+ name: devops-app
149
+ spec:
150
+ replicas: 3
151
+ """))
152
+ (k8s_dir / "service.yaml").write_text(textwrap.dedent("""\
153
+ apiVersion: v1
154
+ kind: Service
155
+ metadata:
156
+ name: devops-app
157
+ spec:
158
+ type: ClusterIP
159
+ """))
160
+
161
+ # -- Helm chart --
162
+ chart_dir = tmp_path / "charts" / "devops-app"
163
+ chart_dir.mkdir(parents=True)
164
+ (chart_dir / "Chart.yaml").write_text(textwrap.dedent("""\
165
+ apiVersion: v2
166
+ name: devops-app
167
+ version: 1.0.0
168
+ description: DevOps integration test chart
169
+ """))
170
+
171
+ # -- Flux GitOps --
172
+ gitops_dir = tmp_path / "gitops" / "clusters" / "dev"
173
+ gitops_dir.mkdir(parents=True)
174
+ (gitops_dir / "kustomization.yaml").write_text(textwrap.dedent("""\
175
+ apiVersion: kustomize.toolkit.fluxcd.io/v1
176
+ kind: Kustomization
177
+ metadata:
178
+ name: dev-cluster
179
+ namespace: flux-system
180
+ spec:
181
+ interval: 5m
182
+ path: ./gitops/clusters/dev
183
+ prune: true
184
+ """))
185
+
186
+ # -- Env files --
187
+ (tmp_path / ".env.example").write_text("")
188
+
189
+ return tmp_path
190
+
191
+
192
+ @pytest.fixture
193
+ def existing_agent_context(tmp_path: Path) -> Dict[str, Any]:
194
+ """Create a pre-existing project-context.json with agent-enriched data.
195
+
196
+ Simulates a scenario where agents previously populated cluster_details
197
+ and operational_guidelines.
198
+ """
199
+ return {
200
+ "metadata": {
201
+ "version": "2.0",
202
+ "last_updated": "2026-01-01T00:00:00Z",
203
+ "scan_config": {
204
+ "staleness_hours": 24,
205
+ "last_scan": "2026-01-01T00:00:00Z",
206
+ "scanner_version": "0.1.0",
207
+ },
208
+ },
209
+ "paths": {},
210
+ "sections": {
211
+ "cluster_details": {
212
+ "_source": "agent:cloud-troubleshooter",
213
+ "cluster_name": "prod-us-central1",
214
+ "node_count": 5,
215
+ },
216
+ "operational_guidelines": {
217
+ "_source": "agent:devops-developer",
218
+ "deployment_strategy": "blue-green",
219
+ "rollback_procedure": "manual",
220
+ },
221
+ },
222
+ }
223
+
224
+
225
+ def _make_orchestrator(
226
+ project_root: Path,
227
+ output_path: Path,
228
+ parallel: bool = False,
229
+ ) -> ScanOrchestrator:
230
+ """Build a ScanOrchestrator with an explicit output path.
231
+
232
+ Args:
233
+ project_root: Project root directory.
234
+ output_path: Path for project-context.json.
235
+ parallel: Whether to run scanners in parallel.
236
+
237
+ Returns:
238
+ Configured ScanOrchestrator.
239
+ """
240
+ config = ScanConfig(
241
+ project_root=project_root,
242
+ output_path=output_path,
243
+ parallel=parallel,
244
+ )
245
+ registry = ScannerRegistry()
246
+ return ScanOrchestrator(registry=registry, config=config)
247
+
248
+
249
+ # ===========================================================================
250
+ # T034: Full scan on mock DevOps project
251
+ # ===========================================================================
252
+
253
+
254
+ class TestFullDevOpsScan:
255
+ """Integration test: full scan on a realistic multi-language DevOps project."""
256
+
257
+ def test_full_scan_produces_valid_context(
258
+ self, full_devops_project: Path, tmp_path: Path
259
+ ) -> None:
260
+ """Full scan produces a valid project-context.json with all sections."""
261
+ output_path = tmp_path / "output" / "project-context.json"
262
+ orch = _make_orchestrator(full_devops_project, output_path)
263
+ result = orch.run(project_root=full_devops_project)
264
+
265
+ # ScanOutput should be returned
266
+ assert isinstance(result, ScanOutput)
267
+ # File should be written
268
+ assert output_path.is_file()
269
+
270
+ # Validate JSON structure
271
+ written = json.loads(output_path.read_text())
272
+ assert "metadata" in written
273
+ assert "sections" in written
274
+ assert written["metadata"]["version"] == "2.0"
275
+
276
+ def test_project_identity_from_package_json(
277
+ self, full_devops_project: Path, tmp_path: Path
278
+ ) -> None:
279
+ """project_identity section has name from package.json."""
280
+ output_path = tmp_path / "output" / "project-context.json"
281
+ orch = _make_orchestrator(full_devops_project, output_path)
282
+ result = orch.run(project_root=full_devops_project)
283
+
284
+ sections = result.context["sections"]
285
+ assert "project_identity" in sections
286
+ identity = sections["project_identity"]
287
+ assert identity["name"] == "devops-integration-app"
288
+ assert "_source" in identity
289
+
290
+ def test_stack_detects_languages_and_frameworks(
291
+ self, full_devops_project: Path, tmp_path: Path
292
+ ) -> None:
293
+ """stack section lists TypeScript, Python and detects NestJS framework."""
294
+ output_path = tmp_path / "output" / "project-context.json"
295
+ orch = _make_orchestrator(full_devops_project, output_path)
296
+ result = orch.run(project_root=full_devops_project)
297
+
298
+ sections = result.context["sections"]
299
+ assert "stack" in sections
300
+ stack = sections["stack"]
301
+
302
+ # Multi-language: TypeScript (package.json + tsconfig) and Python
303
+ lang_names = [l["name"] for l in stack["languages"]]
304
+ assert "typescript" in lang_names
305
+ assert "python" in lang_names
306
+
307
+ # NestJS framework detected from @nestjs/core
308
+ fw_names = [fw["name"] for fw in stack["frameworks"]]
309
+ assert "nestjs" in fw_names
310
+
311
+ def test_git_detects_github_platform(
312
+ self, full_devops_project: Path, tmp_path: Path
313
+ ) -> None:
314
+ """git section lists GitHub platform from .git/config."""
315
+ output_path = tmp_path / "output" / "project-context.json"
316
+ orch = _make_orchestrator(full_devops_project, output_path)
317
+ result = orch.run(project_root=full_devops_project)
318
+
319
+ sections = result.context["sections"]
320
+ assert "git" in sections
321
+ git = sections["git"]
322
+ assert git["platform"] == "github"
323
+ assert git["default_branch"] == "main"
324
+ assert len(git["remotes"]) >= 1
325
+
326
+ def test_infrastructure_detects_gcp_terraform_docker_ci(
327
+ self, full_devops_project: Path, tmp_path: Path
328
+ ) -> None:
329
+ """infrastructure section detects GCP, Terraform, Docker, GitHub Actions."""
330
+ output_path = tmp_path / "output" / "project-context.json"
331
+ orch = _make_orchestrator(full_devops_project, output_path)
332
+ result = orch.run(project_root=full_devops_project)
333
+
334
+ sections = result.context["sections"]
335
+ assert "infrastructure" in sections
336
+ infra = sections["infrastructure"]
337
+
338
+ # Cloud: GCP from terraform provider
339
+ cloud_names = [cp["name"] for cp in infra.get("cloud_providers", [])]
340
+ assert "gcp" in cloud_names
341
+
342
+ # IaC: Terraform detected
343
+ iac_tools = [i["tool"] for i in infra.get("iac", [])]
344
+ assert "terraform" in iac_tools
345
+
346
+ # CI/CD: GitHub Actions
347
+ ci_platforms = [c["platform"] for c in infra.get("ci_cd", [])]
348
+ assert "github-actions" in ci_platforms
349
+
350
+ # Containers: Docker
351
+ container_tools = [c["tool"] for c in infra.get("containers", [])]
352
+ assert "docker" in container_tools
353
+
354
+ def test_orchestration_detects_flux_k8s_helm(
355
+ self, full_devops_project: Path, tmp_path: Path
356
+ ) -> None:
357
+ """orchestration section detects Flux, Kubernetes manifests, and Helm."""
358
+ output_path = tmp_path / "output" / "project-context.json"
359
+ orch = _make_orchestrator(full_devops_project, output_path)
360
+ result = orch.run(project_root=full_devops_project)
361
+
362
+ sections = result.context["sections"]
363
+ assert "orchestration" in sections
364
+ orch_data = sections["orchestration"]
365
+
366
+ # GitOps: Flux detected
367
+ assert orch_data["gitops"]["tool"] == "flux"
368
+
369
+ # Kubernetes: manifests found
370
+ assert orch_data["kubernetes"]["detected"] is True
371
+
372
+ # Helm: chart detected
373
+ assert orch_data["helm"]["detected"] is True
374
+
375
+ def test_environment_has_os_info(
376
+ self, full_devops_project: Path, tmp_path: Path
377
+ ) -> None:
378
+ """environment section has OS information populated."""
379
+ output_path = tmp_path / "output" / "project-context.json"
380
+ orch = _make_orchestrator(full_devops_project, output_path)
381
+ result = orch.run(project_root=full_devops_project)
382
+
383
+ sections = result.context["sections"]
384
+ assert "environment" in sections
385
+ env = sections["environment"]
386
+ assert "os" in env
387
+ assert env["os"]["platform"] in ("linux", "darwin", "win32")
388
+ assert env["os"]["architecture"] in ("x64", "arm64")
389
+
390
+ def test_v2_sections_present_no_backward_compat(
391
+ self, full_devops_project: Path, tmp_path: Path
392
+ ) -> None:
393
+ """v2 scanner sections present; backward-compat sections NOT produced."""
394
+ output_path = tmp_path / "output" / "project-context.json"
395
+ orch = _make_orchestrator(full_devops_project, output_path)
396
+ result = orch.run(project_root=full_devops_project)
397
+
398
+ sections = result.context["sections"]
399
+ # v2 sections must be present
400
+ assert "project_identity" in sections
401
+ assert "stack" in sections
402
+ assert "git" in sections
403
+ assert "environment" in sections
404
+ assert "infrastructure" in sections
405
+ # backward-compat sections must NOT be produced
406
+ assert "project_details" not in sections
407
+ assert "application_architecture" not in sections
408
+ assert "development_standards" not in sections
409
+
410
+ def test_agent_enriched_data_preserved(
411
+ self,
412
+ full_devops_project: Path,
413
+ existing_agent_context: Dict[str, Any],
414
+ tmp_path: Path,
415
+ ) -> None:
416
+ """Pre-existing agent-enriched sections (cluster_details) are preserved."""
417
+ output_path = tmp_path / "output" / "project-context.json"
418
+ output_path.parent.mkdir(parents=True, exist_ok=True)
419
+
420
+ # Write existing context with agent-enriched data
421
+ output_path.write_text(json.dumps(existing_agent_context, indent=2))
422
+
423
+ orch = _make_orchestrator(full_devops_project, output_path)
424
+ result = orch.run(project_root=full_devops_project)
425
+
426
+ sections = result.context["sections"]
427
+ # Agent-enriched cluster_details should be preserved
428
+ assert "cluster_details" in sections
429
+ assert sections["cluster_details"]["cluster_name"] == "prod-us-central1"
430
+ assert sections["cluster_details"]["node_count"] == 5
431
+
432
+ # operational_guidelines should also be preserved
433
+ assert "operational_guidelines" in sections
434
+ assert sections["operational_guidelines"]["deployment_strategy"] == "blue-green"
435
+
436
+ def test_scan_completes_under_10_seconds(
437
+ self, full_devops_project: Path, tmp_path: Path
438
+ ) -> None:
439
+ """Full scan completes in under 10 seconds (NFR-001)."""
440
+ output_path = tmp_path / "output" / "project-context.json"
441
+ orch = _make_orchestrator(full_devops_project, output_path)
442
+
443
+ start = time.monotonic()
444
+ result = orch.run(project_root=full_devops_project)
445
+ elapsed = time.monotonic() - start
446
+
447
+ assert elapsed < 10.0, f"Scan took {elapsed:.2f}s, exceeds 10s NFR-001 limit"
448
+
449
+ def test_sections_updated_tracking(
450
+ self, full_devops_project: Path, tmp_path: Path
451
+ ) -> None:
452
+ """ScanOutput tracks which sections were updated."""
453
+ output_path = tmp_path / "output" / "project-context.json"
454
+ orch = _make_orchestrator(full_devops_project, output_path)
455
+ result = orch.run(project_root=full_devops_project)
456
+
457
+ assert len(result.sections_updated) > 0
458
+ # Core scanner-produced sections should be in the updated list
459
+ assert "project_identity" in result.sections_updated
460
+ assert "stack" in result.sections_updated
461
+ assert "git" in result.sections_updated
462
+
463
+ def test_json_schema_written_correctly(
464
+ self, full_devops_project: Path, tmp_path: Path
465
+ ) -> None:
466
+ """project-context.json has correct top-level schema."""
467
+ output_path = tmp_path / "output" / "project-context.json"
468
+ orch = _make_orchestrator(full_devops_project, output_path)
469
+ orch.run(project_root=full_devops_project)
470
+
471
+ written = json.loads(output_path.read_text())
472
+ # Top-level keys
473
+ assert "metadata" in written
474
+ assert "sections" in written
475
+ # Metadata fields
476
+ assert "last_updated" in written["metadata"]
477
+ assert "scan_config" in written["metadata"]
478
+ assert "scanner_version" in written["metadata"]["scan_config"]
479
+
480
+
481
+ # ===========================================================================
482
+ # T035: Scan on minimal project
483
+ # ===========================================================================
484
+
485
+
486
+ def _patch_host_detections():
487
+ """Return a list of mock patches that suppress host-level detections.
488
+
489
+ The infrastructure scanner checks ~/.aws/config, ~/.config/gcloud, etc.
490
+ The orchestration scanner checks ~/.kube/config and KUBECONFIG env var.
491
+ These detect host-level CLI configs that are not project-specific.
492
+ We patch them out so minimal-project tests are host-independent.
493
+ """
494
+ return [
495
+ patch(
496
+ "tools.scan.scanners.infrastructure.InfrastructureScanner"
497
+ "._detect_providers_from_cli_configs",
498
+ lambda self, providers: None,
499
+ ),
500
+ patch(
501
+ "tools.scan.scanners.infrastructure.InfrastructureScanner"
502
+ "._detect_providers_from_env_vars",
503
+ lambda self, providers: None,
504
+ ),
505
+ patch(
506
+ "tools.scan.scanners.orchestration.OrchestrationScanner"
507
+ "._find_kubeconfig",
508
+ lambda self: None,
509
+ ),
510
+ ]
511
+
512
+
513
+ class TestMinimalProjectScan:
514
+ """Integration test: scan on an empty or minimal project."""
515
+
516
+ def test_empty_directory_completes_without_error(
517
+ self, tmp_path: Path
518
+ ) -> None:
519
+ """Scan on empty directory completes without errors or crashes."""
520
+ output_path = tmp_path / "output" / "project-context.json"
521
+ orch = _make_orchestrator(tmp_path, output_path)
522
+ result = orch.run(project_root=tmp_path)
523
+
524
+ assert isinstance(result, ScanOutput)
525
+ assert len(result.errors) == 0
526
+
527
+ def test_empty_directory_has_environment_os(
528
+ self, tmp_path: Path
529
+ ) -> None:
530
+ """Empty directory scan still populates environment.os."""
531
+ output_path = tmp_path / "output" / "project-context.json"
532
+ orch = _make_orchestrator(tmp_path, output_path)
533
+ result = orch.run(project_root=tmp_path)
534
+
535
+ sections = result.context["sections"]
536
+ assert "environment" in sections
537
+ assert "os" in sections["environment"]
538
+ assert sections["environment"]["os"]["platform"] in ("linux", "darwin", "win32")
539
+
540
+ def test_empty_directory_has_project_identity(
541
+ self, tmp_path: Path
542
+ ) -> None:
543
+ """Empty directory has project_identity with type 'unknown' and dir name."""
544
+ output_path = tmp_path / "output" / "project-context.json"
545
+ orch = _make_orchestrator(tmp_path, output_path)
546
+ result = orch.run(project_root=tmp_path)
547
+
548
+ sections = result.context["sections"]
549
+ assert "project_identity" in sections
550
+ identity = sections["project_identity"]
551
+ assert identity["type"] == "unknown"
552
+ # Name should fall back to directory name
553
+ assert identity["name"] == tmp_path.name
554
+
555
+ def test_empty_directory_has_stack_with_empty_languages(
556
+ self, tmp_path: Path
557
+ ) -> None:
558
+ """Empty directory has stack section with empty languages list."""
559
+ output_path = tmp_path / "output" / "project-context.json"
560
+ orch = _make_orchestrator(tmp_path, output_path)
561
+ result = orch.run(project_root=tmp_path)
562
+
563
+ sections = result.context["sections"]
564
+ assert "stack" in sections
565
+ assert sections["stack"]["languages"] == []
566
+
567
+ def test_empty_directory_has_git_with_null_platform(
568
+ self, tmp_path: Path
569
+ ) -> None:
570
+ """Empty directory has git section with platform null."""
571
+ output_path = tmp_path / "output" / "project-context.json"
572
+ orch = _make_orchestrator(tmp_path, output_path)
573
+ result = orch.run(project_root=tmp_path)
574
+
575
+ sections = result.context["sections"]
576
+ assert "git" in sections
577
+ assert sections["git"]["platform"] is None
578
+
579
+ def test_empty_directory_no_infrastructure(
580
+ self, tmp_path: Path
581
+ ) -> None:
582
+ """Empty directory has no infrastructure section (project-level only).
583
+
584
+ Host-level CLI configs (e.g., ~/.aws/config, ~/.config/gcloud) are
585
+ patched out so this test only checks for project-level indicators.
586
+ """
587
+ output_path = tmp_path / "output" / "project-context.json"
588
+ patches = _patch_host_detections()
589
+ for p in patches:
590
+ p.start()
591
+ try:
592
+ orch = _make_orchestrator(tmp_path, output_path)
593
+ result = orch.run(project_root=tmp_path)
594
+ finally:
595
+ for p in patches:
596
+ p.stop()
597
+
598
+ sections = result.context["sections"]
599
+ assert "infrastructure" not in sections
600
+
601
+ def test_empty_directory_no_orchestration(
602
+ self, tmp_path: Path
603
+ ) -> None:
604
+ """Empty directory has no orchestration section (project-level only).
605
+
606
+ Host-level kubeconfig detection is patched out so this test only
607
+ checks for project-level Kubernetes/GitOps indicators.
608
+ """
609
+ output_path = tmp_path / "output" / "project-context.json"
610
+ patches = _patch_host_detections()
611
+ for p in patches:
612
+ p.start()
613
+ try:
614
+ orch = _make_orchestrator(tmp_path, output_path)
615
+ result = orch.run(project_root=tmp_path)
616
+ finally:
617
+ for p in patches:
618
+ p.stop()
619
+
620
+ sections = result.context["sections"]
621
+ assert "orchestration" not in sections
622
+
623
+ def test_minimal_readme_only_project(
624
+ self, tmp_path: Path
625
+ ) -> None:
626
+ """Minimal project with only README.md produces same base structure.
627
+
628
+ Host-level detections are patched out so dynamic sections (infrastructure,
629
+ orchestration) are only produced when project files indicate them.
630
+ """
631
+ (tmp_path / "README.md").write_text("# My Project\n")
632
+ output_path = tmp_path / "output" / "project-context.json"
633
+ patches = _patch_host_detections()
634
+ for p in patches:
635
+ p.start()
636
+ try:
637
+ orch = _make_orchestrator(tmp_path, output_path)
638
+ result = orch.run(project_root=tmp_path)
639
+ finally:
640
+ for p in patches:
641
+ p.stop()
642
+
643
+ sections = result.context["sections"]
644
+ # Same base sections as empty directory
645
+ assert "project_identity" in sections
646
+ assert "stack" in sections
647
+ assert "git" in sections
648
+ assert "environment" in sections
649
+ # No dynamic sections
650
+ assert "infrastructure" not in sections
651
+ assert "orchestration" not in sections
652
+
653
+ def test_minimal_project_writes_valid_json(
654
+ self, tmp_path: Path
655
+ ) -> None:
656
+ """Minimal project writes valid JSON to disk."""
657
+ (tmp_path / "README.md").write_text("# My Project\n")
658
+ output_path = tmp_path / "output" / "project-context.json"
659
+ orch = _make_orchestrator(tmp_path, output_path)
660
+ orch.run(project_root=tmp_path)
661
+
662
+ assert output_path.is_file()
663
+ written = json.loads(output_path.read_text())
664
+ assert "metadata" in written
665
+ assert "sections" in written
666
+
667
+
668
+ # ===========================================================================
669
+ # T036: Idempotency test
670
+ # ===========================================================================
671
+
672
+
673
+ class TestIdempotency:
674
+ """Integration test: running scan twice produces identical results.
675
+
676
+ True idempotency is measured from stabilized state: run 2 vs run 3,
677
+ both of which read back the written context before scanning.
678
+ """
679
+
680
+ def test_consecutive_scans_produce_identical_sections(
681
+ self, full_devops_project: Path, tmp_path: Path
682
+ ) -> None:
683
+ """Stabilized scans (run 2 and run 3) produce identical sections."""
684
+ output_path = tmp_path / "output" / "project-context.json"
685
+ orch = _make_orchestrator(full_devops_project, output_path)
686
+
687
+ # Run 1: initial scan (creates context from scratch)
688
+ orch.run(project_root=full_devops_project)
689
+ # Run 2: reads back context, merges -- this is the stabilized state
690
+ result2 = orch.run(project_root=full_devops_project)
691
+ # Run 3: should be identical to run 2
692
+ result3 = orch.run(project_root=full_devops_project)
693
+
694
+ assert result2.context["sections"] == result3.context["sections"]
695
+
696
+ def test_only_timestamps_differ_between_scans(
697
+ self, full_devops_project: Path, tmp_path: Path
698
+ ) -> None:
699
+ """Only metadata timestamps differ between stabilized scans."""
700
+ output_path = tmp_path / "output" / "project-context.json"
701
+ orch = _make_orchestrator(full_devops_project, output_path)
702
+
703
+ # Stabilize by running twice first
704
+ orch.run(project_root=full_devops_project)
705
+ result2 = orch.run(project_root=full_devops_project)
706
+ result3 = orch.run(project_root=full_devops_project)
707
+
708
+ ctx2 = copy.deepcopy(result2.context)
709
+ ctx3 = copy.deepcopy(result3.context)
710
+
711
+ # Remove timestamp fields
712
+ for ctx in (ctx2, ctx3):
713
+ ctx["metadata"].pop("last_updated", None)
714
+ if "scan_config" in ctx["metadata"]:
715
+ ctx["metadata"]["scan_config"].pop("last_scan", None)
716
+
717
+ assert ctx2 == ctx3
718
+
719
+ def test_clean_slate_scans_are_deterministic(
720
+ self, full_devops_project: Path, tmp_path: Path
721
+ ) -> None:
722
+ """Two independent clean-slate scans produce identical sections.
723
+
724
+ Each scan writes to a separate output path so there is no existing
725
+ context to merge with, isolating scanner-level determinism.
726
+ """
727
+ output1 = tmp_path / "out1" / "project-context.json"
728
+ output2 = tmp_path / "out2" / "project-context.json"
729
+
730
+ orch1 = _make_orchestrator(full_devops_project, output1)
731
+ orch2 = _make_orchestrator(full_devops_project, output2)
732
+
733
+ result1 = orch1.run(project_root=full_devops_project)
734
+ result2 = orch2.run(project_root=full_devops_project)
735
+
736
+ assert result1.context["sections"] == result2.context["sections"]
737
+
738
+ def test_agent_enriched_data_preserved_between_scans(
739
+ self,
740
+ full_devops_project: Path,
741
+ existing_agent_context: Dict[str, Any],
742
+ tmp_path: Path,
743
+ ) -> None:
744
+ """Agent-enriched data is preserved across multiple scans."""
745
+ output_path = tmp_path / "output" / "project-context.json"
746
+ output_path.parent.mkdir(parents=True, exist_ok=True)
747
+
748
+ # Write existing context with agent-enriched data
749
+ output_path.write_text(json.dumps(existing_agent_context, indent=2))
750
+
751
+ orch = _make_orchestrator(full_devops_project, output_path)
752
+
753
+ # Run scan twice
754
+ result1 = orch.run(project_root=full_devops_project)
755
+ result2 = orch.run(project_root=full_devops_project)
756
+
757
+ # Agent-enriched sections must survive both scans
758
+ for result in (result1, result2):
759
+ sections = result.context["sections"]
760
+ assert "cluster_details" in sections
761
+ assert sections["cluster_details"]["cluster_name"] == "prod-us-central1"
762
+ assert "operational_guidelines" in sections
763
+ assert sections["operational_guidelines"]["deployment_strategy"] == "blue-green"
764
+
765
+ def test_no_random_ordering_in_output(
766
+ self, full_devops_project: Path, tmp_path: Path
767
+ ) -> None:
768
+ """Scanner output is deterministic -- no random ordering in lists.
769
+
770
+ After stabilization (run 1 + run 2), subsequent runs produce
771
+ byte-identical sections.
772
+ """
773
+ output_path = tmp_path / "output" / "project-context.json"
774
+ orch = _make_orchestrator(full_devops_project, output_path)
775
+
776
+ # Stabilize
777
+ orch.run(project_root=full_devops_project)
778
+ orch.run(project_root=full_devops_project)
779
+
780
+ # Run 3 more times and compare
781
+ results = []
782
+ for _ in range(3):
783
+ result = orch.run(project_root=full_devops_project)
784
+ results.append(result.context["sections"])
785
+
786
+ assert results[0] == results[1]
787
+ assert results[1] == results[2]
788
+
789
+
790
+ # ===========================================================================
791
+ # T037: Scanner failure isolation
792
+ # ===========================================================================
793
+
794
+
795
+ class _FailingScanner(BaseScanner):
796
+ """A scanner that always raises RuntimeError for testing failure isolation."""
797
+
798
+ @property
799
+ def SCANNER_NAME(self) -> str:
800
+ return "stack"
801
+
802
+ @property
803
+ def SCANNER_VERSION(self) -> str:
804
+ return "1.0.0"
805
+
806
+ @property
807
+ def OWNED_SECTIONS(self):
808
+ return ["project_identity", "stack"]
809
+
810
+ def scan(self, root: Path) -> ScanResult:
811
+ raise RuntimeError("Simulated scanner failure for testing")
812
+
813
+
814
+ class TestScannerFailureIsolation:
815
+ """Integration test: single scanner failure does not abort entire scan."""
816
+
817
+ def _build_orchestrator_with_failing_stack(
818
+ self, project_root: Path, output_path: Path
819
+ ) -> ScanOrchestrator:
820
+ """Build an orchestrator with the stack scanner replaced by a failing one."""
821
+ config = ScanConfig(
822
+ project_root=project_root,
823
+ output_path=output_path,
824
+ parallel=False,
825
+ )
826
+ registry = ScannerRegistry()
827
+
828
+ # Replace the real stack scanner with the failing one
829
+ failing = _FailingScanner()
830
+ if "stack" in registry._scanners:
831
+ # Remove the existing stack scanner and its section ownership
832
+ old_scanner = registry._scanners.pop("stack")
833
+ for section in old_scanner.OWNED_SECTIONS:
834
+ registry._section_owners.pop(section, None)
835
+
836
+ # Register the failing scanner
837
+ registry._scanners[failing.SCANNER_NAME] = failing
838
+ for section in failing.OWNED_SECTIONS:
839
+ registry._section_owners[section] = failing.SCANNER_NAME
840
+
841
+ return ScanOrchestrator(registry=registry, config=config)
842
+
843
+ def test_scan_completes_despite_scanner_failure(
844
+ self, full_devops_project: Path, tmp_path: Path
845
+ ) -> None:
846
+ """Scan completes without raising when one scanner fails."""
847
+ output_path = tmp_path / "output" / "project-context.json"
848
+ orch = self._build_orchestrator_with_failing_stack(
849
+ full_devops_project, output_path
850
+ )
851
+
852
+ # Should NOT raise
853
+ result = orch.run(project_root=full_devops_project)
854
+ assert isinstance(result, ScanOutput)
855
+
856
+ def test_warning_recorded_for_failed_scanner(
857
+ self, full_devops_project: Path, tmp_path: Path
858
+ ) -> None:
859
+ """ScanOutput.warnings includes a message about the failed scanner."""
860
+ output_path = tmp_path / "output" / "project-context.json"
861
+ orch = self._build_orchestrator_with_failing_stack(
862
+ full_devops_project, output_path
863
+ )
864
+ result = orch.run(project_root=full_devops_project)
865
+
866
+ # Should have a warning about the stack scanner failure
867
+ warning_text = " ".join(result.warnings)
868
+ assert "stack" in warning_text.lower() or "RuntimeError" in warning_text
869
+
870
+ def test_failed_scanner_sections_absent(
871
+ self, full_devops_project: Path, tmp_path: Path
872
+ ) -> None:
873
+ """project_identity and stack sections are absent (failed scanner's sections)."""
874
+ output_path = tmp_path / "output" / "project-context.json"
875
+ orch = self._build_orchestrator_with_failing_stack(
876
+ full_devops_project, output_path
877
+ )
878
+ result = orch.run(project_root=full_devops_project)
879
+
880
+ sections = result.context["sections"]
881
+ # The stack scanner failed, so its owned sections should not be present
882
+ assert "project_identity" not in sections
883
+ assert "stack" not in sections
884
+
885
+ def test_other_scanners_still_produce_output(
886
+ self, full_devops_project: Path, tmp_path: Path
887
+ ) -> None:
888
+ """git, environment, infrastructure, and orchestration still produce output."""
889
+ output_path = tmp_path / "output" / "project-context.json"
890
+ orch = self._build_orchestrator_with_failing_stack(
891
+ full_devops_project, output_path
892
+ )
893
+ result = orch.run(project_root=full_devops_project)
894
+
895
+ sections = result.context["sections"]
896
+
897
+ # Other scanners should still produce their sections
898
+ assert "git" in sections
899
+ assert sections["git"]["platform"] == "github"
900
+
901
+ assert "environment" in sections
902
+ assert "os" in sections["environment"]
903
+
904
+ assert "infrastructure" in sections
905
+ assert "orchestration" in sections
906
+
907
+ def test_scanner_results_include_failed_scanner(
908
+ self, full_devops_project: Path, tmp_path: Path
909
+ ) -> None:
910
+ """scanner_results includes the failed scanner with empty sections."""
911
+ output_path = tmp_path / "output" / "project-context.json"
912
+ orch = self._build_orchestrator_with_failing_stack(
913
+ full_devops_project, output_path
914
+ )
915
+ result = orch.run(project_root=full_devops_project)
916
+
917
+ assert "stack" in result.scanner_results
918
+ failed_result = result.scanner_results["stack"]
919
+ assert failed_result.sections == {}
920
+ assert len(failed_result.warnings) > 0