@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,604 @@
1
+ """
2
+ Unit tests for the Stack Scanner (T020).
3
+
4
+ Tests language detection, framework detection, build tool detection,
5
+ monorepo detection, and project_identity extraction.
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Any, Dict
11
+
12
+ import pytest
13
+
14
+ from tools.scan.scanners.stack import StackScanner
15
+
16
+
17
+ @pytest.fixture
18
+ def scanner() -> StackScanner:
19
+ """Create a StackScanner instance."""
20
+ return StackScanner()
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Scanner basics
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ class TestStackScannerBasics:
29
+ """Test scanner metadata and basic contract."""
30
+
31
+ def test_scanner_name(self, scanner: StackScanner) -> None:
32
+ assert scanner.SCANNER_NAME == "stack"
33
+
34
+ def test_scanner_version(self, scanner: StackScanner) -> None:
35
+ assert scanner.SCANNER_VERSION == "1.1.0"
36
+
37
+ def test_owned_sections(self, scanner: StackScanner) -> None:
38
+ assert "project_identity" in scanner.OWNED_SECTIONS
39
+ assert "stack" in scanner.OWNED_SECTIONS
40
+
41
+ def test_source_tag(self, scanner: StackScanner) -> None:
42
+ assert scanner.source_tag == "scanner:stack"
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Language detection
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ class TestLanguageDetection:
51
+ """Test language detection from manifest files."""
52
+
53
+ def test_detect_nodejs_from_package_json(
54
+ self, scanner: StackScanner, node_project: Path
55
+ ) -> None:
56
+ result = scanner.scan(node_project)
57
+ languages = result.sections["stack"]["languages"]
58
+ lang_names = [lang["name"] for lang in languages]
59
+ assert "javascript" in lang_names
60
+
61
+ def test_detect_typescript_with_tsconfig(
62
+ self, scanner: StackScanner, node_project: Path
63
+ ) -> None:
64
+ (node_project / "tsconfig.json").write_text("{}")
65
+ result = scanner.scan(node_project)
66
+ languages = result.sections["stack"]["languages"]
67
+ lang_names = [lang["name"] for lang in languages]
68
+ assert "typescript" in lang_names
69
+ assert "javascript" not in lang_names
70
+
71
+ def test_detect_typescript_with_tsconfig_in_subdirectory(
72
+ self, scanner: StackScanner, tmp_path: Path
73
+ ) -> None:
74
+ """TypeScript detected when tsconfig.json is in a monorepo subdirectory."""
75
+ pkg = {"name": "mono", "private": True, "workspaces": ["packages/*"]}
76
+ (tmp_path / "package.json").write_text(json.dumps(pkg))
77
+ sub = tmp_path / "packages" / "api"
78
+ sub.mkdir(parents=True)
79
+ (sub / "package.json").write_text('{"name": "@mono/api"}')
80
+ (sub / "tsconfig.json").write_text("{}")
81
+ result = scanner.scan(tmp_path)
82
+ languages = result.sections["stack"]["languages"]
83
+ lang_names = [lang["name"] for lang in languages]
84
+ assert "typescript" in lang_names
85
+ assert "javascript" not in lang_names
86
+
87
+ def test_detect_typescript_with_tsconfig_build_variant(
88
+ self, scanner: StackScanner, node_project: Path
89
+ ) -> None:
90
+ """TypeScript detected from tsconfig.build.json (glob pattern)."""
91
+ (node_project / "tsconfig.build.json").write_text("{}")
92
+ result = scanner.scan(node_project)
93
+ languages = result.sections["stack"]["languages"]
94
+ lang_names = [lang["name"] for lang in languages]
95
+ assert "typescript" in lang_names
96
+
97
+ def test_detect_typescript_from_ts_extension(
98
+ self, scanner: StackScanner, node_project: Path
99
+ ) -> None:
100
+ """TypeScript detected from .ts file extension even without tsconfig."""
101
+ (node_project / "index.ts").write_text("console.log('hello');")
102
+ result = scanner.scan(node_project)
103
+ languages = result.sections["stack"]["languages"]
104
+ lang_names = [lang["name"] for lang in languages]
105
+ assert "typescript" in lang_names
106
+
107
+ def test_detect_python_from_pyproject_toml(
108
+ self, scanner: StackScanner, python_project: Path
109
+ ) -> None:
110
+ result = scanner.scan(python_project)
111
+ languages = result.sections["stack"]["languages"]
112
+ lang_names = [lang["name"] for lang in languages]
113
+ assert "python" in lang_names
114
+
115
+ def test_detect_python_from_setup_py(
116
+ self, scanner: StackScanner, tmp_path: Path
117
+ ) -> None:
118
+ (tmp_path / "setup.py").write_text(
119
+ 'from setuptools import setup\nsetup(name="test")\n'
120
+ )
121
+ result = scanner.scan(tmp_path)
122
+ languages = result.sections["stack"]["languages"]
123
+ lang_names = [lang["name"] for lang in languages]
124
+ assert "python" in lang_names
125
+
126
+ def test_detect_python_from_requirements_txt(
127
+ self, scanner: StackScanner, tmp_path: Path
128
+ ) -> None:
129
+ (tmp_path / "requirements.txt").write_text("flask>=2.0\n")
130
+ result = scanner.scan(tmp_path)
131
+ languages = result.sections["stack"]["languages"]
132
+ lang_names = [lang["name"] for lang in languages]
133
+ assert "python" in lang_names
134
+
135
+ def test_detect_go_from_go_mod(
136
+ self, scanner: StackScanner, go_project: Path
137
+ ) -> None:
138
+ result = scanner.scan(go_project)
139
+ languages = result.sections["stack"]["languages"]
140
+ lang_names = [lang["name"] for lang in languages]
141
+ assert "go" in lang_names
142
+
143
+ def test_detect_rust_from_cargo_toml(
144
+ self, scanner: StackScanner, rust_project: Path
145
+ ) -> None:
146
+ result = scanner.scan(rust_project)
147
+ languages = result.sections["stack"]["languages"]
148
+ lang_names = [lang["name"] for lang in languages]
149
+ assert "rust" in lang_names
150
+
151
+ def test_detect_java_from_pom_xml(
152
+ self, scanner: StackScanner, java_maven_project: Path
153
+ ) -> None:
154
+ result = scanner.scan(java_maven_project)
155
+ languages = result.sections["stack"]["languages"]
156
+ lang_names = [lang["name"] for lang in languages]
157
+ assert "java" in lang_names
158
+
159
+ def test_detect_java_from_build_gradle(
160
+ self, scanner: StackScanner, java_gradle_project: Path
161
+ ) -> None:
162
+ result = scanner.scan(java_gradle_project)
163
+ languages = result.sections["stack"]["languages"]
164
+ lang_names = [lang["name"] for lang in languages]
165
+ assert "java" in lang_names
166
+
167
+ def test_detect_php_from_composer_json(
168
+ self, scanner: StackScanner, php_project: Path
169
+ ) -> None:
170
+ result = scanner.scan(php_project)
171
+ languages = result.sections["stack"]["languages"]
172
+ lang_names = [lang["name"] for lang in languages]
173
+ assert "php" in lang_names
174
+
175
+ def test_detect_ruby_from_gemfile(
176
+ self, scanner: StackScanner, ruby_project: Path
177
+ ) -> None:
178
+ result = scanner.scan(ruby_project)
179
+ languages = result.sections["stack"]["languages"]
180
+ lang_names = [lang["name"] for lang in languages]
181
+ assert "ruby" in lang_names
182
+
183
+ def test_detect_csharp_from_csproj(
184
+ self, scanner: StackScanner, csharp_project: Path
185
+ ) -> None:
186
+ result = scanner.scan(csharp_project)
187
+ languages = result.sections["stack"]["languages"]
188
+ lang_names = [lang["name"] for lang in languages]
189
+ assert "csharp" in lang_names
190
+
191
+ def test_primary_language_flag(
192
+ self, scanner: StackScanner, node_project: Path
193
+ ) -> None:
194
+ result = scanner.scan(node_project)
195
+ languages = result.sections["stack"]["languages"]
196
+ primary_langs = [lang for lang in languages if lang.get("primary")]
197
+ assert len(primary_langs) == 1
198
+
199
+ def test_empty_project_no_languages(
200
+ self, scanner: StackScanner, empty_project: Path
201
+ ) -> None:
202
+ result = scanner.scan(empty_project)
203
+ languages = result.sections["stack"]["languages"]
204
+ assert languages == []
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Framework detection
209
+ # ---------------------------------------------------------------------------
210
+
211
+
212
+ class TestFrameworkDetection:
213
+ """Test framework detection from dependency declarations."""
214
+
215
+ def test_detect_nestjs_from_nestjs_core(
216
+ self, scanner: StackScanner, node_project: Path
217
+ ) -> None:
218
+ result = scanner.scan(node_project)
219
+ frameworks = result.sections["stack"]["frameworks"]
220
+ fw_names = [fw["name"] for fw in frameworks]
221
+ assert "nestjs" in fw_names
222
+
223
+ def test_detect_express(
224
+ self, scanner: StackScanner, node_project: Path
225
+ ) -> None:
226
+ result = scanner.scan(node_project)
227
+ frameworks = result.sections["stack"]["frameworks"]
228
+ fw_names = [fw["name"] for fw in frameworks]
229
+ assert "express" in fw_names
230
+
231
+ def test_detect_react(
232
+ self, scanner: StackScanner, node_project: Path
233
+ ) -> None:
234
+ result = scanner.scan(node_project)
235
+ frameworks = result.sections["stack"]["frameworks"]
236
+ fw_names = [fw["name"] for fw in frameworks]
237
+ assert "react" in fw_names
238
+
239
+ def test_detect_fastapi_from_pyproject(
240
+ self, scanner: StackScanner, python_project: Path
241
+ ) -> None:
242
+ result = scanner.scan(python_project)
243
+ frameworks = result.sections["stack"]["frameworks"]
244
+ fw_names = [fw["name"] for fw in frameworks]
245
+ assert "fastapi" in fw_names
246
+
247
+ def test_detect_fastapi_from_requirements_txt(
248
+ self, scanner: StackScanner, tmp_path: Path
249
+ ) -> None:
250
+ (tmp_path / "requirements.txt").write_text("fastapi>=0.100.0\nuvicorn\n")
251
+ result = scanner.scan(tmp_path)
252
+ frameworks = result.sections["stack"]["frameworks"]
253
+ fw_names = [fw["name"] for fw in frameworks]
254
+ assert "fastapi" in fw_names
255
+
256
+ def test_detect_flask(
257
+ self, scanner: StackScanner, tmp_path: Path
258
+ ) -> None:
259
+ (tmp_path / "requirements.txt").write_text("flask>=2.0\n")
260
+ result = scanner.scan(tmp_path)
261
+ frameworks = result.sections["stack"]["frameworks"]
262
+ fw_names = [fw["name"] for fw in frameworks]
263
+ assert "flask" in fw_names
264
+
265
+ def test_detect_django(
266
+ self, scanner: StackScanner, tmp_path: Path
267
+ ) -> None:
268
+ (tmp_path / "requirements.txt").write_text("django>=4.0\n")
269
+ result = scanner.scan(tmp_path)
270
+ frameworks = result.sections["stack"]["frameworks"]
271
+ fw_names = [fw["name"] for fw in frameworks]
272
+ assert "django" in fw_names
273
+
274
+ def test_nestjs_promoted_above_express(
275
+ self, scanner: StackScanner, node_project: Path
276
+ ) -> None:
277
+ """NestJS should be first framework when both NestJS and Express present."""
278
+ result = scanner.scan(node_project)
279
+ frameworks = result.sections["stack"]["frameworks"]
280
+ fw_names = [fw["name"] for fw in frameworks]
281
+ assert fw_names[0] == "nestjs"
282
+
283
+ def test_express_marked_as_underlying_when_nestjs_present(
284
+ self, scanner: StackScanner, node_project: Path
285
+ ) -> None:
286
+ """Express should be marked as underlying when NestJS is detected."""
287
+ result = scanner.scan(node_project)
288
+ frameworks = result.sections["stack"]["frameworks"]
289
+ express = [fw for fw in frameworks if fw["name"] == "express"][0]
290
+ assert express.get("role") == "underlying"
291
+
292
+ def test_detect_vue(
293
+ self, scanner: StackScanner, tmp_path: Path
294
+ ) -> None:
295
+ pkg = {"name": "vue-app", "dependencies": {"vue": "^3.0.0"}}
296
+ (tmp_path / "package.json").write_text(json.dumps(pkg))
297
+ result = scanner.scan(tmp_path)
298
+ frameworks = result.sections["stack"]["frameworks"]
299
+ fw_names = [fw["name"] for fw in frameworks]
300
+ assert "vue" in fw_names
301
+
302
+ def test_detect_angular(
303
+ self, scanner: StackScanner, tmp_path: Path
304
+ ) -> None:
305
+ pkg = {"name": "angular-app", "dependencies": {"@angular/core": "^17.0.0"}}
306
+ (tmp_path / "package.json").write_text(json.dumps(pkg))
307
+ result = scanner.scan(tmp_path)
308
+ frameworks = result.sections["stack"]["frameworks"]
309
+ fw_names = [fw["name"] for fw in frameworks]
310
+ assert "angular" in fw_names
311
+
312
+ def test_detect_nextjs(
313
+ self, scanner: StackScanner, tmp_path: Path
314
+ ) -> None:
315
+ pkg = {"name": "next-app", "dependencies": {"next": "^14.0.0", "react": "^18.0.0"}}
316
+ (tmp_path / "package.json").write_text(json.dumps(pkg))
317
+ result = scanner.scan(tmp_path)
318
+ frameworks = result.sections["stack"]["frameworks"]
319
+ fw_names = [fw["name"] for fw in frameworks]
320
+ assert "next.js" in fw_names
321
+
322
+ def test_framework_version_extracted(
323
+ self, scanner: StackScanner, node_project: Path
324
+ ) -> None:
325
+ result = scanner.scan(node_project)
326
+ frameworks = result.sections["stack"]["frameworks"]
327
+ express = [fw for fw in frameworks if fw["name"] == "express"][0]
328
+ assert express["version"] is not None
329
+ assert "4.18.0" in express["version"]
330
+
331
+ def test_empty_project_no_frameworks(
332
+ self, scanner: StackScanner, empty_project: Path
333
+ ) -> None:
334
+ result = scanner.scan(empty_project)
335
+ frameworks = result.sections["stack"]["frameworks"]
336
+ assert frameworks == []
337
+
338
+
339
+ # ---------------------------------------------------------------------------
340
+ # Build tool detection
341
+ # ---------------------------------------------------------------------------
342
+
343
+
344
+ class TestBuildToolDetection:
345
+ """Test build tool detection from lock files and manifests."""
346
+
347
+ def test_detect_npm_from_package_lock(
348
+ self, scanner: StackScanner, node_project: Path
349
+ ) -> None:
350
+ result = scanner.scan(node_project)
351
+ build_tools = result.sections["stack"]["build_tools"]
352
+ tool_names = [t["name"] for t in build_tools]
353
+ assert "npm" in tool_names
354
+
355
+ def test_detect_pnpm_from_lock(
356
+ self, scanner: StackScanner, tmp_path: Path
357
+ ) -> None:
358
+ (tmp_path / "package.json").write_text('{"name": "test"}')
359
+ (tmp_path / "pnpm-lock.yaml").write_text("lockfileVersion: 5.4\n")
360
+ result = scanner.scan(tmp_path)
361
+ build_tools = result.sections["stack"]["build_tools"]
362
+ tool_names = [t["name"] for t in build_tools]
363
+ assert "pnpm" in tool_names
364
+
365
+ def test_detect_yarn_from_lock(
366
+ self, scanner: StackScanner, tmp_path: Path
367
+ ) -> None:
368
+ (tmp_path / "package.json").write_text('{"name": "test"}')
369
+ (tmp_path / "yarn.lock").write_text("# yarn lockfile v1\n")
370
+ result = scanner.scan(tmp_path)
371
+ build_tools = result.sections["stack"]["build_tools"]
372
+ tool_names = [t["name"] for t in build_tools]
373
+ assert "yarn" in tool_names
374
+
375
+ def test_detect_cargo_from_lock(
376
+ self, scanner: StackScanner, rust_project: Path
377
+ ) -> None:
378
+ result = scanner.scan(rust_project)
379
+ build_tools = result.sections["stack"]["build_tools"]
380
+ tool_names = [t["name"] for t in build_tools]
381
+ assert "cargo" in tool_names
382
+
383
+ def test_detect_go_build_tool(
384
+ self, scanner: StackScanner, go_project: Path
385
+ ) -> None:
386
+ result = scanner.scan(go_project)
387
+ build_tools = result.sections["stack"]["build_tools"]
388
+ tool_names = [t["name"] for t in build_tools]
389
+ assert "go" in tool_names
390
+
391
+ def test_detect_maven_from_pom(
392
+ self, scanner: StackScanner, java_maven_project: Path
393
+ ) -> None:
394
+ result = scanner.scan(java_maven_project)
395
+ build_tools = result.sections["stack"]["build_tools"]
396
+ tool_names = [t["name"] for t in build_tools]
397
+ assert "maven" in tool_names
398
+
399
+ def test_detect_gradle_from_build_gradle(
400
+ self, scanner: StackScanner, java_gradle_project: Path
401
+ ) -> None:
402
+ result = scanner.scan(java_gradle_project)
403
+ build_tools = result.sections["stack"]["build_tools"]
404
+ tool_names = [t["name"] for t in build_tools]
405
+ assert "gradle" in tool_names
406
+
407
+ def test_detect_pip_from_requirements(
408
+ self, scanner: StackScanner, tmp_path: Path
409
+ ) -> None:
410
+ (tmp_path / "requirements.txt").write_text("flask>=2.0\n")
411
+ result = scanner.scan(tmp_path)
412
+ build_tools = result.sections["stack"]["build_tools"]
413
+ tool_names = [t["name"] for t in build_tools]
414
+ assert "pip" in tool_names
415
+
416
+ def test_detect_turborepo_in_build_tools(
417
+ self, scanner: StackScanner, monorepo_project: Path
418
+ ) -> None:
419
+ """Turborepo should appear in build_tools when turbo.json exists."""
420
+ result = scanner.scan(monorepo_project)
421
+ build_tools = result.sections["stack"]["build_tools"]
422
+ tool_names = [t["name"] for t in build_tools]
423
+ assert "turborepo" in tool_names
424
+
425
+ def test_turborepo_build_tool_has_config_file(
426
+ self, scanner: StackScanner, monorepo_project: Path
427
+ ) -> None:
428
+ """Turborepo build tool entry should reference the config file."""
429
+ result = scanner.scan(monorepo_project)
430
+ build_tools = result.sections["stack"]["build_tools"]
431
+ turbo = [t for t in build_tools if t["name"] == "turborepo"][0]
432
+ assert turbo["detected_by"] == "config_file"
433
+ assert turbo["config_file"] == "turbo.json"
434
+
435
+ def test_turborepo_detected_in_subdirectory(
436
+ self, scanner: StackScanner, tmp_path: Path
437
+ ) -> None:
438
+ """Turborepo detected when turbo.json is in a monorepo subdirectory."""
439
+ sub = tmp_path / "qxo-monorepo"
440
+ sub.mkdir()
441
+ (sub / "turbo.json").write_text('{"pipeline": {}}')
442
+ (sub / "package.json").write_text('{"name": "mono", "private": true}')
443
+ result = scanner.scan(tmp_path)
444
+ build_tools = result.sections["stack"]["build_tools"]
445
+ tool_names = [t["name"] for t in build_tools]
446
+ assert "turborepo" in tool_names
447
+
448
+ def test_empty_project_no_build_tools(
449
+ self, scanner: StackScanner, empty_project: Path
450
+ ) -> None:
451
+ result = scanner.scan(empty_project)
452
+ build_tools = result.sections["stack"]["build_tools"]
453
+ assert build_tools == []
454
+
455
+
456
+ # ---------------------------------------------------------------------------
457
+ # Monorepo detection
458
+ # ---------------------------------------------------------------------------
459
+
460
+
461
+ class TestMonorepoDetection:
462
+ """Test monorepo detection from workspace configs."""
463
+
464
+ def test_detect_monorepo_with_turbo(
465
+ self, scanner: StackScanner, monorepo_project: Path
466
+ ) -> None:
467
+ result = scanner.scan(monorepo_project)
468
+ identity = result.sections["project_identity"]
469
+ assert identity["monorepo"]["detected"] is True
470
+ assert identity["monorepo"]["tool"] == "turborepo"
471
+
472
+ def test_detect_monorepo_multiple_languages(
473
+ self, scanner: StackScanner, monorepo_project: Path
474
+ ) -> None:
475
+ result = scanner.scan(monorepo_project)
476
+ languages = result.sections["stack"]["languages"]
477
+ lang_names = [lang["name"] for lang in languages]
478
+ assert "javascript" in lang_names
479
+ assert "python" in lang_names
480
+
481
+ def test_detect_pnpm_workspace_monorepo(
482
+ self, scanner: StackScanner, tmp_path: Path
483
+ ) -> None:
484
+ (tmp_path / "package.json").write_text('{"name": "mono", "private": true}')
485
+ (tmp_path / "pnpm-workspace.yaml").write_text("packages:\n - 'apps/*'\n")
486
+ result = scanner.scan(tmp_path)
487
+ identity = result.sections["project_identity"]
488
+ assert identity["monorepo"]["detected"] is True
489
+
490
+ def test_detect_npm_workspaces_monorepo(
491
+ self, scanner: StackScanner, tmp_path: Path
492
+ ) -> None:
493
+ pkg = {"name": "mono", "private": True, "workspaces": ["packages/*"]}
494
+ (tmp_path / "package.json").write_text(json.dumps(pkg))
495
+ result = scanner.scan(tmp_path)
496
+ identity = result.sections["project_identity"]
497
+ assert identity["monorepo"]["detected"] is True
498
+
499
+ def test_no_monorepo_single_language(
500
+ self, scanner: StackScanner, node_project: Path
501
+ ) -> None:
502
+ result = scanner.scan(node_project)
503
+ identity = result.sections["project_identity"]
504
+ assert identity["monorepo"]["detected"] is False
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # Project identity
509
+ # ---------------------------------------------------------------------------
510
+
511
+
512
+ class TestProjectIdentity:
513
+ """Test project identity extraction."""
514
+
515
+ def test_name_from_package_json(
516
+ self, scanner: StackScanner, node_project: Path
517
+ ) -> None:
518
+ result = scanner.scan(node_project)
519
+ identity = result.sections["project_identity"]
520
+ assert identity["name"] == "test-node-project"
521
+
522
+ def test_name_from_pyproject_toml(
523
+ self, scanner: StackScanner, python_project: Path
524
+ ) -> None:
525
+ result = scanner.scan(python_project)
526
+ identity = result.sections["project_identity"]
527
+ assert identity["name"] == "test-python-project"
528
+
529
+ def test_name_from_go_mod(
530
+ self, scanner: StackScanner, go_project: Path
531
+ ) -> None:
532
+ result = scanner.scan(go_project)
533
+ identity = result.sections["project_identity"]
534
+ assert identity["name"] == "test-go-project"
535
+
536
+ def test_name_from_cargo_toml(
537
+ self, scanner: StackScanner, rust_project: Path
538
+ ) -> None:
539
+ result = scanner.scan(rust_project)
540
+ identity = result.sections["project_identity"]
541
+ assert identity["name"] == "test-rust-project"
542
+
543
+ def test_fallback_to_directory_name(
544
+ self, scanner: StackScanner, empty_project: Path
545
+ ) -> None:
546
+ result = scanner.scan(empty_project)
547
+ identity = result.sections["project_identity"]
548
+ assert identity["name"] == empty_project.name
549
+
550
+ def test_empty_project_type_unknown(
551
+ self, scanner: StackScanner, empty_project: Path
552
+ ) -> None:
553
+ result = scanner.scan(empty_project)
554
+ identity = result.sections["project_identity"]
555
+ assert identity["type"] == "unknown"
556
+
557
+ def test_monorepo_type(
558
+ self, scanner: StackScanner, monorepo_project: Path
559
+ ) -> None:
560
+ result = scanner.scan(monorepo_project)
561
+ identity = result.sections["project_identity"]
562
+ assert identity["type"] == "monorepo"
563
+
564
+ def test_description_from_package_json(
565
+ self, scanner: StackScanner, node_project: Path
566
+ ) -> None:
567
+ result = scanner.scan(node_project)
568
+ identity = result.sections["project_identity"]
569
+ assert identity["description"] == "A test Node.js project"
570
+
571
+
572
+ # ---------------------------------------------------------------------------
573
+ # ScanResult contract
574
+ # ---------------------------------------------------------------------------
575
+
576
+
577
+ class TestScanResultContract:
578
+ """Test that scan results follow the expected contract."""
579
+
580
+ def test_result_has_source_tags(
581
+ self, scanner: StackScanner, node_project: Path
582
+ ) -> None:
583
+ result = scanner.scan(node_project)
584
+ assert result.sections["project_identity"]["_source"] == "scanner:stack"
585
+ assert result.sections["stack"]["_source"] == "scanner:stack"
586
+
587
+ def test_result_has_duration(
588
+ self, scanner: StackScanner, node_project: Path
589
+ ) -> None:
590
+ result = scanner.scan(node_project)
591
+ assert result.duration_ms >= 0
592
+
593
+ def test_result_scanner_name(
594
+ self, scanner: StackScanner, node_project: Path
595
+ ) -> None:
596
+ result = scanner.scan(node_project)
597
+ assert result.scanner == "stack"
598
+
599
+ def test_empty_project_returns_both_sections(
600
+ self, scanner: StackScanner, empty_project: Path
601
+ ) -> None:
602
+ result = scanner.scan(empty_project)
603
+ assert "project_identity" in result.sections
604
+ assert "stack" in result.sections