@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,258 @@
1
+ """
2
+ Results reporter for gaia-ops replay testing.
3
+
4
+ Formats and presents ReplayResult data for human consumption and
5
+ programmatic analysis. Completely decoupled from execution.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from collections import Counter
12
+ from dataclasses import asdict
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from gaia_simulator.extractor import ReplayEvent
17
+ from gaia_simulator.runner import ReplayResult
18
+
19
+
20
+ class ReplayReporter:
21
+ """Formats replay results for human consumption and export."""
22
+
23
+ def events_payload(self, events: list[ReplayEvent]) -> list[dict[str, Any]]:
24
+ """Convert extracted events to JSON-serializable dicts."""
25
+ return [asdict(event) for event in events]
26
+
27
+ def results_payload(self, results: list[ReplayResult]) -> list[dict[str, Any]]:
28
+ """Convert replay results to JSON-serializable dicts."""
29
+ output: list[dict[str, Any]] = []
30
+ for r in results:
31
+ entry = {
32
+ "timestamp": r.event.timestamp,
33
+ "hook_name": r.event.hook_name,
34
+ "tool_name": r.event.tool_name,
35
+ "source_file": r.event.source_file,
36
+ "limitations": list(r.event.limitations),
37
+ "expected": {
38
+ "decision": r.event.expected_decision,
39
+ "exit_code": r.event.expected_exit_code,
40
+ "tier": r.event.expected_tier,
41
+ "metadata": r.event.expected_metadata,
42
+ },
43
+ "actual": {
44
+ "decision": r.actual_decision,
45
+ "exit_code": r.actual_exit_code,
46
+ "tier": r.actual_tier,
47
+ "metadata": r.actual_metadata,
48
+ },
49
+ "matched": r.matched,
50
+ "regression_type": r.regression_type,
51
+ }
52
+
53
+ tool_input = r.event.stdin_payload.get("tool_input", {})
54
+ if r.event.tool_name == "Bash":
55
+ entry["command"] = tool_input.get("command", "")
56
+ elif r.event.tool_name == "Agent":
57
+ entry["agent"] = tool_input.get("subagent_type", "")
58
+
59
+ output.append(entry)
60
+ return output
61
+
62
+ def summary(self, results: list[ReplayResult]) -> str:
63
+ """Quick summary: X events, Y matched, Z regressions.
64
+
65
+ Args:
66
+ results: List of ReplayResult instances.
67
+
68
+ Returns:
69
+ Multi-line summary string.
70
+ """
71
+ if not results:
72
+ return "No events replayed."
73
+
74
+ total = len(results)
75
+ matched = sum(1 for r in results if r.matched)
76
+ regressions = total - matched
77
+
78
+ lines = [
79
+ "=" * 60,
80
+ "REPLAY SUMMARY",
81
+ "=" * 60,
82
+ f"Total events: {total}",
83
+ f"Matched: {matched}",
84
+ f"Regressions: {regressions}",
85
+ ]
86
+
87
+ if regressions > 0:
88
+ lines.append("")
89
+ lines.append("Regression breakdown:")
90
+ reg_types = Counter(
91
+ r.regression_type for r in results if not r.matched
92
+ )
93
+ for rtype, count in reg_types.most_common():
94
+ lines.append(f" {rtype}: {count}")
95
+
96
+ # Quick stats by decision
97
+ lines.append("")
98
+ decision_counts = Counter(r.event.expected_decision for r in results)
99
+ lines.append("Events by expected decision:")
100
+ for dec, count in decision_counts.most_common():
101
+ lines.append(f" {dec}: {count}")
102
+
103
+ lines.append("=" * 60)
104
+ return "\n".join(lines)
105
+
106
+ def regressions_only(self, results: list[ReplayResult]) -> str:
107
+ """Show only regressions with details.
108
+
109
+ Args:
110
+ results: List of ReplayResult instances.
111
+
112
+ Returns:
113
+ Formatted string showing regression details, or a success message.
114
+ """
115
+ regressions = [r for r in results if not r.matched]
116
+
117
+ if not regressions:
118
+ return "No regressions detected. All events matched expected behavior."
119
+
120
+ lines = [
121
+ "=" * 60,
122
+ f"REGRESSIONS FOUND: {len(regressions)}",
123
+ "=" * 60,
124
+ ]
125
+
126
+ for i, r in enumerate(regressions, 1):
127
+ lines.append("")
128
+ lines.append(f"--- Regression #{i} [{r.regression_type}] ---")
129
+ lines.append(f" Timestamp: {r.event.timestamp}")
130
+ lines.append(f" Hook: {r.event.hook_name}")
131
+ lines.append(f" Tool: {r.event.tool_name}")
132
+ lines.append(f" Source: {r.event.source_file}")
133
+
134
+ # Show the command or agent name
135
+ tool_input = r.event.stdin_payload.get("tool_input", {})
136
+ if r.event.tool_name == "Bash":
137
+ cmd = tool_input.get("command", "")
138
+ if len(cmd) > 120:
139
+ cmd = cmd[:120] + "..."
140
+ lines.append(f" Command: {cmd}")
141
+ elif r.event.tool_name == "Agent":
142
+ agent = tool_input.get("subagent_type", tool_input.get("description", ""))
143
+ lines.append(f" Agent: {agent}")
144
+
145
+ lines.append(f" Expected: decision={r.event.expected_decision}, "
146
+ f"exit_code={r.event.expected_exit_code}, "
147
+ f"tier={r.event.expected_tier or 'n/a'}")
148
+ lines.append(f" Actual: decision={r.actual_decision}, "
149
+ f"exit_code={r.actual_exit_code}, "
150
+ f"tier={r.actual_tier or 'n/a'}")
151
+
152
+ if r.actual_stderr and len(r.actual_stderr) < 200:
153
+ lines.append(f" Stderr: {r.actual_stderr.strip()}")
154
+
155
+ lines.append("")
156
+ lines.append("=" * 60)
157
+ return "\n".join(lines)
158
+
159
+ def full_report(self, results: list[ReplayResult]) -> str:
160
+ """Full report with stats per hook, per tier, per decision type.
161
+
162
+ Args:
163
+ results: List of ReplayResult instances.
164
+
165
+ Returns:
166
+ Comprehensive formatted report string.
167
+ """
168
+ if not results:
169
+ return "No events to report on."
170
+
171
+ lines = [self.summary(results)]
172
+
173
+ # Stats by hook
174
+ hooks: dict[str, list[ReplayResult]] = {}
175
+ for r in results:
176
+ hooks.setdefault(r.event.hook_name, []).append(r)
177
+
178
+ lines.append("")
179
+ lines.append("BREAKDOWN BY HOOK:")
180
+ lines.append("-" * 40)
181
+ for hook_name, hook_results in sorted(hooks.items()):
182
+ total = len(hook_results)
183
+ matched = sum(1 for r in hook_results if r.matched)
184
+ lines.append(f" {hook_name}: {total} events, {matched} matched, "
185
+ f"{total - matched} regressions")
186
+
187
+ # Stats by tier
188
+ tiers: dict[str, list[ReplayResult]] = {}
189
+ for r in results:
190
+ tier = r.event.expected_tier or "n/a"
191
+ tiers.setdefault(tier, []).append(r)
192
+
193
+ lines.append("")
194
+ lines.append("BREAKDOWN BY TIER:")
195
+ lines.append("-" * 40)
196
+ for tier_name, tier_results in sorted(tiers.items()):
197
+ total = len(tier_results)
198
+ matched = sum(1 for r in tier_results if r.matched)
199
+ lines.append(f" {tier_name}: {total} events, {matched} matched, "
200
+ f"{total - matched} regressions")
201
+
202
+ # Stats by tool
203
+ tools: dict[str, list[ReplayResult]] = {}
204
+ for r in results:
205
+ tools.setdefault(r.event.tool_name, []).append(r)
206
+
207
+ lines.append("")
208
+ lines.append("BREAKDOWN BY TOOL:")
209
+ lines.append("-" * 40)
210
+ for tool_name, tool_results in sorted(tools.items()):
211
+ total = len(tool_results)
212
+ matched = sum(1 for r in tool_results if r.matched)
213
+ lines.append(f" {tool_name}: {total} events, {matched} matched, "
214
+ f"{total - matched} regressions")
215
+
216
+ # Stats by source file / artifact
217
+ sources: dict[str, list[ReplayResult]] = {}
218
+ for r in results:
219
+ sources.setdefault(r.event.source_file, []).append(r)
220
+
221
+ lines.append("")
222
+ lines.append("BREAKDOWN BY SOURCE:")
223
+ lines.append("-" * 40)
224
+ for source_name, source_results in sorted(sources.items()):
225
+ total = len(source_results)
226
+ matched = sum(1 for r in source_results if r.matched)
227
+ lines.append(f" {source_name}: {total} events, {matched} matched, "
228
+ f"{total - matched} regressions")
229
+
230
+ limitations = sorted({
231
+ limitation
232
+ for r in results
233
+ for limitation in r.event.limitations
234
+ if limitation
235
+ })
236
+ if limitations:
237
+ lines.append("")
238
+ lines.append("SOURCE LIMITATIONS:")
239
+ lines.append("-" * 40)
240
+ for limitation in limitations:
241
+ lines.append(f" - {limitation}")
242
+
243
+ # Show regressions if any
244
+ regressions = [r for r in results if not r.matched]
245
+ if regressions:
246
+ lines.append("")
247
+ lines.append(self.regressions_only(results))
248
+
249
+ return "\n".join(lines)
250
+
251
+ def save_json(self, results: list[ReplayResult], path: Path) -> None:
252
+ """Save results as JSON for programmatic analysis.
253
+
254
+ Args:
255
+ results: List of ReplayResult instances.
256
+ path: Output file path.
257
+ """
258
+ path.write_text(json.dumps(self.results_payload(results), indent=2, default=str))
@@ -0,0 +1,334 @@
1
+ """
2
+ Routing simulator for gaia-ops surface routing analysis.
3
+
4
+ Simulates what would happen when a prompt enters the orchestrator:
5
+ which surfaces activate, which agent is selected, what skills load,
6
+ what context sections are injected, and what contract permissions apply.
7
+
8
+ Uses the real classify_surfaces() from surface_router.py for fidelity.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ import sys
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Any, Optional
19
+
20
+ _TOOLS_DIR = Path(__file__).resolve().parent.parent
21
+ if str(_TOOLS_DIR) not in sys.path:
22
+ sys.path.insert(0, str(_TOOLS_DIR))
23
+
24
+ from context.surface_router import classify_surfaces, load_surface_routing_config
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Data classes
29
+ # ---------------------------------------------------------------------------
30
+
31
+
32
+ @dataclass
33
+ class RoutingResult:
34
+ """Result of simulating a prompt through the routing pipeline."""
35
+
36
+ prompt: str
37
+ surfaces_active: list[str]
38
+ primary_agent: str
39
+ adjacent_agents: list[str]
40
+ skills_loaded: list[str]
41
+ context_sections: list[str]
42
+ tokens_estimate: int
43
+ contracts: dict[str, list[str]] # {"read": [...], "write": [...]}
44
+ confidence: float
45
+ multi_surface: bool
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Frontmatter parsing
50
+ # ---------------------------------------------------------------------------
51
+
52
+ _RE_FRONTMATTER = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)
53
+ _RE_SKILLS_LINE = re.compile(r"^\s+-\s+(.+)$", re.MULTILINE)
54
+
55
+
56
+ def _parse_frontmatter(content: str) -> dict[str, Any]:
57
+ """Parse YAML frontmatter from agent .md files."""
58
+ if not content.startswith("---"):
59
+ return {}
60
+
61
+ match = _RE_FRONTMATTER.match(content)
62
+ if not match:
63
+ return {}
64
+
65
+ yaml_block = match.group(1)
66
+ result: dict[str, Any] = {}
67
+
68
+ for line in yaml_block.splitlines():
69
+ line_stripped = line.strip()
70
+ if not line_stripped or line_stripped.startswith("#"):
71
+ continue
72
+ if ":" not in line_stripped:
73
+ continue
74
+
75
+ key, _, value = line_stripped.partition(":")
76
+ key = key.strip()
77
+ value = value.strip()
78
+
79
+ if key == "skills":
80
+ skills: list[str] = []
81
+ if value.startswith("[") and value.endswith("]"):
82
+ skills = [s.strip() for s in value[1:-1].split(",") if s.strip()]
83
+ else:
84
+ skills_start = yaml_block.index("skills:")
85
+ skills_section = yaml_block[skills_start:]
86
+ for skill_match in _RE_SKILLS_LINE.finditer(skills_section):
87
+ skill_name = skill_match.group(1).strip()
88
+ if skill_name:
89
+ skills.append(skill_name)
90
+ result["skills"] = skills
91
+ else:
92
+ result[key] = value
93
+
94
+ return result
95
+
96
+
97
+ def _load_agent_skills(agents_dir: Path) -> dict[str, list[str]]:
98
+ """Load skills lists from all agent .md files in a directory.
99
+
100
+ Returns:
101
+ Dict mapping agent name to list of skill names.
102
+ """
103
+ agent_skills: dict[str, list[str]] = {}
104
+
105
+ if not agents_dir.is_dir():
106
+ return agent_skills
107
+
108
+ for md_file in sorted(agents_dir.glob("*.md")):
109
+ content = md_file.read_text(encoding="utf-8", errors="replace")
110
+ frontmatter = _parse_frontmatter(content)
111
+ agent_name = frontmatter.get("name", md_file.stem)
112
+ agent_skills[agent_name] = frontmatter.get("skills", [])
113
+
114
+ return agent_skills
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # RoutingSimulator
119
+ # ---------------------------------------------------------------------------
120
+
121
+
122
+ class RoutingSimulator:
123
+ """Simulates the gaia-ops routing pipeline for a given prompt.
124
+
125
+ Loads surface-routing.json, context-contracts.json, and agent frontmatter
126
+ to predict: which surfaces activate, which agent handles, what skills and
127
+ context sections are injected, and what contract permissions apply.
128
+ """
129
+
130
+ def __init__(self, config_dir: Path, agents_dir: Path):
131
+ """Initialize the simulator with config and agents directories.
132
+
133
+ Args:
134
+ config_dir: Path to the config/ directory containing
135
+ surface-routing.json and context-contracts.json.
136
+ agents_dir: Path to the agents/ directory containing agent .md files.
137
+ """
138
+ self._config_dir = config_dir
139
+ self._agents_dir = agents_dir
140
+
141
+ # Load routing config
142
+ routing_file = config_dir / "surface-routing.json"
143
+ self._routing_config = load_surface_routing_config(routing_file)
144
+
145
+ # Load contracts
146
+ contracts_file = config_dir / "context-contracts.json"
147
+ if contracts_file.is_file():
148
+ self._contracts = json.loads(contracts_file.read_text(encoding="utf-8"))
149
+ else:
150
+ self._contracts = {"agents": {}}
151
+
152
+ # Load agent skills from frontmatter
153
+ self._agent_skills = _load_agent_skills(agents_dir)
154
+
155
+ def simulate(self, prompt: str, agent_type: Optional[str] = None) -> RoutingResult:
156
+ """Simulate routing for a prompt.
157
+
158
+ Args:
159
+ prompt: The user prompt to classify.
160
+ agent_type: If provided, show what this specific agent would receive.
161
+ If not, determine the agent from surface routing.
162
+
163
+ Returns:
164
+ RoutingResult with full routing prediction.
165
+ """
166
+ routing = classify_surfaces(
167
+ prompt,
168
+ current_agent=agent_type or "",
169
+ routing_config=self._routing_config,
170
+ )
171
+
172
+ active_surfaces = routing.get("active_surfaces", [])
173
+ confidence = routing.get("confidence", 0.0)
174
+ multi_surface = routing.get("multi_surface", False)
175
+ recommended_agents = routing.get("recommended_agents", [])
176
+
177
+ surfaces_cfg = self._routing_config.get("surfaces", {})
178
+ if agent_type:
179
+ primary_agent = agent_type
180
+ elif recommended_agents:
181
+ primary_agent = recommended_agents[0]
182
+ else:
183
+ primary_agent = self._routing_config.get(
184
+ "reconnaissance_agent", "devops-developer"
185
+ )
186
+
187
+ skills = self._agent_skills.get(primary_agent, [])
188
+
189
+ context_sections: list[str] = []
190
+ seen_sections: set[str] = set()
191
+ for surface in active_surfaces:
192
+ surface_cfg = surfaces_cfg.get(surface, {})
193
+ for section in surface_cfg.get("contract_sections", []):
194
+ if section not in seen_sections:
195
+ context_sections.append(section)
196
+ seen_sections.add(section)
197
+
198
+ agent_contract = self._contracts.get("agents", {}).get(primary_agent, {})
199
+ read_sections = agent_contract.get("read", [])
200
+ write_sections = agent_contract.get("write", [])
201
+
202
+ tokens_estimate = len(context_sections) * 100
203
+
204
+ adjacent_agents = [a for a in recommended_agents if a != primary_agent]
205
+
206
+ return RoutingResult(
207
+ prompt=prompt,
208
+ surfaces_active=active_surfaces,
209
+ primary_agent=primary_agent,
210
+ adjacent_agents=adjacent_agents,
211
+ skills_loaded=skills,
212
+ context_sections=context_sections,
213
+ tokens_estimate=tokens_estimate,
214
+ contracts={"read": read_sections, "write": write_sections},
215
+ confidence=confidence,
216
+ multi_surface=multi_surface,
217
+ )
218
+
219
+ def simulate_from_log(self, events: list[dict[str, Any]]) -> list[RoutingResult]:
220
+ """Simulate routing for all events from logs.
221
+
222
+ Args:
223
+ events: List of event dicts, each with prompt or tool_input data.
224
+
225
+ Returns:
226
+ List of RoutingResult instances.
227
+ """
228
+ results: list[RoutingResult] = []
229
+ for event in events:
230
+ prompt = event.get("prompt", "")
231
+ if not prompt:
232
+ tool_input = event.get("tool_input", {})
233
+ if isinstance(tool_input, dict):
234
+ prompt = tool_input.get(
235
+ "command", tool_input.get("description", "")
236
+ )
237
+ if not prompt:
238
+ continue
239
+ agent_used = event.get("agent", event.get("subagent_type", None))
240
+ result = self.simulate(prompt, agent_type=agent_used)
241
+ results.append(result)
242
+ return results
243
+
244
+ def compare_routing(self, events: list[dict[str, Any]]) -> dict[str, Any]:
245
+ """Compare simulated routing vs actual agent used in logs.
246
+
247
+ Args:
248
+ events: List of event dicts, each with prompt and agent keys.
249
+
250
+ Returns:
251
+ Dict with matches, mismatches, and statistics.
252
+ """
253
+ matches: list[dict[str, Any]] = []
254
+ mismatches: list[dict[str, Any]] = []
255
+
256
+ for event in events:
257
+ prompt = event.get("prompt", "")
258
+ actual_agent = event.get("agent", event.get("subagent_type", ""))
259
+ if not prompt or not actual_agent:
260
+ continue
261
+
262
+ result = self.simulate(prompt)
263
+
264
+ entry = {
265
+ "prompt": prompt[:120],
266
+ "simulated_agent": result.primary_agent,
267
+ "actual_agent": actual_agent,
268
+ "surfaces": result.surfaces_active,
269
+ "confidence": result.confidence,
270
+ }
271
+
272
+ if result.primary_agent == actual_agent:
273
+ matches.append(entry)
274
+ else:
275
+ mismatches.append(entry)
276
+
277
+ total = len(matches) + len(mismatches)
278
+ return {
279
+ "total": total,
280
+ "matches": len(matches),
281
+ "mismatches": len(mismatches),
282
+ "match_rate": round(len(matches) / max(total, 1), 2),
283
+ "match_details": matches,
284
+ "mismatch_details": mismatches,
285
+ }
286
+
287
+
288
+ def format_routing_result(result: RoutingResult) -> str:
289
+ """Format a RoutingResult as human-readable text.
290
+
291
+ Args:
292
+ result: The routing simulation result.
293
+
294
+ Returns:
295
+ Formatted multi-line string.
296
+ """
297
+ lines = [
298
+ "=" * 60,
299
+ "ROUTING SIMULATION",
300
+ "=" * 60,
301
+ ]
302
+ lines.append("Prompt: " + result.prompt[:100])
303
+ lines.append("Primary agent: " + result.primary_agent)
304
+ adj = ", ".join(result.adjacent_agents) or "none"
305
+ lines.append("Adjacent agents: " + adj)
306
+ lines.append("Confidence: " + str(result.confidence))
307
+ lines.append("Multi-surface: " + str(result.multi_surface))
308
+ lines.append("")
309
+ lines.append("Active surfaces:")
310
+ for surface in result.surfaces_active:
311
+ lines.append(" - " + surface)
312
+ if not result.surfaces_active:
313
+ lines.append(" (none)")
314
+ lines.append("")
315
+ lines.append("Skills loaded:")
316
+ for skill in result.skills_loaded:
317
+ lines.append(" - " + skill)
318
+ if not result.skills_loaded:
319
+ lines.append(" (none)")
320
+ lines.append("")
321
+ lines.append("Context sections:")
322
+ for section in result.context_sections:
323
+ lines.append(" - " + section)
324
+ if not result.context_sections:
325
+ lines.append(" (none)")
326
+ lines.append("Tokens estimate: ~" + str(result.tokens_estimate))
327
+ lines.append("")
328
+ lines.append("Contracts:")
329
+ read_str = ", ".join(result.contracts.get("read", [])) or "none"
330
+ write_str = ", ".join(result.contracts.get("write", [])) or "none"
331
+ lines.append(" Read: " + read_str)
332
+ lines.append(" Write: " + write_str)
333
+ lines.append("=" * 60)
334
+ return chr(10).join(lines)