@jaguilar87/gaia 5.0.0-rc1

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 (609) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +26 -0
  3. package/ARCHITECTURE.md +335 -0
  4. package/CHANGELOG.md +1212 -0
  5. package/CODE_OF_CONDUCT.md +11 -0
  6. package/CONTRIBUTING.md +146 -0
  7. package/INSTALL.md +436 -0
  8. package/LICENSE +21 -0
  9. package/README.md +222 -0
  10. package/SECURITY.md +47 -0
  11. package/agents/README.md +78 -0
  12. package/agents/cloud-troubleshooter.md +73 -0
  13. package/agents/developer.md +65 -0
  14. package/agents/gaia-operator.md +64 -0
  15. package/agents/gaia-orchestrator.md +237 -0
  16. package/agents/gaia-planner.md +53 -0
  17. package/agents/gaia-system.md +70 -0
  18. package/agents/gitops-operator.md +61 -0
  19. package/agents/terraform-architect.md +63 -0
  20. package/bin/README.md +106 -0
  21. package/bin/cli/__init__.py +1 -0
  22. package/bin/cli/approvals.py +740 -0
  23. package/bin/cli/cleanup.py +562 -0
  24. package/bin/cli/context.py +283 -0
  25. package/bin/cli/doctor.py +628 -0
  26. package/bin/cli/history.py +305 -0
  27. package/bin/cli/memory.py +464 -0
  28. package/bin/cli/metrics.py +1068 -0
  29. package/bin/cli/plans.py +515 -0
  30. package/bin/cli/status.py +302 -0
  31. package/bin/cli/update.py +382 -0
  32. package/bin/gaia +112 -0
  33. package/bin/gaia-cleanup.js +531 -0
  34. package/bin/gaia-doctor.js +635 -0
  35. package/bin/gaia-evidence +126 -0
  36. package/bin/gaia-history.js +251 -0
  37. package/bin/gaia-metrics.js +1278 -0
  38. package/bin/gaia-review.js +269 -0
  39. package/bin/gaia-scan +44 -0
  40. package/bin/gaia-scan.py +589 -0
  41. package/bin/gaia-skills-diagnose.js +929 -0
  42. package/bin/gaia-status.js +278 -0
  43. package/bin/gaia-uninstall.js +111 -0
  44. package/bin/gaia-update.js +816 -0
  45. package/bin/pre-publish-validate.js +610 -0
  46. package/bin/python-detect.js +60 -0
  47. package/commands/README.md +64 -0
  48. package/commands/gaia.md +37 -0
  49. package/commands/scan-project.md +67 -0
  50. package/config/README.md +71 -0
  51. package/config/cloud/aws.json +134 -0
  52. package/config/cloud/gcp.json +139 -0
  53. package/config/context-contracts.json +158 -0
  54. package/config/crons-schema.md +81 -0
  55. package/config/git_standards.json +72 -0
  56. package/config/surface-routing.json +421 -0
  57. package/config/universal-rules.json +102 -0
  58. package/dist/gaia-ops/.claude-plugin/plugin.json +24 -0
  59. package/dist/gaia-ops/README.md +80 -0
  60. package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
  61. package/dist/gaia-ops/agents/developer.md +65 -0
  62. package/dist/gaia-ops/agents/gaia-operator.md +64 -0
  63. package/dist/gaia-ops/agents/gaia-orchestrator.md +237 -0
  64. package/dist/gaia-ops/agents/gaia-planner.md +53 -0
  65. package/dist/gaia-ops/agents/gaia-system.md +70 -0
  66. package/dist/gaia-ops/agents/gitops-operator.md +61 -0
  67. package/dist/gaia-ops/agents/terraform-architect.md +63 -0
  68. package/dist/gaia-ops/commands/gaia.md +37 -0
  69. package/dist/gaia-ops/config/README.md +71 -0
  70. package/dist/gaia-ops/config/cloud/aws.json +134 -0
  71. package/dist/gaia-ops/config/cloud/gcp.json +139 -0
  72. package/dist/gaia-ops/config/context-contracts.json +158 -0
  73. package/dist/gaia-ops/config/crons-schema.md +81 -0
  74. package/dist/gaia-ops/config/git_standards.json +72 -0
  75. package/dist/gaia-ops/config/surface-routing.json +421 -0
  76. package/dist/gaia-ops/config/universal-rules.json +102 -0
  77. package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
  78. package/dist/gaia-ops/hooks/adapters/base.py +219 -0
  79. package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
  80. package/dist/gaia-ops/hooks/adapters/claude_code.py +1890 -0
  81. package/dist/gaia-ops/hooks/adapters/types.py +194 -0
  82. package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
  83. package/dist/gaia-ops/hooks/hooks.json +163 -0
  84. package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
  85. package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
  86. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
  87. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
  88. package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +120 -0
  89. package/dist/gaia-ops/hooks/modules/agents/state_tracker.py +267 -0
  90. package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
  91. package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
  92. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
  93. package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
  94. package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
  95. package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
  96. package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
  97. package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +611 -0
  98. package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
  99. package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
  100. package/dist/gaia-ops/hooks/modules/context/agentic_loop_detector.py +165 -0
  101. package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
  102. package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +218 -0
  103. package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
  104. package/dist/gaia-ops/hooks/modules/context/context_injector.py +558 -0
  105. package/dist/gaia-ops/hooks/modules/context/context_writer.py +530 -0
  106. package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
  107. package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
  108. package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
  109. package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
  110. package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
  111. package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +577 -0
  112. package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
  113. package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
  114. package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
  115. package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
  116. package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
  117. package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +216 -0
  118. package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
  119. package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +122 -0
  120. package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
  121. package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
  122. package/dist/gaia-ops/hooks/modules/security/__init__.py +120 -0
  123. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
  124. package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
  125. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +1638 -0
  126. package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
  127. package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +222 -0
  128. package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +595 -0
  129. package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +87 -0
  130. package/dist/gaia-ops/hooks/modules/security/command_semantics.py +181 -0
  131. package/dist/gaia-ops/hooks/modules/security/composition_rules.py +547 -0
  132. package/dist/gaia-ops/hooks/modules/security/flag_classifiers.py +873 -0
  133. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
  134. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +1131 -0
  135. package/dist/gaia-ops/hooks/modules/security/network_hosts.py +481 -0
  136. package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
  137. package/dist/gaia-ops/hooks/modules/security/shell_unwrapper.py +165 -0
  138. package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
  139. package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
  140. package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +174 -0
  141. package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
  142. package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +160 -0
  143. package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
  144. package/dist/gaia-ops/hooks/modules/session/session_registry.py +232 -0
  145. package/dist/gaia-ops/hooks/modules/tools/__init__.py +29 -0
  146. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +1008 -0
  147. package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  148. package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
  149. package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
  150. package/dist/gaia-ops/hooks/modules/tools/stage_decomposer.py +315 -0
  151. package/dist/gaia-ops/hooks/modules/tools/task_validator.py +294 -0
  152. package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
  153. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
  154. package/dist/gaia-ops/hooks/post_compact.py +43 -0
  155. package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
  156. package/dist/gaia-ops/hooks/pre_compact.py +60 -0
  157. package/dist/gaia-ops/hooks/pre_tool_use.py +413 -0
  158. package/dist/gaia-ops/hooks/session_start.py +81 -0
  159. package/dist/gaia-ops/hooks/stop_hook.py +82 -0
  160. package/dist/gaia-ops/hooks/subagent_start.py +71 -0
  161. package/dist/gaia-ops/hooks/subagent_stop.py +295 -0
  162. package/dist/gaia-ops/hooks/task_completed.py +70 -0
  163. package/dist/gaia-ops/hooks/user_prompt_submit.py +246 -0
  164. package/dist/gaia-ops/settings.json +72 -0
  165. package/dist/gaia-ops/skills/README.md +154 -0
  166. package/dist/gaia-ops/skills/agent-protocol/SKILL.md +93 -0
  167. package/dist/gaia-ops/skills/agent-protocol/examples.md +223 -0
  168. package/dist/gaia-ops/skills/agent-response/SKILL.md +69 -0
  169. package/dist/gaia-ops/skills/agentic-loop/SKILL.md +80 -0
  170. package/dist/gaia-ops/skills/agentic-loop/reference.md +378 -0
  171. package/dist/gaia-ops/skills/blog-writing/SKILL.md +98 -0
  172. package/dist/gaia-ops/skills/blog-writing/reference.md +130 -0
  173. package/dist/gaia-ops/skills/brief-spec/SKILL.md +182 -0
  174. package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
  175. package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
  176. package/dist/gaia-ops/skills/context-updater/SKILL.md +87 -0
  177. package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
  178. package/dist/gaia-ops/skills/developer-patterns/SKILL.md +50 -0
  179. package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
  180. package/dist/gaia-ops/skills/execution/SKILL.md +99 -0
  181. package/dist/gaia-ops/skills/fast-queries/SKILL.md +43 -0
  182. package/dist/gaia-ops/skills/gaia-compact/SKILL.md +74 -0
  183. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +108 -0
  184. package/dist/gaia-ops/skills/gaia-patterns/reference.md +395 -0
  185. package/dist/gaia-ops/skills/gaia-planner/SKILL.md +37 -0
  186. package/dist/gaia-ops/skills/gaia-planner/reference.md +107 -0
  187. package/dist/gaia-ops/skills/gaia-release/SKILL.md +82 -0
  188. package/dist/gaia-ops/skills/gaia-release/reference.md +102 -0
  189. package/dist/gaia-ops/skills/gaia-self-check/SKILL.md +114 -0
  190. package/dist/gaia-ops/skills/gaia-self-check/reference.md +453 -0
  191. package/dist/gaia-ops/skills/gaia-verify/SKILL.md +77 -0
  192. package/dist/gaia-ops/skills/gaia-verify/reference.md +80 -0
  193. package/dist/gaia-ops/skills/git-conventions/SKILL.md +47 -0
  194. package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +60 -0
  195. package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
  196. package/dist/gaia-ops/skills/gmail-policy/SKILL.md +200 -0
  197. package/dist/gaia-ops/skills/gmail-policy/reference.md +150 -0
  198. package/dist/gaia-ops/skills/gmail-triage/SKILL.md +100 -0
  199. package/dist/gaia-ops/skills/gws-setup/SKILL.md +99 -0
  200. package/dist/gaia-ops/skills/gws-setup/reference.md +73 -0
  201. package/dist/gaia-ops/skills/investigation/SKILL.md +100 -0
  202. package/dist/gaia-ops/skills/memory-curation/SKILL.md +83 -0
  203. package/dist/gaia-ops/skills/memory-search/SKILL.md +88 -0
  204. package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +160 -0
  205. package/dist/gaia-ops/skills/orchestrator-approval/reference.md +174 -0
  206. package/dist/gaia-ops/skills/pending-approvals/SKILL.md +72 -0
  207. package/dist/gaia-ops/skills/pending-approvals/reference.md +214 -0
  208. package/dist/gaia-ops/skills/readme-writing/SKILL.md +71 -0
  209. package/dist/gaia-ops/skills/readme-writing/reference.md +188 -0
  210. package/dist/gaia-ops/skills/reference.md +135 -0
  211. package/dist/gaia-ops/skills/request-approval/SKILL.md +140 -0
  212. package/dist/gaia-ops/skills/request-approval/examples.md +140 -0
  213. package/dist/gaia-ops/skills/request-approval/reference.md +57 -0
  214. package/dist/gaia-ops/skills/schedule-task/SKILL.md +64 -0
  215. package/dist/gaia-ops/skills/schedule-task/reference.md +233 -0
  216. package/dist/gaia-ops/skills/security-tiers/SKILL.md +141 -0
  217. package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
  218. package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
  219. package/dist/gaia-ops/skills/skill-creation/SKILL.md +92 -0
  220. package/dist/gaia-ops/skills/skill-creation/reference.md +29 -0
  221. package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +89 -0
  222. package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
  223. package/dist/gaia-ops/tools/__init__.py +9 -0
  224. package/dist/gaia-ops/tools/agentic-loop/decide-status.py +210 -0
  225. package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +106 -0
  226. package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +221 -0
  227. package/dist/gaia-ops/tools/context/README.md +132 -0
  228. package/dist/gaia-ops/tools/context/__init__.py +42 -0
  229. package/dist/gaia-ops/tools/context/_paths.py +20 -0
  230. package/dist/gaia-ops/tools/context/context_provider.py +721 -0
  231. package/dist/gaia-ops/tools/context/context_section_reader.py +342 -0
  232. package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
  233. package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
  234. package/dist/gaia-ops/tools/context/surface_router.py +278 -0
  235. package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
  236. package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
  237. package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  238. package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  239. package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  240. package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  241. package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
  242. package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  243. package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
  244. package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
  245. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
  246. package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
  247. package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
  248. package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
  249. package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +264 -0
  250. package/dist/gaia-ops/tools/memory/README.md +0 -0
  251. package/dist/gaia-ops/tools/memory/__init__.py +20 -0
  252. package/dist/gaia-ops/tools/memory/backfill_fts5.py +107 -0
  253. package/dist/gaia-ops/tools/memory/conflict_detector.py +295 -0
  254. package/dist/gaia-ops/tools/memory/episodic.py +1210 -0
  255. package/dist/gaia-ops/tools/memory/git_invalidator.py +262 -0
  256. package/dist/gaia-ops/tools/memory/paths.py +102 -0
  257. package/dist/gaia-ops/tools/memory/scoring.py +193 -0
  258. package/dist/gaia-ops/tools/memory/search_store.py +360 -0
  259. package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
  260. package/dist/gaia-ops/tools/review/__init__.py +1 -0
  261. package/dist/gaia-ops/tools/review/review_engine.py +157 -0
  262. package/dist/gaia-ops/tools/scan/__init__.py +35 -0
  263. package/dist/gaia-ops/tools/scan/config.py +247 -0
  264. package/dist/gaia-ops/tools/scan/merge.py +212 -0
  265. package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
  266. package/dist/gaia-ops/tools/scan/registry.py +127 -0
  267. package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
  268. package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
  269. package/dist/gaia-ops/tools/scan/scanners/environment.py +349 -0
  270. package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
  271. package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
  272. package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
  273. package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
  274. package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
  275. package/dist/gaia-ops/tools/scan/setup.py +686 -0
  276. package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
  277. package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
  278. package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
  279. package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
  280. package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
  281. package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
  282. package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
  283. package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
  284. package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
  285. package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
  286. package/dist/gaia-ops/tools/scan/ui.py +624 -0
  287. package/dist/gaia-ops/tools/scan/verify.py +270 -0
  288. package/dist/gaia-ops/tools/scan/walk.py +118 -0
  289. package/dist/gaia-ops/tools/scan/workspace.py +85 -0
  290. package/dist/gaia-ops/tools/validation/README.md +244 -0
  291. package/dist/gaia-ops/tools/validation/__init__.py +17 -0
  292. package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
  293. package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
  294. package/dist/gaia-security/.claude-plugin/plugin.json +24 -0
  295. package/dist/gaia-security/README.md +90 -0
  296. package/dist/gaia-security/config/universal-rules.json +102 -0
  297. package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
  298. package/dist/gaia-security/hooks/adapters/base.py +219 -0
  299. package/dist/gaia-security/hooks/adapters/channel.py +17 -0
  300. package/dist/gaia-security/hooks/adapters/claude_code.py +1890 -0
  301. package/dist/gaia-security/hooks/adapters/types.py +194 -0
  302. package/dist/gaia-security/hooks/adapters/utils.py +25 -0
  303. package/dist/gaia-security/hooks/hooks.json +84 -0
  304. package/dist/gaia-security/hooks/modules/__init__.py +15 -0
  305. package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
  306. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
  307. package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
  308. package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +120 -0
  309. package/dist/gaia-security/hooks/modules/agents/state_tracker.py +267 -0
  310. package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
  311. package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
  312. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
  313. package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
  314. package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
  315. package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
  316. package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
  317. package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +611 -0
  318. package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
  319. package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
  320. package/dist/gaia-security/hooks/modules/context/agentic_loop_detector.py +165 -0
  321. package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
  322. package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +218 -0
  323. package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
  324. package/dist/gaia-security/hooks/modules/context/context_injector.py +558 -0
  325. package/dist/gaia-security/hooks/modules/context/context_writer.py +530 -0
  326. package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
  327. package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
  328. package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
  329. package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
  330. package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
  331. package/dist/gaia-security/hooks/modules/core/plugin_setup.py +577 -0
  332. package/dist/gaia-security/hooks/modules/core/state.py +179 -0
  333. package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
  334. package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
  335. package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
  336. package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
  337. package/dist/gaia-security/hooks/modules/memory/episode_writer.py +216 -0
  338. package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
  339. package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +122 -0
  340. package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
  341. package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
  342. package/dist/gaia-security/hooks/modules/security/__init__.py +120 -0
  343. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
  344. package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
  345. package/dist/gaia-security/hooks/modules/security/approval_grants.py +1638 -0
  346. package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
  347. package/dist/gaia-security/hooks/modules/security/approval_scopes.py +222 -0
  348. package/dist/gaia-security/hooks/modules/security/blocked_commands.py +595 -0
  349. package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +87 -0
  350. package/dist/gaia-security/hooks/modules/security/command_semantics.py +181 -0
  351. package/dist/gaia-security/hooks/modules/security/composition_rules.py +547 -0
  352. package/dist/gaia-security/hooks/modules/security/flag_classifiers.py +873 -0
  353. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
  354. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +1131 -0
  355. package/dist/gaia-security/hooks/modules/security/network_hosts.py +481 -0
  356. package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
  357. package/dist/gaia-security/hooks/modules/security/shell_unwrapper.py +165 -0
  358. package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
  359. package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
  360. package/dist/gaia-security/hooks/modules/session/pending_scanner.py +174 -0
  361. package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
  362. package/dist/gaia-security/hooks/modules/session/session_event_injector.py +160 -0
  363. package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
  364. package/dist/gaia-security/hooks/modules/session/session_registry.py +232 -0
  365. package/dist/gaia-security/hooks/modules/tools/__init__.py +29 -0
  366. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +1008 -0
  367. package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  368. package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
  369. package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
  370. package/dist/gaia-security/hooks/modules/tools/stage_decomposer.py +315 -0
  371. package/dist/gaia-security/hooks/modules/tools/task_validator.py +294 -0
  372. package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
  373. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
  374. package/dist/gaia-security/hooks/post_tool_use.py +54 -0
  375. package/dist/gaia-security/hooks/pre_tool_use.py +413 -0
  376. package/dist/gaia-security/hooks/session_start.py +81 -0
  377. package/dist/gaia-security/hooks/stop_hook.py +82 -0
  378. package/dist/gaia-security/hooks/user_prompt_submit.py +246 -0
  379. package/dist/gaia-security/settings.json +58 -0
  380. package/git-hooks/commit-msg +41 -0
  381. package/hooks/README.md +100 -0
  382. package/hooks/adapters/__init__.py +52 -0
  383. package/hooks/adapters/base.py +219 -0
  384. package/hooks/adapters/channel.py +17 -0
  385. package/hooks/adapters/claude_code.py +1890 -0
  386. package/hooks/adapters/types.py +194 -0
  387. package/hooks/adapters/utils.py +25 -0
  388. package/hooks/elicitation_result.py +179 -0
  389. package/hooks/hooks.json +84 -0
  390. package/hooks/modules/README.md +189 -0
  391. package/hooks/modules/__init__.py +15 -0
  392. package/hooks/modules/agents/__init__.py +29 -0
  393. package/hooks/modules/agents/contract_validator.py +647 -0
  394. package/hooks/modules/agents/response_contract.py +496 -0
  395. package/hooks/modules/agents/skill_injection_verifier.py +120 -0
  396. package/hooks/modules/agents/state_tracker.py +267 -0
  397. package/hooks/modules/agents/task_info_builder.py +74 -0
  398. package/hooks/modules/agents/transcript_analyzer.py +458 -0
  399. package/hooks/modules/agents/transcript_reader.py +152 -0
  400. package/hooks/modules/audit/__init__.py +28 -0
  401. package/hooks/modules/audit/event_detector.py +168 -0
  402. package/hooks/modules/audit/logger.py +131 -0
  403. package/hooks/modules/audit/metrics.py +134 -0
  404. package/hooks/modules/audit/workflow_auditor.py +611 -0
  405. package/hooks/modules/audit/workflow_recorder.py +296 -0
  406. package/hooks/modules/context/__init__.py +11 -0
  407. package/hooks/modules/context/agentic_loop_detector.py +165 -0
  408. package/hooks/modules/context/anchor_tracker.py +317 -0
  409. package/hooks/modules/context/compact_context_builder.py +218 -0
  410. package/hooks/modules/context/context_freshness.py +145 -0
  411. package/hooks/modules/context/context_injector.py +558 -0
  412. package/hooks/modules/context/context_writer.py +530 -0
  413. package/hooks/modules/context/contracts_loader.py +161 -0
  414. package/hooks/modules/core/__init__.py +40 -0
  415. package/hooks/modules/core/hook_entry.py +78 -0
  416. package/hooks/modules/core/paths.py +160 -0
  417. package/hooks/modules/core/plugin_mode.py +149 -0
  418. package/hooks/modules/core/plugin_setup.py +577 -0
  419. package/hooks/modules/core/state.py +179 -0
  420. package/hooks/modules/core/stdin.py +24 -0
  421. package/hooks/modules/events/__init__.py +1 -0
  422. package/hooks/modules/events/event_writer.py +210 -0
  423. package/hooks/modules/evidence/__init__.py +34 -0
  424. package/hooks/modules/evidence/assertions.py +137 -0
  425. package/hooks/modules/evidence/index_writer.py +57 -0
  426. package/hooks/modules/evidence/loader.py +126 -0
  427. package/hooks/modules/evidence/runner.py +241 -0
  428. package/hooks/modules/memory/__init__.py +8 -0
  429. package/hooks/modules/memory/episode_writer.py +216 -0
  430. package/hooks/modules/orchestrator/__init__.py +1 -0
  431. package/hooks/modules/orchestrator/delegate_mode.py +122 -0
  432. package/hooks/modules/scanning/__init__.py +8 -0
  433. package/hooks/modules/scanning/scan_trigger.py +84 -0
  434. package/hooks/modules/security/__init__.py +120 -0
  435. package/hooks/modules/security/approval_cleanup.py +87 -0
  436. package/hooks/modules/security/approval_constants.py +23 -0
  437. package/hooks/modules/security/approval_grants.py +1638 -0
  438. package/hooks/modules/security/approval_messages.py +71 -0
  439. package/hooks/modules/security/approval_scopes.py +222 -0
  440. package/hooks/modules/security/blocked_commands.py +595 -0
  441. package/hooks/modules/security/blocked_message_formatter.py +87 -0
  442. package/hooks/modules/security/command_semantics.py +181 -0
  443. package/hooks/modules/security/composition_rules.py +547 -0
  444. package/hooks/modules/security/flag_classifiers.py +873 -0
  445. package/hooks/modules/security/gitops_validator.py +179 -0
  446. package/hooks/modules/security/mutative_verbs.py +1131 -0
  447. package/hooks/modules/security/network_hosts.py +481 -0
  448. package/hooks/modules/security/prompt_validator.py +40 -0
  449. package/hooks/modules/security/shell_unwrapper.py +165 -0
  450. package/hooks/modules/security/tiers.py +196 -0
  451. package/hooks/modules/session/__init__.py +10 -0
  452. package/hooks/modules/session/pending_scanner.py +174 -0
  453. package/hooks/modules/session/session_context_writer.py +100 -0
  454. package/hooks/modules/session/session_event_injector.py +160 -0
  455. package/hooks/modules/session/session_manager.py +31 -0
  456. package/hooks/modules/session/session_registry.py +232 -0
  457. package/hooks/modules/tools/__init__.py +29 -0
  458. package/hooks/modules/tools/bash_validator.py +1008 -0
  459. package/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  460. package/hooks/modules/tools/hook_response.py +55 -0
  461. package/hooks/modules/tools/shell_parser.py +227 -0
  462. package/hooks/modules/tools/stage_decomposer.py +315 -0
  463. package/hooks/modules/tools/task_validator.py +294 -0
  464. package/hooks/modules/validation/__init__.py +23 -0
  465. package/hooks/modules/validation/commit_validator.py +380 -0
  466. package/hooks/post_compact.py +43 -0
  467. package/hooks/post_tool_use.py +54 -0
  468. package/hooks/pre_compact.py +60 -0
  469. package/hooks/pre_tool_use.py +413 -0
  470. package/hooks/session_start.py +81 -0
  471. package/hooks/stop_hook.py +82 -0
  472. package/hooks/subagent_start.py +71 -0
  473. package/hooks/subagent_stop.py +295 -0
  474. package/hooks/task_completed.py +70 -0
  475. package/hooks/user_prompt_submit.py +246 -0
  476. package/index.js +83 -0
  477. package/package.json +99 -0
  478. package/pyproject.toml +32 -0
  479. package/skills/README.md +154 -0
  480. package/skills/agent-protocol/SKILL.md +93 -0
  481. package/skills/agent-protocol/examples.md +223 -0
  482. package/skills/agent-response/SKILL.md +69 -0
  483. package/skills/agentic-loop/SKILL.md +80 -0
  484. package/skills/agentic-loop/reference.md +378 -0
  485. package/skills/blog-writing/SKILL.md +98 -0
  486. package/skills/blog-writing/reference.md +130 -0
  487. package/skills/brief-spec/SKILL.md +182 -0
  488. package/skills/command-execution/SKILL.md +64 -0
  489. package/skills/command-execution/reference.md +83 -0
  490. package/skills/context-updater/SKILL.md +87 -0
  491. package/skills/context-updater/examples.md +71 -0
  492. package/skills/developer-patterns/SKILL.md +50 -0
  493. package/skills/developer-patterns/reference.md +112 -0
  494. package/skills/execution/SKILL.md +99 -0
  495. package/skills/fast-queries/SKILL.md +43 -0
  496. package/skills/gaia-compact/SKILL.md +74 -0
  497. package/skills/gaia-patterns/SKILL.md +108 -0
  498. package/skills/gaia-patterns/reference.md +395 -0
  499. package/skills/gaia-planner/SKILL.md +37 -0
  500. package/skills/gaia-planner/reference.md +107 -0
  501. package/skills/gaia-release/SKILL.md +82 -0
  502. package/skills/gaia-release/reference.md +102 -0
  503. package/skills/gaia-self-check/SKILL.md +114 -0
  504. package/skills/gaia-self-check/reference.md +453 -0
  505. package/skills/gaia-verify/SKILL.md +77 -0
  506. package/skills/gaia-verify/reference.md +80 -0
  507. package/skills/git-conventions/SKILL.md +47 -0
  508. package/skills/gitops-patterns/SKILL.md +60 -0
  509. package/skills/gitops-patterns/reference.md +183 -0
  510. package/skills/gmail-policy/SKILL.md +200 -0
  511. package/skills/gmail-policy/reference.md +150 -0
  512. package/skills/gmail-triage/SKILL.md +100 -0
  513. package/skills/gws-setup/SKILL.md +99 -0
  514. package/skills/gws-setup/reference.md +73 -0
  515. package/skills/investigation/SKILL.md +100 -0
  516. package/skills/memory-curation/SKILL.md +83 -0
  517. package/skills/memory-search/SKILL.md +88 -0
  518. package/skills/orchestrator-approval/SKILL.md +160 -0
  519. package/skills/orchestrator-approval/reference.md +174 -0
  520. package/skills/pending-approvals/SKILL.md +72 -0
  521. package/skills/pending-approvals/reference.md +214 -0
  522. package/skills/readme-writing/SKILL.md +71 -0
  523. package/skills/readme-writing/reference.md +188 -0
  524. package/skills/reference.md +135 -0
  525. package/skills/request-approval/SKILL.md +140 -0
  526. package/skills/request-approval/examples.md +140 -0
  527. package/skills/request-approval/reference.md +57 -0
  528. package/skills/schedule-task/SKILL.md +64 -0
  529. package/skills/schedule-task/reference.md +233 -0
  530. package/skills/security-tiers/SKILL.md +141 -0
  531. package/skills/security-tiers/destructive-commands-reference.md +623 -0
  532. package/skills/security-tiers/reference.md +39 -0
  533. package/skills/skill-creation/SKILL.md +92 -0
  534. package/skills/skill-creation/reference.md +29 -0
  535. package/skills/terraform-patterns/SKILL.md +89 -0
  536. package/skills/terraform-patterns/reference.md +93 -0
  537. package/templates/README.md +69 -0
  538. package/templates/managed-settings.template.json +43 -0
  539. package/tools/__init__.py +9 -0
  540. package/tools/agentic-loop/decide-status.py +210 -0
  541. package/tools/agentic-loop/parse-metric.py +106 -0
  542. package/tools/agentic-loop/record-iteration.py +221 -0
  543. package/tools/context/README.md +132 -0
  544. package/tools/context/__init__.py +42 -0
  545. package/tools/context/_paths.py +20 -0
  546. package/tools/context/context_provider.py +721 -0
  547. package/tools/context/context_section_reader.py +342 -0
  548. package/tools/context/deep_merge.py +159 -0
  549. package/tools/context/pending_updates.py +760 -0
  550. package/tools/context/surface_router.py +278 -0
  551. package/tools/fast-queries/README.md +65 -0
  552. package/tools/fast-queries/__init__.py +30 -0
  553. package/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  554. package/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  555. package/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  556. package/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  557. package/tools/fast-queries/run_triage.sh +59 -0
  558. package/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  559. package/tools/gaia_simulator/__init__.py +33 -0
  560. package/tools/gaia_simulator/cli.py +354 -0
  561. package/tools/gaia_simulator/extractor.py +457 -0
  562. package/tools/gaia_simulator/reporter.py +258 -0
  563. package/tools/gaia_simulator/routing_simulator.py +334 -0
  564. package/tools/gaia_simulator/runner.py +539 -0
  565. package/tools/gaia_simulator/skills_mapper.py +264 -0
  566. package/tools/memory/README.md +0 -0
  567. package/tools/memory/__init__.py +20 -0
  568. package/tools/memory/backfill_fts5.py +107 -0
  569. package/tools/memory/conflict_detector.py +295 -0
  570. package/tools/memory/episodic.py +1210 -0
  571. package/tools/memory/git_invalidator.py +262 -0
  572. package/tools/memory/paths.py +102 -0
  573. package/tools/memory/scoring.py +193 -0
  574. package/tools/memory/search_store.py +360 -0
  575. package/tools/persist_transcript_analysis.py +85 -0
  576. package/tools/review/__init__.py +1 -0
  577. package/tools/review/review_engine.py +157 -0
  578. package/tools/scan/__init__.py +35 -0
  579. package/tools/scan/config.py +247 -0
  580. package/tools/scan/merge.py +212 -0
  581. package/tools/scan/orchestrator.py +549 -0
  582. package/tools/scan/registry.py +127 -0
  583. package/tools/scan/scanners/__init__.py +18 -0
  584. package/tools/scan/scanners/base.py +137 -0
  585. package/tools/scan/scanners/environment.py +349 -0
  586. package/tools/scan/scanners/git.py +570 -0
  587. package/tools/scan/scanners/infrastructure.py +875 -0
  588. package/tools/scan/scanners/orchestration.py +600 -0
  589. package/tools/scan/scanners/stack.py +1085 -0
  590. package/tools/scan/scanners/tools.py +260 -0
  591. package/tools/scan/setup.py +686 -0
  592. package/tools/scan/tests/__init__.py +1 -0
  593. package/tools/scan/tests/conftest.py +796 -0
  594. package/tools/scan/tests/test_environment.py +323 -0
  595. package/tools/scan/tests/test_git.py +419 -0
  596. package/tools/scan/tests/test_infrastructure.py +382 -0
  597. package/tools/scan/tests/test_integration.py +920 -0
  598. package/tools/scan/tests/test_merge.py +269 -0
  599. package/tools/scan/tests/test_orchestration.py +304 -0
  600. package/tools/scan/tests/test_stack.py +604 -0
  601. package/tools/scan/tests/test_tools.py +349 -0
  602. package/tools/scan/ui.py +624 -0
  603. package/tools/scan/verify.py +270 -0
  604. package/tools/scan/walk.py +118 -0
  605. package/tools/scan/workspace.py +85 -0
  606. package/tools/validation/README.md +244 -0
  607. package/tools/validation/__init__.py +17 -0
  608. package/tools/validation/approval_gate.py +321 -0
  609. package/tools/validation/validate_skills.py +189 -0
@@ -0,0 +1,721 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Context Provider for Claude Agent System
4
+
5
+ Generates structured context payloads for agents based on:
6
+ 1. Agent contracts (context-contracts.json + cloud overlays)
7
+ 2. Universal rules (universal-rules.json)
8
+ 3. Historical episodes (episodic memory)
9
+
10
+ Usage:
11
+ python3 context_provider.py <agent_name> [user_task] [--context-file PATH]
12
+ """
13
+
14
+ import json
15
+ import argparse
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Dict, List, Any, Optional
19
+
20
+ # Ensure the package root (gaia-ops-dev/) is on sys.path so that
21
+ # `tools.memory.scoring` and `tools.memory.search_store` resolve when
22
+ # this file runs as a subprocess with cwd=workspace-root.
23
+ # Pattern: same as hooks/pre_tool_use.py line 22.
24
+ _PACKAGE_ROOT = str(Path(__file__).resolve().parent.parent.parent)
25
+ if _PACKAGE_ROOT not in sys.path:
26
+ sys.path.insert(0, _PACKAGE_ROOT)
27
+
28
+ try:
29
+ from ._paths import resolve_config_dir
30
+ from .surface_router import (
31
+ build_investigation_brief,
32
+ classify_surfaces,
33
+ load_surface_routing_config,
34
+ )
35
+ except ImportError:
36
+ from _paths import resolve_config_dir
37
+ from surface_router import (
38
+ build_investigation_brief,
39
+ classify_surfaces,
40
+ load_surface_routing_config,
41
+ )
42
+
43
+ # Default paths
44
+ DEFAULT_CONTEXT_PATH = Path(".claude/project-context/project-context.json")
45
+
46
+
47
+ # ============================================================================
48
+ # CONTRACTS DIRECTORY RESOLUTION
49
+ # ============================================================================
50
+
51
+ def get_contracts_dir():
52
+ """Determines the correct contracts directory based on execution context."""
53
+ return resolve_config_dir()
54
+
55
+
56
+ DEFAULT_CONTRACTS_DIR = get_contracts_dir()
57
+
58
+
59
+ # ============================================================================
60
+ # UNIVERSAL RULES SYSTEM
61
+ # ============================================================================
62
+
63
+ DEFAULT_RULES_FILE = "universal-rules.json"
64
+
65
+
66
+ def load_universal_rules(agent_name: str, rules_file: Optional[Path] = None) -> Dict[str, Any]:
67
+ """Load universal rules and agent-specific rules from JSON file."""
68
+ if rules_file is None:
69
+ rules_file = get_contracts_dir() / DEFAULT_RULES_FILE
70
+
71
+ if not rules_file.is_file():
72
+ print(f"Warning: Rules file not found: {rules_file}", file=sys.stderr)
73
+ return {"universal": [], "agent_specific": []}
74
+
75
+ try:
76
+ with open(rules_file, 'r', encoding='utf-8') as f:
77
+ rules_data = json.load(f)
78
+
79
+ universal = [r["rule"] for r in rules_data.get("rules", {}).get("universal", [])]
80
+ # agent_specific values may be a flat list [{rule:...}] or a nested
81
+ # dict {"rules": [{rule:...}]} -- handle both formats.
82
+ agent_raw = rules_data.get("rules", {}).get("agent_specific", {}).get(agent_name, [])
83
+ if isinstance(agent_raw, dict):
84
+ agent_raw = agent_raw.get("rules", [])
85
+ agent_specific = [r["rule"] for r in agent_raw]
86
+
87
+ total_rules = len(universal) + len(agent_specific)
88
+ if total_rules > 0:
89
+ print(f"Loaded {len(universal)} universal rules, {len(agent_specific)} agent-specific", file=sys.stderr)
90
+
91
+ return {
92
+ "universal": universal,
93
+ "agent_specific": agent_specific
94
+ }
95
+ except Exception as e:
96
+ print(f"Warning: Could not load rules: {e}", file=sys.stderr)
97
+ return {"universal": [], "agent_specific": []}
98
+
99
+
100
+ # ============================================================================
101
+ # CLOUD PROVIDER DETECTION
102
+ # ============================================================================
103
+
104
+ def detect_cloud_provider(project_context: Dict[str, Any]) -> str:
105
+ """Detects the cloud provider from project-context.json.
106
+
107
+ Detection priority:
108
+ 1. metadata.cloud_provider (explicit user/scanner setting)
109
+ 2. infrastructure.cloud_providers[0].name (v2 scanner section)
110
+ 3. metadata.project_id presence -> gcp
111
+ 4. Fallback -> gcp
112
+ """
113
+ metadata = project_context.get("metadata", {})
114
+ if "cloud_provider" in metadata:
115
+ provider = metadata["cloud_provider"].lower()
116
+ if provider == "multi-cloud":
117
+ print("Multi-cloud detected, using GCP contracts as primary", file=sys.stderr)
118
+ return "gcp"
119
+ return provider
120
+
121
+ sections = project_context.get("sections", {})
122
+
123
+ # v2: read from infrastructure.cloud_providers
124
+ infra = sections.get("infrastructure", {})
125
+ if isinstance(infra, dict):
126
+ cloud_providers = infra.get("cloud_providers", [])
127
+ if isinstance(cloud_providers, list) and cloud_providers:
128
+ primary = cloud_providers[0]
129
+ if isinstance(primary, dict):
130
+ name = primary.get("name", "")
131
+ if name:
132
+ provider = name.lower()
133
+ if provider == "multi-cloud":
134
+ return "gcp"
135
+ return provider
136
+
137
+ if "project_id" in metadata:
138
+ return "gcp"
139
+
140
+ print("Could not detect cloud provider, defaulting to GCP", file=sys.stderr)
141
+ return "gcp"
142
+
143
+
144
+ def load_provider_contracts(cloud_provider: str, contracts_dir: Path = DEFAULT_CONTRACTS_DIR) -> Dict[str, Any]:
145
+ """
146
+ Loads context contracts using the base+cloud merge strategy.
147
+
148
+ Strategy:
149
+ 1. Load base contracts from context-contracts.json (cloud-agnostic)
150
+ 2. Load cloud overrides from cloud/{provider}.json and merge (extend) read/write lists
151
+ 3. If base contracts missing → error (contracts are the single source of truth)
152
+ """
153
+ base_file = contracts_dir / "context-contracts.json"
154
+ cloud_file = contracts_dir / "cloud" / f"{cloud_provider}.json"
155
+
156
+ # --- Step 1: Load base contracts ---
157
+ if not base_file.is_file():
158
+ print(f"Error: Contract file not found at {base_file}", file=sys.stderr)
159
+ sys.exit(1)
160
+
161
+ try:
162
+ with open(base_file, 'r', encoding='utf-8') as f:
163
+ base_contracts = json.load(f)
164
+ print(f"Loaded base contracts from {base_file}", file=sys.stderr)
165
+ except json.JSONDecodeError as e:
166
+ print(f"Error: Invalid JSON in {base_file}: {e}", file=sys.stderr)
167
+ sys.exit(1)
168
+
169
+ # --- Step 2: Merge cloud-specific overrides ---
170
+ if cloud_file.is_file():
171
+ try:
172
+ with open(cloud_file, 'r', encoding='utf-8') as f:
173
+ cloud_overrides = json.load(f)
174
+ print(f"Loaded {cloud_provider.upper()} cloud overrides from {cloud_file}", file=sys.stderr)
175
+
176
+ for agent_name, agent_overrides in cloud_overrides.get("agents", {}).items():
177
+ if agent_name in base_contracts.get("agents", {}):
178
+ existing_read = base_contracts["agents"][agent_name].get("read", [])
179
+ existing_write = base_contracts["agents"][agent_name].get("write", [])
180
+ extra_read = [s for s in agent_overrides.get("read", []) if s not in existing_read]
181
+ extra_write = [s for s in agent_overrides.get("write", []) if s not in existing_write]
182
+ base_contracts["agents"][agent_name]["read"] = existing_read + extra_read
183
+ base_contracts["agents"][agent_name]["write"] = existing_write + extra_write
184
+ else:
185
+ base_contracts["agents"][agent_name] = agent_overrides
186
+
187
+ except json.JSONDecodeError as e:
188
+ print(f"Warning: Invalid JSON in {cloud_file}: {e} — skipping cloud overrides", file=sys.stderr)
189
+ else:
190
+ print(f"No cloud overrides found at {cloud_file}, using base contracts only", file=sys.stderr)
191
+
192
+ return {
193
+ "version": base_contracts.get("version", "unknown"),
194
+ "provider": cloud_provider,
195
+ "agents": base_contracts.get("agents", {})
196
+ }
197
+
198
+
199
+ def load_project_context(context_path: Path) -> Dict[str, Any]:
200
+ """Loads the project context from the specified JSON file."""
201
+ if not context_path.is_file():
202
+ print(f"Error: Context file not found at {context_path}", file=sys.stderr)
203
+ sys.exit(1)
204
+ with open(context_path, 'r', encoding='utf-8') as f:
205
+ return json.load(f)
206
+
207
+
208
+ # ============================================================================
209
+ # CONTEXT EXTRACTION
210
+ # ============================================================================
211
+
212
+ def get_relevant_sections(
213
+ sections: Dict[str, Any],
214
+ contract_keys: List[str],
215
+ surface_routing: Optional[Dict[str, Any]] = None,
216
+ routing_config: Optional[Dict[str, Any]] = None,
217
+ ) -> Dict[str, Any]:
218
+ """Filter sections by surface relevance, with fallback to all readable sections.
219
+
220
+ Args:
221
+ sections: All available sections from project-context.json.
222
+ contract_keys: The agent's permitted read keys (from context-contracts).
223
+ surface_routing: The routing result from classify_surfaces().
224
+ routing_config: The full surface-routing.json config (has contract_sections per surface).
225
+
226
+ Returns:
227
+ Filtered dict of sections. Falls back to all readable sections when:
228
+ - No surface_routing or routing_config provided
229
+ - No active surfaces detected
230
+ - Surface has no contract_sections defined
231
+ - Intersection of surface sections and agent permissions is empty
232
+ """
233
+ all_readable = {k: sections[k] for k in contract_keys if k in sections}
234
+
235
+ if not surface_routing or not routing_config:
236
+ return all_readable
237
+
238
+ active_surfaces = surface_routing.get("active_surfaces", [])
239
+ if not active_surfaces:
240
+ return all_readable
241
+
242
+ surfaces_cfg = routing_config.get("surfaces", {})
243
+
244
+ # Collect relevant sections from all active surfaces
245
+ relevant: set = set()
246
+ for surface in active_surfaces:
247
+ surface_config = surfaces_cfg.get(surface, {})
248
+ surface_sections = surface_config.get("contract_sections", [])
249
+ relevant.update(surface_sections)
250
+
251
+ if not relevant:
252
+ # Surfaces have no contract_sections defined -- inject all (fallback)
253
+ return all_readable
254
+
255
+ # Filter: agent permissions AND surface relevance
256
+ filtered = {k: sections[k] for k in contract_keys if k in sections and k in relevant}
257
+
258
+ if not filtered:
259
+ # Nothing matched -- inject all (fallback)
260
+ return all_readable
261
+
262
+ omitted = set(all_readable.keys()) - set(filtered.keys())
263
+ if omitted:
264
+ print(
265
+ f"Surface gating: {len(filtered)} sections injected, "
266
+ f"{len(omitted)} omitted ({', '.join(sorted(omitted))})",
267
+ file=sys.stderr,
268
+ )
269
+ else:
270
+ print(
271
+ f"Surface gating: all {len(filtered)} readable sections match active surfaces",
272
+ file=sys.stderr,
273
+ )
274
+
275
+ return filtered
276
+
277
+
278
+ def get_contract_context(
279
+ project_context: Dict[str, Any],
280
+ agent_name: str,
281
+ provider_contracts: Dict[str, Any],
282
+ surface_routing: Optional[Dict[str, Any]] = None,
283
+ routing_config: Optional[Dict[str, Any]] = None,
284
+ ) -> Dict[str, Any]:
285
+ """Extracts the contract-defined context sections for a given agent.
286
+
287
+ When surface_routing and routing_config are provided, sections are filtered
288
+ to only those relevant to the active surface(s). Falls back to returning
289
+ all readable sections when routing is unavailable or yields an empty set.
290
+ """
291
+ agent_contract = provider_contracts.get("agents", {}).get(agent_name)
292
+ if not agent_contract:
293
+ print(f"ERROR: Invalid agent '{agent_name}'. Available: {list(provider_contracts.get('agents', {}).keys())}", file=sys.stderr)
294
+ sys.exit(1)
295
+
296
+ contract_keys = agent_contract.get("read", [])
297
+
298
+ sections = project_context.get("sections", {})
299
+ if not sections:
300
+ raise KeyError("project-context.json must contain a 'sections' object.")
301
+
302
+ return get_relevant_sections(
303
+ sections, contract_keys,
304
+ surface_routing=surface_routing,
305
+ routing_config=routing_config,
306
+ )
307
+
308
+
309
+ def get_context_update_contract(
310
+ agent_name: str,
311
+ provider_contracts: Dict[str, Any]
312
+ ) -> Dict[str, Any]:
313
+ """Return the SSOT contract agents should use for CONTEXT_UPDATE decisions."""
314
+ agent_contract = provider_contracts.get("agents", {}).get(agent_name)
315
+ if not agent_contract:
316
+ print(f"ERROR: Invalid agent '{agent_name}'. Available: {list(provider_contracts.get('agents', {}).keys())}", file=sys.stderr)
317
+ sys.exit(1)
318
+
319
+ return {
320
+ "readable_sections": agent_contract.get("read", []),
321
+ "writable_sections": agent_contract.get("write", []),
322
+ "source": "config/context-contracts.json + config/cloud/{provider}.json",
323
+ }
324
+
325
+
326
+ # ============================================================================
327
+ # EPISODIC MEMORY
328
+ # ============================================================================
329
+
330
+ try:
331
+ from tools.memory.scoring import rank_episodes as _rank_episodes
332
+ _HAS_SCORING = True
333
+ except ImportError:
334
+ try:
335
+ import importlib, sys as _sys
336
+ _scoring = importlib.import_module("tools.memory.scoring")
337
+ _rank_episodes = _scoring.rank_episodes
338
+ _HAS_SCORING = True
339
+ except ImportError:
340
+ _rank_episodes = None
341
+ _HAS_SCORING = False
342
+
343
+ try:
344
+ from tools.memory.search_store import search as fts5_search
345
+ except ImportError:
346
+ fts5_search = None
347
+
348
+
349
+ def _estimate_tokens(text: str) -> int:
350
+ """Rough token estimate: 1 token ≈ 4 characters."""
351
+ return len(text) // 4
352
+
353
+
354
+ def _build_memory_index_table(index_episodes: List[Dict[str, Any]]) -> str:
355
+ """Build a compact markdown table of all memory sources for Layer 1."""
356
+ from datetime import datetime, timezone
357
+ lines = ["## Memory Index", "", "| # | Title | Type | Score | Age |", "|----|-------|------|-------|-----|"]
358
+ for i, ep in enumerate(index_episodes, 1):
359
+ title = ep.get("title", "")[:40]
360
+ ep_type = ep.get("type", "unknown")
361
+ score = ep.get("relevance_score", ep.get("_score", 0.0))
362
+ # Calculate age from timestamp field
363
+ ts = ep.get("timestamp", "")
364
+ try:
365
+ if ts:
366
+ ep_time = datetime.fromisoformat(ts.replace("Z", "+00:00"))
367
+ age = (datetime.now(timezone.utc) - ep_time).days
368
+ age_str = f"{age}d"
369
+ else:
370
+ age_str = "?d"
371
+ except Exception:
372
+ age_str = "?d"
373
+ lines.append(f"| {i} | {title} | {ep_type} | {score:.2f} | {age_str} |")
374
+ return "\n".join(lines)
375
+
376
+
377
+ def _fallback_keyword_score(episode: Dict[str, Any], user_task: str) -> float:
378
+ """Keyword-based relevance scoring fallback when scoring module is unavailable."""
379
+ task_lower = user_task.lower()
380
+ task_words = set(task_lower.split())
381
+ score = 0.0
382
+ for tag in episode.get("tags", []):
383
+ if tag.lower() in task_lower:
384
+ score += 0.4
385
+ title_words = set(episode.get("title", "").lower().split())
386
+ common_words = task_words & title_words
387
+ if common_words:
388
+ score += 0.3 * (len(common_words) / max(len(title_words), 1))
389
+ return score * episode.get("relevance_score", 0.5)
390
+
391
+
392
+ def load_relevant_episodes(
393
+ user_task: str,
394
+ max_episodes: int = 2,
395
+ max_tokens: Optional[int] = None,
396
+ ) -> Dict[str, Any]:
397
+ """Load relevant historical episodes using 2-layer progressive disclosure.
398
+
399
+ Layer 1 (always): A compact markdown table of all scored memory sources
400
+ (~200 tokens), returned under the ``memory_index`` key.
401
+
402
+ Layer 2 (selective): Full content of top-N episodes ranked by
403
+ ``tools.memory.scoring.rank_episodes()``, loaded only within the
404
+ remaining token budget.
405
+
406
+ Parameters
407
+ ----------
408
+ user_task:
409
+ Free-text description of the user's current task.
410
+ max_episodes:
411
+ Legacy cap on the number of full episodes to include (Layer 2).
412
+ max_tokens:
413
+ Total token budget for the episodic memory block. Reads from
414
+ ``GAIA_MEMORY_TOKEN_BUDGET`` env var when not supplied explicitly.
415
+ Defaults to 2000.
416
+ """
417
+ import os as _os
418
+
419
+ if max_tokens is None:
420
+ env_budget = _os.environ.get("GAIA_MEMORY_TOKEN_BUDGET")
421
+ if env_budget:
422
+ try:
423
+ max_tokens = int(env_budget)
424
+ except ValueError:
425
+ max_tokens = 2000
426
+ else:
427
+ max_tokens = 2000
428
+
429
+ try:
430
+ index_file = Path(".claude/project-context/episodic-memory/index.json")
431
+ if not index_file.exists():
432
+ return {}
433
+
434
+ with open(index_file) as f:
435
+ index = json.load(f)
436
+
437
+ all_index_episodes = index.get("episodes", [])
438
+ if not all_index_episodes:
439
+ return {}
440
+
441
+ # --- Layer 1: Memory Index -- compact markdown table (~200 tokens, always included) ---
442
+ layer1_text = _build_memory_index_table(all_index_episodes)
443
+ layer1_tokens = _estimate_tokens(layer1_text)
444
+ remaining_budget = max_tokens - layer1_tokens
445
+
446
+ # --- Score and rank episodes: hybrid FTS5 + keyword fallback ---
447
+ # Build a lookup map from episode id to index entry for fast access
448
+ ep_by_id = {ep["id"]: ep for ep in all_index_episodes if "id" in ep}
449
+
450
+ # Try FTS5 first; results are BM25-ranked (better quality)
451
+ fts5_ids: List[str] = []
452
+ if fts5_search is not None:
453
+ try:
454
+ fts5_results = fts5_search(user_task, max_results=max_episodes * 3)
455
+ fts5_ids = [r["episode_id"] for r in fts5_results if "episode_id" in r]
456
+ print(
457
+ f"FTS5 search returned {len(fts5_ids)} candidates for retrieval",
458
+ file=sys.stderr,
459
+ )
460
+ except Exception as _fts_err:
461
+ print(
462
+ f"Warning: FTS5 search failed (non-fatal): {_fts_err}",
463
+ file=sys.stderr,
464
+ )
465
+ fts5_ids = []
466
+
467
+ # Build ranked list: FTS5 hits first, then fill with keyword/scoring results
468
+ fts5_id_set = set(fts5_ids)
469
+
470
+ # Keyword/scoring baseline (used to fill gaps and as full fallback)
471
+ if _HAS_SCORING and _rank_episodes is not None:
472
+ keyword_ranked = _rank_episodes(all_index_episodes, user_task)
473
+ else:
474
+ keyword_ranked = sorted(
475
+ [dict(ep, _score=_fallback_keyword_score(ep, user_task)) for ep in all_index_episodes],
476
+ key=lambda x: x["_score"],
477
+ reverse=True,
478
+ )
479
+
480
+ # If FTS5 found candidates, prepend them (with decay scoring if available)
481
+ if fts5_ids:
482
+ fts5_episodes = [ep_by_id[eid] for eid in fts5_ids if eid in ep_by_id]
483
+ if _HAS_SCORING and _rank_episodes is not None:
484
+ fts5_episodes = _rank_episodes(fts5_episodes, user_task)
485
+ else:
486
+ # Assign a generous score so they sort above keyword results
487
+ fts5_episodes = [dict(ep, _score=max(ep.get("_score", 0.0), 0.5)) for ep in fts5_episodes]
488
+ # Fill remaining slots with keyword results not already in FTS5 set
489
+ keyword_fill = [ep for ep in keyword_ranked if ep.get("id") not in fts5_id_set]
490
+ ranked = fts5_episodes + keyword_fill
491
+ else:
492
+ # FTS5 not available or returned nothing — fall back entirely to keyword
493
+ ranked = keyword_ranked
494
+
495
+ # --- Layer 2: full content of top episodes within remaining budget ---
496
+ full_episodes = []
497
+ tokens_used = 0
498
+ for ep in ranked:
499
+ if len(full_episodes) >= max_episodes:
500
+ break
501
+ score = ep.get("_score", 0.0)
502
+ if score <= 0.05:
503
+ continue
504
+ if remaining_budget <= 0:
505
+ break
506
+
507
+ full_ep = load_full_episode(ep["id"], index_file.parent)
508
+ if not full_ep:
509
+ continue
510
+
511
+ episode_entry = {
512
+ "id": full_ep["id"],
513
+ "title": full_ep["title"],
514
+ "type": full_ep["type"],
515
+ "relevance": round(score, 4),
516
+ "lessons_learned": full_ep.get("lessons_learned", [])[:2],
517
+ "resolution": full_ep.get("resolution", "")[:200],
518
+ }
519
+ entry_text = json.dumps(episode_entry)
520
+ entry_tokens = _estimate_tokens(entry_text)
521
+
522
+ if tokens_used + entry_tokens > remaining_budget:
523
+ break
524
+
525
+ full_episodes.append(episode_entry)
526
+ tokens_used += entry_tokens
527
+
528
+ result: Dict[str, Any] = {
529
+ "memory_index": layer1_text,
530
+ }
531
+
532
+ if full_episodes:
533
+ result["episodes"] = full_episodes
534
+ result["summary"] = f"Found {len(full_episodes)} relevant historical episodes"
535
+ print(
536
+ f"Added {len(full_episodes)} historical episodes to context "
537
+ f"(budget={max_tokens}, used≈{layer1_tokens + tokens_used})",
538
+ file=sys.stderr,
539
+ )
540
+ else:
541
+ print(
542
+ f"Memory index built ({len(all_index_episodes)} entries, "
543
+ f"no full episodes within score/budget threshold)",
544
+ file=sys.stderr,
545
+ )
546
+
547
+ # --- Retrieval strengthening: update retrieval_count + last_retrieved ---
548
+ # Failure here must never block context injection.
549
+ try:
550
+ if full_episodes:
551
+ import tempfile as _tempfile
552
+ from datetime import datetime as _datetime
553
+
554
+ selected_ids = {ep["id"] for ep in full_episodes}
555
+ now_iso = _datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
556
+
557
+ updated = False
558
+ for entry in index.get("episodes", []):
559
+ if entry.get("id") in selected_ids:
560
+ entry["retrieval_count"] = entry.get("retrieval_count", 0) + 1
561
+ entry["last_retrieved"] = now_iso
562
+ updated = True
563
+
564
+ if updated:
565
+ index_path = index_file.resolve()
566
+ index_dir = index_path.parent
567
+ fd, tmp_path = _tempfile.mkstemp(
568
+ dir=str(index_dir), suffix=".tmp", prefix="index_"
569
+ )
570
+ try:
571
+ with _os.fdopen(fd, "w", encoding="utf-8") as tf:
572
+ json.dump(index, tf, indent=2)
573
+ _os.rename(tmp_path, str(index_path))
574
+ print(
575
+ f"Retrieval strengthening: updated {len(selected_ids)} episode(s)",
576
+ file=sys.stderr,
577
+ )
578
+ except Exception:
579
+ try:
580
+ _os.unlink(tmp_path)
581
+ except Exception:
582
+ pass
583
+ raise
584
+ except Exception as _rs_err:
585
+ print(
586
+ f"Warning: retrieval_count update failed (non-fatal): {_rs_err}",
587
+ file=sys.stderr,
588
+ )
589
+
590
+ return result
591
+
592
+ except Exception as e:
593
+ print(f"Warning: Could not load episodic memory: {e}", file=sys.stderr)
594
+ return {}
595
+
596
+
597
+ def load_full_episode(episode_id: str, memory_dir: Path) -> Optional[Dict[str, Any]]:
598
+ """Load full episode details from JSONL file."""
599
+ try:
600
+ episodes_file = memory_dir / "episodes.jsonl"
601
+ if episodes_file.exists():
602
+ with open(episodes_file) as f:
603
+ for line in f:
604
+ try:
605
+ episode = json.loads(line)
606
+ if episode.get("id") == episode_id:
607
+ return episode
608
+ except Exception:
609
+ continue
610
+ except Exception:
611
+ pass
612
+ return None
613
+
614
+
615
+ # ============================================================================
616
+ # MAIN FUNCTION
617
+ # ============================================================================
618
+
619
+ def main():
620
+ """Main function to generate and print the context payload."""
621
+ import os as _os
622
+
623
+ parser = argparse.ArgumentParser(
624
+ description="Generates a structured context payload for a Claude agent."
625
+ )
626
+ parser.add_argument("agent_name", help="The name of the agent being invoked.")
627
+ parser.add_argument("user_task", nargs="?", default="General inquiry",
628
+ help="The user's task or query for the agent.")
629
+ parser.add_argument(
630
+ "--context-file",
631
+ type=Path,
632
+ default=DEFAULT_CONTEXT_PATH,
633
+ help=f"Path to the project-context.json file. Defaults to '{DEFAULT_CONTEXT_PATH}'"
634
+ )
635
+ parser.add_argument(
636
+ "--memory-token-budget",
637
+ type=int,
638
+ default=None,
639
+ help=(
640
+ "Token budget for episodic memory injection. "
641
+ "Overrides GAIA_MEMORY_TOKEN_BUDGET env var. Default: 2000."
642
+ ),
643
+ )
644
+
645
+ args = parser.parse_args()
646
+
647
+ # Resolve memory token budget: CLI arg > env var > default
648
+ memory_token_budget: Optional[int] = args.memory_token_budget
649
+ if memory_token_budget is None:
650
+ env_budget = _os.environ.get("GAIA_MEMORY_TOKEN_BUDGET")
651
+ if env_budget:
652
+ try:
653
+ memory_token_budget = int(env_budget)
654
+ except ValueError:
655
+ memory_token_budget = 2000
656
+ else:
657
+ memory_token_budget = 2000
658
+
659
+ # Load project context
660
+ project_context = load_project_context(args.context_file)
661
+
662
+ # Detect cloud provider and load contracts
663
+ cloud_provider = detect_cloud_provider(project_context)
664
+ provider_contracts = load_provider_contracts(cloud_provider)
665
+
666
+ # Compute surface routing BEFORE extracting sections so we can gate by surface
667
+ surface_routing_config = load_surface_routing_config()
668
+ surface_routing = classify_surfaces(
669
+ args.user_task,
670
+ current_agent=args.agent_name,
671
+ routing_config=surface_routing_config,
672
+ )
673
+
674
+ # Extract contracted sections (surface-gated when routing is available)
675
+ contract_context = get_contract_context(
676
+ project_context, args.agent_name, provider_contracts,
677
+ surface_routing=surface_routing,
678
+ routing_config=surface_routing_config,
679
+ )
680
+ context_update_contract = get_context_update_contract(args.agent_name, provider_contracts)
681
+
682
+ # Load historical episodes (2-layer progressive disclosure)
683
+ historical_context = load_relevant_episodes(args.user_task, max_tokens=memory_token_budget)
684
+
685
+ # Load universal rules
686
+ rules_context = load_universal_rules(args.agent_name)
687
+ investigation_brief = build_investigation_brief(
688
+ args.user_task,
689
+ args.agent_name,
690
+ contract_context,
691
+ routing_config=surface_routing_config,
692
+ routing=surface_routing,
693
+ )
694
+
695
+ # Build final payload
696
+ final_payload = {
697
+ "project_knowledge": contract_context,
698
+ "write_permissions": context_update_contract,
699
+ "rules": rules_context,
700
+ "surface_routing": surface_routing,
701
+ "investigation_brief": investigation_brief,
702
+ "metadata": {
703
+ "cloud_provider": cloud_provider,
704
+ "contract_version": provider_contracts.get("version", "unknown"),
705
+ "historical_episodes_count": len(historical_context.get("episodes", [])),
706
+ "rules_count": len(rules_context.get("universal", [])) + len(rules_context.get("agent_specific", [])),
707
+ "surface_routing_version": surface_routing_config.get("version", "unknown"),
708
+ "active_surfaces_count": len(surface_routing.get("active_surfaces", [])),
709
+ "surface_routing_confidence": surface_routing.get("confidence", 0.0),
710
+ }
711
+ }
712
+
713
+ # Add historical context if episodes found
714
+ if historical_context:
715
+ final_payload["historical_context"] = historical_context
716
+
717
+ print(json.dumps(final_payload, indent=2))
718
+
719
+
720
+ if __name__ == "__main__":
721
+ main()