@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,1210 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Episodic Memory System for GAIA-OPS
4
+
5
+ This module provides functionality to store, index, and search episodic memory
6
+ for the workflow system. Episodes capture user interactions, clarifications,
7
+ and enriched prompts for future reference and context enhancement.
8
+
9
+ Architecture:
10
+ - Episodes stored as individual JSON files with metadata
11
+ - JSONL index for fast keyword-based search
12
+ - Automatic directory creation and management
13
+ - Integration with workflow.py for context enhancement
14
+
15
+ P0 Enhancement: Outcome tracking (success/failure/partial, duration, commands)
16
+ P1 Enhancement: Simple relationships between episodes (SOLVES, CAUSES, etc.)
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import sys
22
+ import uuid
23
+ from datetime import datetime, timezone, timedelta
24
+ from pathlib import Path
25
+ from typing import Dict, List, Any, Optional, Union
26
+ import re
27
+ from dataclasses import dataclass, asdict, field
28
+ import hashlib
29
+
30
+ try:
31
+ from tools.memory.search_store import index_episode as _fts5_index
32
+ except ImportError:
33
+ _fts5_index = None
34
+
35
+
36
+ # Valid relationship types for episode connections
37
+ RELATIONSHIP_TYPES = frozenset([
38
+ "SOLVES", # This episode solves another (problem -> solution)
39
+ "CAUSES", # This episode caused another (action -> consequence)
40
+ "DEPENDS_ON", # This episode depends on another
41
+ "VALIDATES", # This episode validates another
42
+ "SUPERSEDES", # This episode replaces another
43
+ "RELATED_TO", # Generic relation
44
+ ])
45
+
46
+ # Valid outcome values
47
+ OUTCOME_VALUES = frozenset(["success", "partial", "failed", "abandoned"])
48
+
49
+
50
+ @dataclass
51
+ class Episode:
52
+ """Represents a single episodic memory entry."""
53
+ episode_id: str
54
+ timestamp: str
55
+ keywords: List[str]
56
+ prompt: str
57
+ clarifications: Dict[str, Any]
58
+ enriched_prompt: str
59
+ context: Dict[str, Any]
60
+ tags: Optional[List[str]] = None
61
+ type: Optional[str] = None
62
+ title: Optional[str] = None
63
+ relevance_score: float = 1.0
64
+ # P0: Outcome tracking fields
65
+ outcome: Optional[str] = None # "success", "partial", "failed", "abandoned"
66
+ success: Optional[bool] = None
67
+ duration_seconds: Optional[float] = None
68
+ commands_executed: Optional[List[str]] = None
69
+ # P1: Simple relationships
70
+ related_episodes: Optional[List[Dict[str, str]]] = None # [{"id": "ep_xxx", "type": "SOLVES"}]
71
+
72
+ def to_dict(self) -> Dict[str, Any]:
73
+ """Convert episode to dictionary."""
74
+ data = asdict(self)
75
+ return {k: v for k, v in data.items() if v is not None}
76
+
77
+
78
+ class EpisodicMemory:
79
+ """
80
+ Manages episodic memory storage and retrieval.
81
+
82
+ This class provides methods to:
83
+ - Store new episodes with automatic indexing
84
+ - Search episodes by keywords and context
85
+ - Maintain an efficient index for fast retrieval
86
+ - Auto-create required directory structures
87
+ - Track outcomes and relationships between episodes (P0/P1)
88
+ """
89
+
90
+ def __init__(self, base_path: Optional[Union[str, Path]] = None):
91
+ """
92
+ Initialize EpisodicMemory with specified or default path.
93
+
94
+ Args:
95
+ base_path: Base directory for episodic memory storage.
96
+ Defaults to .claude/project-context/episodic-memory/
97
+ """
98
+ if base_path:
99
+ self.base_path = Path(base_path)
100
+ else:
101
+ # Try to find the best location
102
+ candidates = [
103
+ Path(".claude/project-context/episodic-memory"),
104
+ ]
105
+
106
+ # Use first existing or first candidate
107
+ for path in candidates:
108
+ if path.parent.exists():
109
+ self.base_path = path
110
+ break
111
+ else:
112
+ self.base_path = candidates[0]
113
+
114
+ self.episodes_dir = self.base_path / "episodes"
115
+ self.index_file = self.base_path / "index.json"
116
+ self.episodes_jsonl = self.base_path / "episodes.jsonl"
117
+
118
+ # Auto-create directories
119
+ self._ensure_directories()
120
+
121
+ def _ensure_directories(self):
122
+ """Create required directories if they don't exist."""
123
+ self.base_path.mkdir(parents=True, exist_ok=True)
124
+ self.episodes_dir.mkdir(parents=True, exist_ok=True)
125
+
126
+ if not self.index_file.exists():
127
+ self._save_index({
128
+ "episodes": [],
129
+ "relationships": [], # P1: Track relationships in index
130
+ "metadata": {"created": datetime.now(timezone.utc).isoformat()}
131
+ })
132
+
133
+ def _save_index(self, index_data: Dict[str, Any]):
134
+ """Save index to JSON file."""
135
+ with open(self.index_file, 'w') as f:
136
+ json.dump(index_data, f, indent=2)
137
+
138
+ def _load_index(self) -> Dict[str, Any]:
139
+ """Load index from JSON file."""
140
+ if not self.index_file.exists():
141
+ return {"episodes": [], "relationships": [], "metadata": {}}
142
+
143
+ try:
144
+ with open(self.index_file, 'r') as f:
145
+ index = json.load(f)
146
+ # Ensure relationships key exists for backward compatibility
147
+ if "relationships" not in index:
148
+ index["relationships"] = []
149
+ return index
150
+ except (json.JSONDecodeError, IOError):
151
+ # Return empty index if file is corrupted
152
+ return {"episodes": [], "relationships": [], "metadata": {}}
153
+
154
+ def _extract_keywords(self, text: str) -> List[str]:
155
+ """
156
+ Extract keywords from text for indexing.
157
+
158
+ Uses simple tokenization and filtering. Can be enhanced with NLP.
159
+
160
+ Args:
161
+ text: Text to extract keywords from
162
+
163
+ Returns:
164
+ List of keywords
165
+ """
166
+ words = re.findall(r'\b[a-z]+\b', text.lower())
167
+
168
+ stopwords = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
169
+ 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'been', 'be',
170
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should',
171
+ 'could', 'may', 'might', 'can', 'must', 'shall', 'need', 'dare'}
172
+
173
+ keywords = [w for w in words if w not in stopwords and len(w) > 2]
174
+
175
+ seen = set()
176
+ unique_keywords = []
177
+ for kw in keywords:
178
+ if kw not in seen:
179
+ seen.add(kw)
180
+ unique_keywords.append(kw)
181
+
182
+ return unique_keywords[:20] # Limit to 20 keywords
183
+
184
+ def _generate_title(self, prompt: str) -> str:
185
+ """Generate a short title from the prompt."""
186
+ # Take first 60 characters or first sentence
187
+ title = prompt.split('.')[0] if '.' in prompt else prompt
188
+ return title[:60] + ('...' if len(title) > 60 else '')
189
+
190
+ def _determine_type(self, prompt: str, context: Dict[str, Any]) -> str:
191
+ """Determine episode type based on prompt and context."""
192
+ prompt_lower = prompt.lower()
193
+
194
+ # Check for common operation types
195
+ if any(word in prompt_lower for word in ['deploy', 'apply', 'push', 'release']):
196
+ return 'deployment'
197
+ elif any(word in prompt_lower for word in ['fix', 'error', 'issue', 'problem', 'debug']):
198
+ return 'troubleshooting'
199
+ elif any(word in prompt_lower for word in ['create', 'add', 'new', 'setup', 'init']):
200
+ return 'creation'
201
+ elif any(word in prompt_lower for word in ['update', 'modify', 'change', 'edit']):
202
+ return 'modification'
203
+ elif any(word in prompt_lower for word in ['check', 'verify', 'test', 'validate']):
204
+ return 'validation'
205
+ elif any(word in prompt_lower for word in ['delete', 'remove', 'clean']):
206
+ return 'deletion'
207
+ else:
208
+ return 'general'
209
+
210
+ def store_episode(
211
+ self,
212
+ prompt: str,
213
+ clarifications: Optional[Dict[str, Any]] = None,
214
+ enriched_prompt: Optional[str] = None,
215
+ context: Optional[Dict[str, Any]] = None,
216
+ tags: Optional[List[str]] = None,
217
+ episode_id: Optional[str] = None,
218
+ # P0: Outcome tracking parameters
219
+ outcome: Optional[str] = None,
220
+ success: Optional[bool] = None,
221
+ duration_seconds: Optional[float] = None,
222
+ commands_executed: Optional[List[str]] = None,
223
+ # P1: Relationship parameters
224
+ related_episodes: Optional[List[Dict[str, str]]] = None,
225
+ # P3: Workflow metric fields for CLI compatibility
226
+ workflow_metrics: Optional[Dict] = None
227
+ ) -> str:
228
+ """
229
+ Store a new episode in memory.
230
+
231
+ Args:
232
+ prompt: Original user prompt
233
+ clarifications: Any clarifications made during processing
234
+ enriched_prompt: Enriched version of the prompt
235
+ context: Additional context information
236
+ tags: Optional tags for categorization
237
+ episode_id: Optional specific ID (auto-generated if not provided)
238
+ outcome: Episode outcome ("success", "partial", "failed", "abandoned")
239
+ success: Boolean indicating if episode was successful
240
+ duration_seconds: How long the episode took to complete
241
+ commands_executed: List of commands executed during episode
242
+ related_episodes: List of related episode references [{"id": "ep_xxx", "type": "SOLVES"}]
243
+ workflow_metrics: Optional workflow metrics dict (agent, session_id, task_id, etc.)
244
+
245
+ Returns:
246
+ Episode ID
247
+ """
248
+ if not episode_id:
249
+ episode_id = f"ep_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
250
+
251
+ if outcome is not None and outcome not in OUTCOME_VALUES:
252
+ print(f"Warning: Invalid outcome '{outcome}'. Must be one of {OUTCOME_VALUES}", file=sys.stderr)
253
+ outcome = None
254
+
255
+ validated_relationships = None
256
+ if related_episodes:
257
+ validated_relationships = []
258
+ for rel in related_episodes:
259
+ if isinstance(rel, dict) and "id" in rel and "type" in rel:
260
+ if rel["type"] in RELATIONSHIP_TYPES:
261
+ validated_relationships.append({"id": rel["id"], "type": rel["type"]})
262
+ else:
263
+ print(f"Warning: Invalid relationship type '{rel['type']}'. Skipping.", file=sys.stderr)
264
+ if not validated_relationships:
265
+ validated_relationships = None
266
+
267
+ all_text = prompt
268
+ if enriched_prompt:
269
+ all_text += " " + enriched_prompt
270
+ keywords = self._extract_keywords(all_text)
271
+
272
+ if tags:
273
+ keywords = list(set(keywords + [t.lower() for t in tags]))
274
+
275
+ episode_type = self._determine_type(prompt, context or {})
276
+ title = self._generate_title(enriched_prompt or prompt)
277
+
278
+ episode = Episode(
279
+ episode_id=episode_id,
280
+ timestamp=datetime.now(timezone.utc).isoformat(),
281
+ keywords=keywords,
282
+ prompt=prompt,
283
+ clarifications=clarifications or {},
284
+ enriched_prompt=enriched_prompt or prompt,
285
+ context=context or {},
286
+ tags=tags,
287
+ type=episode_type,
288
+ title=title,
289
+ relevance_score=1.0,
290
+ # P0: Outcome fields
291
+ outcome=outcome,
292
+ success=success,
293
+ duration_seconds=duration_seconds,
294
+ commands_executed=commands_executed,
295
+ # P1: Relationships
296
+ related_episodes=validated_relationships
297
+ )
298
+
299
+ episode_file = self.episodes_dir / f"episode-{episode_id}.json"
300
+ with open(episode_file, 'w') as f:
301
+ json.dump(episode.to_dict(), f, indent=2)
302
+
303
+ # Append to JSONL file (enriched with workflow metrics for consistency)
304
+ jsonl_entry = episode.to_dict()
305
+ if workflow_metrics:
306
+ jsonl_entry["agent"] = workflow_metrics.get("agent", "")
307
+ jsonl_entry["session_id"] = workflow_metrics.get("session_id", "")
308
+ jsonl_entry["task_id"] = workflow_metrics.get("task_id", "")
309
+ jsonl_entry["exit_code"] = workflow_metrics.get("exit_code", 0)
310
+ jsonl_entry["plan_status"] = workflow_metrics.get("plan_status", "")
311
+ jsonl_entry["output_length"] = workflow_metrics.get("output_length", 0)
312
+ jsonl_entry["output_tokens_approx"] = workflow_metrics.get("output_tokens_approx", 0)
313
+ jsonl_entry["wf_prompt"] = workflow_metrics.get("prompt", "")
314
+ with open(self.episodes_jsonl, 'a') as f:
315
+ f.write(json.dumps(jsonl_entry) + '\n')
316
+
317
+ index = self._load_index()
318
+ index_entry = {
319
+ "id": episode_id,
320
+ "timestamp": episode.timestamp,
321
+ "keywords": keywords[:10], # Store limited keywords in index
322
+ "tags": tags or [],
323
+ "type": episode_type,
324
+ "title": title,
325
+ "relevance_score": 1.0,
326
+ # P0: Include outcome summary in index
327
+ "outcome": outcome,
328
+ "success": success,
329
+ # P1: Include relationship count in index
330
+ "relationship_count": len(validated_relationships) if validated_relationships else 0,
331
+ # P3: Workflow metric fields for CLI compatibility
332
+ "agent": (workflow_metrics or {}).get("agent", ""),
333
+ "session_id": (workflow_metrics or {}).get("session_id", ""),
334
+ "task_id": (workflow_metrics or {}).get("task_id", ""),
335
+ "exit_code": (workflow_metrics or {}).get("exit_code", 0),
336
+ "plan_status": (workflow_metrics or {}).get("plan_status", ""),
337
+ "output_length": (workflow_metrics or {}).get("output_length", 0),
338
+ "output_tokens_approx": (workflow_metrics or {}).get("output_tokens_approx", 0),
339
+ "prompt": (workflow_metrics or {}).get("prompt", ""),
340
+ "retrieval_count": 0,
341
+ "last_retrieved": None,
342
+ }
343
+ index["episodes"].append(index_entry)
344
+
345
+ # P1: Add relationships to index for fast lookup
346
+ if validated_relationships:
347
+ for rel in validated_relationships:
348
+ index["relationships"].append({
349
+ "source": episode_id,
350
+ "target": rel["id"],
351
+ "type": rel["type"],
352
+ "timestamp": episode.timestamp
353
+ })
354
+
355
+ # Keep only last N episodes in index (configurable via GAIA_EPISODE_INDEX_LIMIT)
356
+ _episode_index_limit = int(os.environ.get("GAIA_EPISODE_INDEX_LIMIT", "50000"))
357
+ if len(index["episodes"]) > _episode_index_limit:
358
+ index["episodes"] = index["episodes"][-_episode_index_limit:]
359
+
360
+ # Keep only last 5000 relationships in index
361
+ if len(index["relationships"]) > 5000:
362
+ index["relationships"] = index["relationships"][-5000:]
363
+
364
+ # Ensure metadata exists
365
+ if "metadata" not in index:
366
+ index["metadata"] = {}
367
+ index["metadata"]["last_updated"] = datetime.now(timezone.utc).isoformat()
368
+ self._save_index(index)
369
+
370
+ print(f"Stored episode: {episode_id} with {len(keywords)} keywords", file=sys.stderr)
371
+
372
+ if _fts5_index:
373
+ try:
374
+ _fts5_index(episode_id, prompt, enriched_prompt, ' '.join(tags or []), title)
375
+ except Exception:
376
+ pass
377
+
378
+ return episode_id
379
+
380
+ def update_outcome(
381
+ self,
382
+ episode_id: str,
383
+ outcome: str,
384
+ success: bool,
385
+ duration_seconds: Optional[float] = None,
386
+ commands_executed: Optional[List[str]] = None
387
+ ) -> bool:
388
+ """
389
+ Update the outcome of an existing episode.
390
+
391
+ Args:
392
+ episode_id: Episode ID to update
393
+ outcome: New outcome ("success", "partial", "failed", "abandoned")
394
+ success: Boolean indicating success
395
+ duration_seconds: Optional duration in seconds
396
+ commands_executed: Optional list of commands that were executed
397
+
398
+ Returns:
399
+ True if updated successfully, False if episode not found or invalid outcome
400
+ """
401
+ if outcome not in OUTCOME_VALUES:
402
+ print(f"Error: Invalid outcome '{outcome}'. Must be one of {OUTCOME_VALUES}", file=sys.stderr)
403
+ return False
404
+
405
+ episode_file = self.episodes_dir / f"episode-{episode_id}.json"
406
+ if not episode_file.exists():
407
+ print(f"Error: Episode {episode_id} not found", file=sys.stderr)
408
+ return False
409
+
410
+ try:
411
+ with open(episode_file, 'r') as f:
412
+ episode_data = json.load(f)
413
+
414
+ episode_data["outcome"] = outcome
415
+ episode_data["success"] = success
416
+ if duration_seconds is not None:
417
+ episode_data["duration_seconds"] = duration_seconds
418
+ if commands_executed is not None:
419
+ episode_data["commands_executed"] = commands_executed
420
+
421
+ with open(episode_file, 'w') as f:
422
+ json.dump(episode_data, f, indent=2)
423
+
424
+ # Append outcome update to JSONL (as a separate event for audit trail)
425
+ with open(self.episodes_jsonl, 'a') as f:
426
+ outcome_event = {
427
+ "event_type": "outcome_update",
428
+ "episode_id": episode_id,
429
+ "timestamp": datetime.now(timezone.utc).isoformat(),
430
+ "outcome": outcome,
431
+ "success": success,
432
+ "duration_seconds": duration_seconds,
433
+ "commands_executed": commands_executed
434
+ }
435
+ f.write(json.dumps(outcome_event) + '\n')
436
+
437
+ index = self._load_index()
438
+ for ep in index["episodes"]:
439
+ if ep.get("id") == episode_id:
440
+ ep["outcome"] = outcome
441
+ ep["success"] = success
442
+ break
443
+ index["metadata"]["last_updated"] = datetime.now(timezone.utc).isoformat()
444
+ self._save_index(index)
445
+
446
+ print(f"Updated outcome for episode {episode_id}: {outcome} (success={success})", file=sys.stderr)
447
+ return True
448
+
449
+ except (json.JSONDecodeError, IOError) as e:
450
+ print(f"Error updating episode {episode_id}: {e}", file=sys.stderr)
451
+ return False
452
+
453
+ def add_relationship(
454
+ self,
455
+ source_episode_id: str,
456
+ target_episode_id: str,
457
+ relationship_type: str
458
+ ) -> bool:
459
+ """
460
+ Add a relationship between two episodes.
461
+
462
+ Args:
463
+ source_episode_id: The source episode ID
464
+ target_episode_id: The target episode ID
465
+ relationship_type: Type of relationship (SOLVES, CAUSES, DEPENDS_ON, etc.)
466
+
467
+ Returns:
468
+ True if relationship added successfully, False otherwise
469
+ """
470
+ if relationship_type not in RELATIONSHIP_TYPES:
471
+ print(f"Error: Invalid relationship type '{relationship_type}'. Must be one of {RELATIONSHIP_TYPES}", file=sys.stderr)
472
+ return False
473
+
474
+ source_file = self.episodes_dir / f"episode-{source_episode_id}.json"
475
+ if not source_file.exists():
476
+ print(f"Error: Source episode {source_episode_id} not found", file=sys.stderr)
477
+ return False
478
+
479
+ # Check target episode exists (optional - might reference external or future episode)
480
+ target_file = self.episodes_dir / f"episode-{target_episode_id}.json"
481
+ target_exists = target_file.exists()
482
+
483
+ try:
484
+ with open(source_file, 'r') as f:
485
+ source_data = json.load(f)
486
+
487
+ if "related_episodes" not in source_data or source_data["related_episodes"] is None:
488
+ source_data["related_episodes"] = []
489
+
490
+ for rel in source_data["related_episodes"]:
491
+ if rel.get("id") == target_episode_id and rel.get("type") == relationship_type:
492
+ print(f"Relationship already exists: {source_episode_id} --{relationship_type}--> {target_episode_id}", file=sys.stderr)
493
+ return True # Not an error, just already exists
494
+
495
+ source_data["related_episodes"].append({
496
+ "id": target_episode_id,
497
+ "type": relationship_type
498
+ })
499
+
500
+ with open(source_file, 'w') as f:
501
+ json.dump(source_data, f, indent=2)
502
+
503
+ with open(self.episodes_jsonl, 'a') as f:
504
+ rel_event = {
505
+ "event_type": "relationship_added",
506
+ "timestamp": datetime.now(timezone.utc).isoformat(),
507
+ "source": source_episode_id,
508
+ "target": target_episode_id,
509
+ "type": relationship_type,
510
+ "target_exists": target_exists
511
+ }
512
+ f.write(json.dumps(rel_event) + '\n')
513
+
514
+ index = self._load_index()
515
+ index["relationships"].append({
516
+ "source": source_episode_id,
517
+ "target": target_episode_id,
518
+ "type": relationship_type,
519
+ "timestamp": datetime.now(timezone.utc).isoformat()
520
+ })
521
+
522
+ for ep in index["episodes"]:
523
+ if ep.get("id") == source_episode_id:
524
+ ep["relationship_count"] = ep.get("relationship_count", 0) + 1
525
+ break
526
+
527
+ index["metadata"]["last_updated"] = datetime.now(timezone.utc).isoformat()
528
+ self._save_index(index)
529
+
530
+ print(f"Added relationship: {source_episode_id} --{relationship_type}--> {target_episode_id}", file=sys.stderr)
531
+ return True
532
+
533
+ except (json.JSONDecodeError, IOError) as e:
534
+ print(f"Error adding relationship: {e}", file=sys.stderr)
535
+ return False
536
+
537
+ def get_related_episodes(
538
+ self,
539
+ episode_id: str,
540
+ relationship_type: Optional[str] = None,
541
+ direction: str = "outgoing"
542
+ ) -> List[Dict[str, Any]]:
543
+ """
544
+ Get episodes related to the given episode.
545
+
546
+ Args:
547
+ episode_id: The episode to find relationships for
548
+ relationship_type: Optional filter by relationship type
549
+ direction: "outgoing" (this episode points to), "incoming" (points to this), or "both"
550
+
551
+ Returns:
552
+ List of related episodes with relationship info
553
+ """
554
+ results = []
555
+ index = self._load_index()
556
+
557
+ for rel in index.get("relationships", []):
558
+ match = False
559
+
560
+ if direction in ("outgoing", "both") and rel.get("source") == episode_id:
561
+ match = True
562
+ related_id = rel.get("target")
563
+ rel_direction = "outgoing"
564
+ elif direction in ("incoming", "both") and rel.get("target") == episode_id:
565
+ match = True
566
+ related_id = rel.get("source")
567
+ rel_direction = "incoming"
568
+
569
+ if not match:
570
+ continue
571
+
572
+ if relationship_type and rel.get("type") != relationship_type:
573
+ continue
574
+
575
+ related_episode = self.get_episode(related_id)
576
+ if related_episode:
577
+ results.append({
578
+ "episode": related_episode,
579
+ "relationship_type": rel.get("type"),
580
+ "direction": rel_direction,
581
+ "relationship_timestamp": rel.get("timestamp")
582
+ })
583
+
584
+ return results
585
+
586
+ def search_episodes(
587
+ self,
588
+ query: str,
589
+ max_results: int = 5,
590
+ min_score: float = 0.1,
591
+ include_relationships: bool = False
592
+ ) -> List[Dict[str, Any]]:
593
+ """
594
+ Search for relevant episodes based on query.
595
+
596
+ Args:
597
+ query: Search query
598
+ max_results: Maximum number of results to return
599
+ min_score: Minimum relevance score threshold
600
+ include_relationships: If True, include related episode summaries in results
601
+
602
+ Returns:
603
+ List of relevant episodes with match scores
604
+ """
605
+ index = self._load_index()
606
+ if not index.get("episodes"):
607
+ return []
608
+
609
+ query_lower = query.lower()
610
+ query_words = set(query_lower.split())
611
+
612
+ scored_episodes = []
613
+
614
+ for episode_meta in index["episodes"]:
615
+ score = 0.0
616
+
617
+ # Tag matching (highest weight)
618
+ for tag in episode_meta.get("tags", []):
619
+ if tag.lower() in query_lower:
620
+ score += 0.4
621
+
622
+ # Keyword matching
623
+ episode_keywords = set(episode_meta.get("keywords", []))
624
+ common_keywords = query_words & episode_keywords
625
+ if common_keywords:
626
+ score += 0.3 * (len(common_keywords) / max(len(episode_keywords), 1))
627
+
628
+ # Title matching
629
+ title_words = set(episode_meta.get("title", "").lower().split())
630
+ common_title = query_words & title_words
631
+ if common_title:
632
+ score += 0.2 * (len(common_title) / max(len(title_words), 1))
633
+
634
+ # Type matching
635
+ if episode_meta.get("type", "") in query_lower:
636
+ score += 0.1
637
+
638
+ # P0: Boost successful episodes slightly
639
+ if episode_meta.get("success") is True:
640
+ score *= 1.1
641
+ elif episode_meta.get("success") is False:
642
+ # Don't penalize failed episodes - they're valuable for troubleshooting
643
+ pass
644
+
645
+ # Apply time decay
646
+ try:
647
+ episode_date = datetime.fromisoformat(episode_meta["timestamp"])
648
+ if episode_date.tzinfo is None:
649
+ episode_date = episode_date.replace(tzinfo=timezone.utc)
650
+ age_days = (datetime.now(timezone.utc) - episode_date).days
651
+
652
+ if age_days < 7:
653
+ time_factor = 1.0
654
+ elif age_days < 30:
655
+ time_factor = 0.9
656
+ elif age_days < 90:
657
+ time_factor = 0.7
658
+ elif age_days < 180:
659
+ time_factor = 0.5
660
+ else:
661
+ time_factor = 0.3
662
+ except:
663
+ time_factor = 0.5
664
+
665
+ final_score = score * time_factor * episode_meta.get("relevance_score", 1.0)
666
+
667
+ if final_score >= min_score:
668
+ # Load full episode if score meets threshold
669
+ full_episode = self.get_episode(episode_meta["id"])
670
+ if full_episode:
671
+ full_episode["match_score"] = final_score
672
+
673
+ # P1: Include relationship summaries if requested
674
+ if include_relationships:
675
+ relationships = self.get_related_episodes(episode_meta["id"], direction="both")
676
+ if relationships:
677
+ full_episode["related_episodes_summary"] = [
678
+ {
679
+ "id": r["episode"].get("episode_id", r["episode"].get("id")),
680
+ "title": r["episode"].get("title", "Untitled"),
681
+ "type": r["relationship_type"],
682
+ "direction": r["direction"],
683
+ "outcome": r["episode"].get("outcome")
684
+ }
685
+ for r in relationships[:5] # Limit to 5 related episodes
686
+ ]
687
+
688
+ scored_episodes.append(full_episode)
689
+
690
+ # Sort by score and return top N
691
+ scored_episodes.sort(key=lambda x: x["match_score"], reverse=True)
692
+ top_episodes = scored_episodes[:max_results]
693
+
694
+ if top_episodes:
695
+ print(f"Found {len(top_episodes)} relevant episodes from {len(index['episodes'])} total", file=sys.stderr)
696
+
697
+ return top_episodes
698
+
699
+ def get_episode(self, episode_id: str) -> Optional[Dict[str, Any]]:
700
+ """
701
+ Retrieve a specific episode by ID.
702
+
703
+ Args:
704
+ episode_id: Episode ID to retrieve
705
+
706
+ Returns:
707
+ Episode dict or None if not found
708
+ """
709
+ episode_file = self.episodes_dir / f"episode-{episode_id}.json"
710
+ if episode_file.exists():
711
+ try:
712
+ with open(episode_file, 'r') as f:
713
+ return json.load(f)
714
+ except (json.JSONDecodeError, IOError):
715
+ pass
716
+
717
+ if self.episodes_jsonl.exists():
718
+ try:
719
+ with open(self.episodes_jsonl, 'r') as f:
720
+ for line in f:
721
+ try:
722
+ episode = json.loads(line)
723
+ if episode.get("episode_id") == episode_id or episode.get("id") == episode_id:
724
+ return episode
725
+ except json.JSONDecodeError:
726
+ continue
727
+ except IOError:
728
+ pass
729
+
730
+ return None
731
+
732
+ def list_episodes(self, limit: int = 10, offset: int = 0) -> List[Dict[str, Any]]:
733
+ """
734
+ List episodes with pagination.
735
+
736
+ Args:
737
+ limit: Maximum number of episodes to return
738
+ offset: Number of episodes to skip
739
+
740
+ Returns:
741
+ List of episode metadata
742
+ """
743
+ index = self._load_index()
744
+ episodes = index.get("episodes", [])
745
+
746
+ episodes.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
747
+
748
+ return episodes[offset:offset + limit]
749
+
750
+ def delete_episode(self, episode_id: str) -> bool:
751
+ """
752
+ Delete an episode from memory.
753
+
754
+ Args:
755
+ episode_id: Episode ID to delete
756
+
757
+ Returns:
758
+ True if deleted, False if not found
759
+ """
760
+ deleted = False
761
+
762
+ episode_file = self.episodes_dir / f"episode-{episode_id}.json"
763
+ if episode_file.exists():
764
+ episode_file.unlink()
765
+ deleted = True
766
+
767
+ index = self._load_index()
768
+ original_count = len(index.get("episodes", []))
769
+ index["episodes"] = [ep for ep in index.get("episodes", [])
770
+ if ep.get("id") != episode_id]
771
+
772
+ # Also remove relationships involving this episode
773
+ index["relationships"] = [
774
+ rel for rel in index.get("relationships", [])
775
+ if rel.get("source") != episode_id and rel.get("target") != episode_id
776
+ ]
777
+
778
+ if len(index["episodes"]) < original_count:
779
+ self._save_index(index)
780
+ deleted = True
781
+
782
+ # Note: We don't remove from JSONL as it's append-only for audit trail
783
+
784
+ return deleted
785
+
786
+ def cleanup_old_episodes(self, days: int = 180) -> int:
787
+ """
788
+ Remove episodes older than specified days.
789
+
790
+ Args:
791
+ days: Age threshold in days
792
+
793
+ Returns:
794
+ Number of episodes deleted
795
+ """
796
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=days)
797
+ deleted_count = 0
798
+
799
+ index = self._load_index()
800
+ episodes_to_keep = []
801
+ deleted_ids = set()
802
+
803
+ for episode_meta in index.get("episodes", []):
804
+ try:
805
+ episode_date = datetime.fromisoformat(episode_meta["timestamp"])
806
+ if episode_date.tzinfo is None:
807
+ episode_date = episode_date.replace(tzinfo=timezone.utc)
808
+
809
+ if episode_date > cutoff_date:
810
+ episodes_to_keep.append(episode_meta)
811
+ else:
812
+ # Delete old episode file
813
+ episode_file = self.episodes_dir / f"episode-{episode_meta['id']}.json"
814
+ if episode_file.exists():
815
+ episode_file.unlink()
816
+ deleted_ids.add(episode_meta['id'])
817
+ deleted_count += 1
818
+ except:
819
+ # Keep episodes with invalid timestamps
820
+ episodes_to_keep.append(episode_meta)
821
+
822
+ if deleted_count > 0:
823
+ index["episodes"] = episodes_to_keep
824
+ # Also clean up relationships involving deleted episodes
825
+ index["relationships"] = [
826
+ rel for rel in index.get("relationships", [])
827
+ if rel.get("source") not in deleted_ids and rel.get("target") not in deleted_ids
828
+ ]
829
+ index["metadata"]["last_cleanup"] = datetime.now(timezone.utc).isoformat()
830
+ self._save_index(index)
831
+
832
+ print(f"Cleaned up {deleted_count} episodes older than {days} days", file=sys.stderr)
833
+
834
+ return deleted_count
835
+
836
+ def get_statistics(self) -> Dict[str, Any]:
837
+ """
838
+ Get statistics about the episodic memory.
839
+
840
+ Returns:
841
+ Dict with statistics including outcome and relationship stats
842
+ """
843
+ index = self._load_index()
844
+ episodes = index.get("episodes", [])
845
+
846
+ if not episodes:
847
+ return {
848
+ "total_episodes": 0,
849
+ "types": {},
850
+ "outcomes": {},
851
+ "relationships": {},
852
+ "recent_episodes": []
853
+ }
854
+
855
+ type_counts = {}
856
+ for ep in episodes:
857
+ ep_type = ep.get("type", "unknown")
858
+ type_counts[ep_type] = type_counts.get(ep_type, 0) + 1
859
+
860
+ # P0: Count by outcome
861
+ outcome_counts = {"success": 0, "partial": 0, "failed": 0, "abandoned": 0, "unknown": 0}
862
+ for ep in episodes:
863
+ outcome = ep.get("outcome", "unknown")
864
+ if outcome in outcome_counts:
865
+ outcome_counts[outcome] += 1
866
+ else:
867
+ outcome_counts["unknown"] += 1
868
+
869
+ # P1: Count relationships by type
870
+ relationship_counts = {}
871
+ for rel in index.get("relationships", []):
872
+ rel_type = rel.get("type", "unknown")
873
+ relationship_counts[rel_type] = relationship_counts.get(rel_type, 0) + 1
874
+
875
+ recent = sorted(episodes, key=lambda x: x.get("timestamp", ""), reverse=True)[:5]
876
+
877
+ ages = []
878
+ now = datetime.now(timezone.utc)
879
+ for ep in episodes:
880
+ try:
881
+ ep_date = datetime.fromisoformat(ep["timestamp"])
882
+ if ep_date.tzinfo is None:
883
+ ep_date = ep_date.replace(tzinfo=timezone.utc)
884
+ ages.append((now - ep_date).days)
885
+ except:
886
+ pass
887
+
888
+ stats = {
889
+ "total_episodes": len(episodes),
890
+ "types": type_counts,
891
+ "outcomes": outcome_counts,
892
+ "total_relationships": len(index.get("relationships", [])),
893
+ "relationship_types": relationship_counts,
894
+ "recent_episodes": recent,
895
+ "storage_size_mb": self._calculate_storage_size() / (1024 * 1024),
896
+ "index_size_kb": self.index_file.stat().st_size / 1024 if self.index_file.exists() else 0
897
+ }
898
+
899
+ if ages:
900
+ stats["age_stats"] = {
901
+ "newest_days": min(ages),
902
+ "oldest_days": max(ages),
903
+ "average_days": sum(ages) / len(ages)
904
+ }
905
+
906
+ return stats
907
+
908
+ def capture_git_state(self, repo_path: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
909
+ """
910
+ Capture current git state as part of episode context.
911
+
912
+ Migrated from session system to provide git context for episodes.
913
+
914
+ Args:
915
+ repo_path: Path to git repository. Defaults to current working directory.
916
+
917
+ Returns:
918
+ Dict with git state including:
919
+ - branch: Current branch name
920
+ - commit: Current commit hash
921
+ - status: List of modified files
922
+ - recent_commits: Last 5 commits (hash, message, timestamp)
923
+ """
924
+ import subprocess
925
+
926
+ repo_path = Path(repo_path) if repo_path else Path.cwd()
927
+ git_state = {
928
+ "branch": None,
929
+ "commit": None,
930
+ "status": [],
931
+ "recent_commits": [],
932
+ "is_git_repo": False
933
+ }
934
+
935
+ try:
936
+ # Check if it is a git repo
937
+ result = subprocess.run(
938
+ ["git", "rev-parse", "--git-dir"],
939
+ cwd=repo_path,
940
+ capture_output=True,
941
+ text=True,
942
+ timeout=5
943
+ )
944
+ if result.returncode != 0:
945
+ return git_state
946
+
947
+ git_state["is_git_repo"] = True
948
+
949
+ # Get current branch
950
+ result = subprocess.run(
951
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
952
+ cwd=repo_path,
953
+ capture_output=True,
954
+ text=True,
955
+ timeout=5
956
+ )
957
+ if result.returncode == 0:
958
+ git_state["branch"] = result.stdout.strip()
959
+
960
+ # Get current commit
961
+ result = subprocess.run(
962
+ ["git", "rev-parse", "HEAD"],
963
+ cwd=repo_path,
964
+ capture_output=True,
965
+ text=True,
966
+ timeout=5
967
+ )
968
+ if result.returncode == 0:
969
+ git_state["commit"] = result.stdout.strip()[:12]
970
+
971
+ # Get status (modified files)
972
+ result = subprocess.run(
973
+ ["git", "status", "--porcelain"],
974
+ cwd=repo_path,
975
+ capture_output=True,
976
+ text=True,
977
+ timeout=10
978
+ )
979
+ if result.returncode == 0 and result.stdout.strip():
980
+ git_state["status"] = result.stdout.strip().split("\n")
981
+
982
+ # Get recent commits
983
+ result = subprocess.run(
984
+ ["git", "log", "--oneline", "-5", "--pretty=format:%H|%s|%ai"],
985
+ cwd=repo_path,
986
+ capture_output=True,
987
+ text=True,
988
+ timeout=10
989
+ )
990
+ if result.returncode == 0 and result.stdout.strip():
991
+ for line in result.stdout.strip().split("\n"):
992
+ if line and "|" in line:
993
+ parts = line.split("|")
994
+ if len(parts) >= 3:
995
+ git_state["recent_commits"].append({
996
+ "hash": parts[0][:12],
997
+ "message": parts[1],
998
+ "timestamp": parts[2]
999
+ })
1000
+
1001
+ except subprocess.TimeoutExpired:
1002
+ print("Warning: Git command timed out", file=sys.stderr)
1003
+ except Exception as e:
1004
+ print(f"Warning: Could not capture git state: {e}", file=sys.stderr)
1005
+
1006
+ return git_state
1007
+
1008
+ def _calculate_storage_size(self) -> float:
1009
+ """Calculate total storage size used by episodic memory."""
1010
+ total_size = 0
1011
+
1012
+ if self.index_file.exists():
1013
+ total_size += self.index_file.stat().st_size
1014
+
1015
+ if self.episodes_jsonl.exists():
1016
+ total_size += self.episodes_jsonl.stat().st_size
1017
+
1018
+ if self.episodes_dir.exists():
1019
+ for episode_file in self.episodes_dir.glob("episode-*.json"):
1020
+ total_size += episode_file.stat().st_size
1021
+
1022
+ return total_size
1023
+
1024
+
1025
+ # Compatibility function for direct use in workflow.py
1026
+ def search_episodic_memory(user_prompt: str, max_results: int = 3) -> List[Dict[str, Any]]:
1027
+ """
1028
+ Compatibility function for workflow.py integration.
1029
+
1030
+ This function can be imported and used directly without instantiating EpisodicMemory.
1031
+
1032
+ Args:
1033
+ user_prompt: User's request to search for
1034
+ max_results: Maximum episodes to return
1035
+
1036
+ Returns:
1037
+ List of relevant episodes with match scores
1038
+ """
1039
+ try:
1040
+ memory = EpisodicMemory()
1041
+ return memory.search_episodes(user_prompt, max_results)
1042
+ except Exception as e:
1043
+ print(f"Warning: Could not search episodic memory: {e}", file=sys.stderr)
1044
+ return []
1045
+
1046
+
1047
+ # CLI interface for testing and management
1048
+ if __name__ == "__main__":
1049
+ import argparse
1050
+
1051
+ parser = argparse.ArgumentParser(description="Episodic Memory Management")
1052
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
1053
+
1054
+ # Store command
1055
+ store_parser = subparsers.add_parser("store", help="Store a new episode")
1056
+ store_parser.add_argument("prompt", help="User prompt")
1057
+ store_parser.add_argument("--enriched", help="Enriched prompt")
1058
+ store_parser.add_argument("--tags", nargs="+", help="Tags")
1059
+ store_parser.add_argument("--outcome", choices=["success", "partial", "failed", "abandoned"], help="Episode outcome")
1060
+ store_parser.add_argument("--duration", type=float, help="Duration in seconds")
1061
+
1062
+ # Search command
1063
+ search_parser = subparsers.add_parser("search", help="Search episodes")
1064
+ search_parser.add_argument("query", help="Search query")
1065
+ search_parser.add_argument("--limit", type=int, default=5, help="Max results")
1066
+ search_parser.add_argument("--include-relationships", action="store_true", help="Include related episodes")
1067
+
1068
+ # List command
1069
+ list_parser = subparsers.add_parser("list", help="List recent episodes")
1070
+ list_parser.add_argument("--limit", type=int, default=10, help="Number to show")
1071
+
1072
+ # Stats command
1073
+ stats_parser = subparsers.add_parser("stats", help="Show statistics")
1074
+
1075
+ # Cleanup command
1076
+ cleanup_parser = subparsers.add_parser("cleanup", help="Clean old episodes")
1077
+ cleanup_parser.add_argument("--days", type=int, default=180, help="Days to keep")
1078
+
1079
+ # Update outcome command
1080
+ outcome_parser = subparsers.add_parser("update-outcome", help="Update episode outcome")
1081
+ outcome_parser.add_argument("episode_id", help="Episode ID")
1082
+ outcome_parser.add_argument("outcome", choices=["success", "partial", "failed", "abandoned"], help="Outcome")
1083
+ outcome_parser.add_argument("--duration", type=float, help="Duration in seconds")
1084
+
1085
+ # Add relationship command
1086
+ rel_parser = subparsers.add_parser("add-relationship", help="Add relationship between episodes")
1087
+ rel_parser.add_argument("source", help="Source episode ID")
1088
+ rel_parser.add_argument("target", help="Target episode ID")
1089
+ rel_parser.add_argument("type", choices=list(RELATIONSHIP_TYPES), help="Relationship type")
1090
+
1091
+ # Get related command
1092
+ related_parser = subparsers.add_parser("get-related", help="Get related episodes")
1093
+ related_parser.add_argument("episode_id", help="Episode ID")
1094
+ related_parser.add_argument("--type", help="Filter by relationship type")
1095
+ related_parser.add_argument("--direction", choices=["outgoing", "incoming", "both"], default="both", help="Direction")
1096
+
1097
+ args = parser.parse_args()
1098
+
1099
+ memory = EpisodicMemory()
1100
+
1101
+ if args.command == "store":
1102
+ episode_id = memory.store_episode(
1103
+ prompt=args.prompt,
1104
+ enriched_prompt=args.enriched,
1105
+ tags=args.tags,
1106
+ outcome=args.outcome,
1107
+ success=args.outcome == "success" if args.outcome else None,
1108
+ duration_seconds=args.duration
1109
+ )
1110
+ print(f"Stored episode: {episode_id}")
1111
+
1112
+ elif args.command == "search":
1113
+ episodes = memory.search_episodes(
1114
+ args.query,
1115
+ max_results=args.limit,
1116
+ include_relationships=args.include_relationships
1117
+ )
1118
+ for i, ep in enumerate(episodes, 1):
1119
+ print(f"\n{i}. [{ep.get('match_score', 0):.2f}] {ep.get('title', 'Untitled')}")
1120
+ print(f" ID: {ep.get('episode_id', ep.get('id'))}")
1121
+ print(f" Type: {ep.get('type', 'unknown')}")
1122
+ print(f" Outcome: {ep.get('outcome', 'unknown')}")
1123
+ print(f" Timestamp: {ep.get('timestamp', 'unknown')}")
1124
+ if ep.get('related_episodes_summary'):
1125
+ print(f" Related: {len(ep['related_episodes_summary'])} episodes")
1126
+
1127
+ elif args.command == "list":
1128
+ episodes = memory.list_episodes(limit=args.limit)
1129
+ for i, ep in enumerate(episodes, 1):
1130
+ print(f"\n{i}. {ep.get('title', 'Untitled')}")
1131
+ print(f" ID: {ep.get('id')}")
1132
+ print(f" Type: {ep.get('type', 'unknown')}")
1133
+ print(f" Outcome: {ep.get('outcome', 'unknown')}")
1134
+ print(f" Timestamp: {ep.get('timestamp', 'unknown')}")
1135
+
1136
+ elif args.command == "stats":
1137
+ stats = memory.get_statistics()
1138
+ print(f"\nEpisodic Memory Statistics:")
1139
+ print(f" Total episodes: {stats['total_episodes']}")
1140
+ print(f" Storage size: {stats['storage_size_mb']:.2f} MB")
1141
+ print(f" Index size: {stats['index_size_kb']:.2f} KB")
1142
+
1143
+ if stats.get("types"):
1144
+ print(f"\n Episode types:")
1145
+ for ep_type, count in stats["types"].items():
1146
+ print(f" {ep_type}: {count}")
1147
+
1148
+ if stats.get("outcomes"):
1149
+ print(f"\n Outcomes:")
1150
+ for outcome, count in stats["outcomes"].items():
1151
+ if count > 0:
1152
+ print(f" {outcome}: {count}")
1153
+
1154
+ if stats.get("total_relationships"):
1155
+ print(f"\n Relationships: {stats['total_relationships']} total")
1156
+ for rel_type, count in stats.get("relationship_types", {}).items():
1157
+ print(f" {rel_type}: {count}")
1158
+
1159
+ if stats.get("age_stats"):
1160
+ print(f"\n Age statistics:")
1161
+ print(f" Newest: {stats['age_stats']['newest_days']} days")
1162
+ print(f" Oldest: {stats['age_stats']['oldest_days']} days")
1163
+ print(f" Average: {stats['age_stats']['average_days']:.1f} days")
1164
+
1165
+ elif args.command == "cleanup":
1166
+ count = memory.cleanup_old_episodes(days=args.days)
1167
+ print(f"Cleaned up {count} episodes older than {args.days} days")
1168
+
1169
+ elif args.command == "update-outcome":
1170
+ success = memory.update_outcome(
1171
+ episode_id=args.episode_id,
1172
+ outcome=args.outcome,
1173
+ success=args.outcome == "success",
1174
+ duration_seconds=args.duration
1175
+ )
1176
+ if success:
1177
+ print(f"Updated outcome for {args.episode_id}")
1178
+ else:
1179
+ print(f"Failed to update outcome")
1180
+
1181
+ elif args.command == "add-relationship":
1182
+ success = memory.add_relationship(
1183
+ source_episode_id=args.source,
1184
+ target_episode_id=args.target,
1185
+ relationship_type=args.type
1186
+ )
1187
+ if success:
1188
+ print(f"Added relationship: {args.source} --{args.type}--> {args.target}")
1189
+ else:
1190
+ print(f"Failed to add relationship")
1191
+
1192
+ elif args.command == "get-related":
1193
+ related = memory.get_related_episodes(
1194
+ episode_id=args.episode_id,
1195
+ relationship_type=args.type,
1196
+ direction=args.direction
1197
+ )
1198
+ if related:
1199
+ print(f"\nRelated episodes for {args.episode_id}:")
1200
+ for rel in related:
1201
+ ep = rel["episode"]
1202
+ print(f"\n --{rel['relationship_type']}--> ({rel['direction']})")
1203
+ print(f" ID: {ep.get('episode_id', ep.get('id'))}")
1204
+ print(f" Title: {ep.get('title', 'Untitled')}")
1205
+ print(f" Outcome: {ep.get('outcome', 'unknown')}")
1206
+ else:
1207
+ print(f"No related episodes found for {args.episode_id}")
1208
+
1209
+ else:
1210
+ parser.print_help()