@jaguilar87/gaia-ops 4.4.0 → 4.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +12 -3
  3. package/ARCHITECTURE.md +9 -8
  4. package/CHANGELOG.md +34 -0
  5. package/README.md +43 -11
  6. package/agents/terraform-architect.md +1 -1
  7. package/bin/README.md +2 -2
  8. package/bin/gaia-doctor.js +18 -5
  9. package/bin/gaia-history.js +0 -1
  10. package/bin/gaia-metrics.js +2 -2
  11. package/bin/gaia-scan.py +23 -1
  12. package/bin/gaia-update.js +346 -54
  13. package/bin/pre-publish-validate.js +33 -10
  14. package/commands/gaia.md +37 -0
  15. package/config/README.md +3 -9
  16. package/config/context-contracts.json +47 -15
  17. package/config/surface-routing.json +9 -1
  18. package/dist/gaia-ops/.claude-plugin/plugin.json +22 -0
  19. package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
  20. package/dist/gaia-ops/agents/devops-developer.md +57 -0
  21. package/dist/gaia-ops/agents/gaia-system.md +58 -0
  22. package/dist/gaia-ops/agents/gitops-operator.md +60 -0
  23. package/dist/gaia-ops/agents/speckit-planner.md +71 -0
  24. package/dist/gaia-ops/agents/terraform-architect.md +60 -0
  25. package/dist/gaia-ops/commands/gaia.md +37 -0
  26. package/dist/gaia-ops/config/README.md +58 -0
  27. package/dist/gaia-ops/config/cloud/aws.json +140 -0
  28. package/dist/gaia-ops/config/cloud/gcp.json +145 -0
  29. package/dist/gaia-ops/config/context-contracts.json +131 -0
  30. package/dist/gaia-ops/config/git_standards.json +72 -0
  31. package/dist/gaia-ops/config/surface-routing.json +197 -0
  32. package/dist/gaia-ops/config/universal-rules.json +10 -0
  33. package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
  34. package/dist/gaia-ops/hooks/adapters/base.py +219 -0
  35. package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
  36. package/dist/gaia-ops/hooks/adapters/claude_code.py +1477 -0
  37. package/dist/gaia-ops/hooks/adapters/types.py +194 -0
  38. package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
  39. package/dist/gaia-ops/hooks/hooks.json +126 -0
  40. package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
  41. package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
  42. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
  43. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
  44. package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +124 -0
  45. package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
  46. package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
  47. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
  48. package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
  49. package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
  50. package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
  51. package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
  52. package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +576 -0
  53. package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
  54. package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
  55. package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
  56. package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +215 -0
  57. package/dist/gaia-ops/hooks/modules/context/context_cache.py +129 -0
  58. package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
  59. package/dist/gaia-ops/hooks/modules/context/context_injector.py +427 -0
  60. package/dist/gaia-ops/hooks/modules/context/context_writer.py +518 -0
  61. package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
  62. package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
  63. package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
  64. package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
  65. package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
  66. package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +558 -0
  67. package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
  68. package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
  69. package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
  70. package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
  71. package/dist/gaia-ops/hooks/modules/identity/__init__.py +0 -0
  72. package/dist/gaia-ops/hooks/modules/identity/identity_provider.py +21 -0
  73. package/dist/gaia-ops/hooks/modules/identity/ops_identity.py +34 -0
  74. package/dist/gaia-ops/hooks/modules/identity/security_identity.py +10 -0
  75. package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
  76. package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +227 -0
  77. package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
  78. package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +128 -0
  79. package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
  80. package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
  81. package/dist/gaia-ops/hooks/modules/security/__init__.py +89 -0
  82. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
  83. package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
  84. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +912 -0
  85. package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
  86. package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +153 -0
  87. package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +584 -0
  88. package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +86 -0
  89. package/dist/gaia-ops/hooks/modules/security/command_semantics.py +130 -0
  90. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
  91. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +850 -0
  92. package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
  93. package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
  94. package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
  95. package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
  96. package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +158 -0
  97. package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
  98. package/dist/gaia-ops/hooks/modules/tools/__init__.py +25 -0
  99. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +708 -0
  100. package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +181 -0
  101. package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
  102. package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
  103. package/dist/gaia-ops/hooks/modules/tools/task_validator.py +283 -0
  104. package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
  105. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
  106. package/dist/gaia-ops/hooks/post_compact.py +43 -0
  107. package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
  108. package/dist/gaia-ops/hooks/pre_tool_use.py +383 -0
  109. package/dist/gaia-ops/hooks/session_start.py +69 -0
  110. package/dist/gaia-ops/hooks/stop_hook.py +69 -0
  111. package/dist/gaia-ops/hooks/subagent_start.py +71 -0
  112. package/dist/gaia-ops/hooks/subagent_stop.py +288 -0
  113. package/dist/gaia-ops/hooks/task_completed.py +70 -0
  114. package/dist/gaia-ops/hooks/user_prompt_submit.py +177 -0
  115. package/dist/gaia-ops/settings.json +72 -0
  116. package/dist/gaia-ops/skills/README.md +109 -0
  117. package/dist/gaia-ops/skills/agent-protocol/SKILL.md +105 -0
  118. package/dist/gaia-ops/skills/agent-protocol/examples.md +170 -0
  119. package/dist/gaia-ops/skills/agent-response/SKILL.md +53 -0
  120. package/dist/gaia-ops/skills/approval/SKILL.md +85 -0
  121. package/dist/gaia-ops/skills/approval/examples.md +140 -0
  122. package/dist/gaia-ops/skills/approval/reference.md +57 -0
  123. package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
  124. package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
  125. package/dist/gaia-ops/skills/context-updater/SKILL.md +76 -0
  126. package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
  127. package/dist/gaia-ops/skills/developer-patterns/SKILL.md +93 -0
  128. package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
  129. package/dist/gaia-ops/skills/execution/SKILL.md +66 -0
  130. package/dist/gaia-ops/skills/fast-queries/SKILL.md +47 -0
  131. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +92 -0
  132. package/dist/gaia-ops/skills/gaia-patterns/reference.md +22 -0
  133. package/dist/gaia-ops/skills/git-conventions/SKILL.md +48 -0
  134. package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +73 -0
  135. package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
  136. package/dist/gaia-ops/skills/investigation/SKILL.md +77 -0
  137. package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +64 -0
  138. package/dist/gaia-ops/skills/reference.md +134 -0
  139. package/dist/gaia-ops/skills/security-tiers/SKILL.md +61 -0
  140. package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
  141. package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
  142. package/dist/gaia-ops/skills/skill-creation/SKILL.md +119 -0
  143. package/dist/gaia-ops/skills/specification/SKILL.md +186 -0
  144. package/dist/gaia-ops/skills/speckit-workflow/SKILL.md +165 -0
  145. package/dist/gaia-ops/skills/speckit-workflow/reference.md +117 -0
  146. package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +63 -0
  147. package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
  148. package/dist/gaia-ops/speckit/README.md +516 -0
  149. package/dist/gaia-ops/speckit/scripts/.gitkeep +0 -0
  150. package/dist/gaia-ops/speckit/templates/adr-template.md +118 -0
  151. package/dist/gaia-ops/speckit/templates/agent-file-template.md +23 -0
  152. package/dist/gaia-ops/speckit/templates/plan-template.md +227 -0
  153. package/dist/gaia-ops/speckit/templates/spec-template.md +140 -0
  154. package/dist/gaia-ops/speckit/templates/tasks-template.md +257 -0
  155. package/dist/gaia-ops/tools/context/README.md +132 -0
  156. package/dist/gaia-ops/tools/context/__init__.py +42 -0
  157. package/dist/gaia-ops/tools/context/_paths.py +20 -0
  158. package/dist/gaia-ops/tools/context/context_provider.py +476 -0
  159. package/dist/gaia-ops/tools/context/context_section_reader.py +330 -0
  160. package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
  161. package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
  162. package/dist/gaia-ops/tools/context/surface_router.py +278 -0
  163. package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
  164. package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
  165. package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  166. package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  167. package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  168. package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  169. package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
  170. package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  171. package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
  172. package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
  173. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
  174. package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
  175. package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
  176. package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
  177. package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +262 -0
  178. package/dist/gaia-ops/tools/memory/README.md +0 -0
  179. package/dist/gaia-ops/tools/memory/__init__.py +20 -0
  180. package/dist/gaia-ops/tools/memory/episodic.py +1196 -0
  181. package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
  182. package/dist/gaia-ops/tools/review/__init__.py +1 -0
  183. package/dist/gaia-ops/tools/review/review_engine.py +157 -0
  184. package/dist/gaia-ops/tools/scan/__init__.py +35 -0
  185. package/dist/gaia-ops/tools/scan/config.py +247 -0
  186. package/dist/gaia-ops/tools/scan/merge.py +212 -0
  187. package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
  188. package/dist/gaia-ops/tools/scan/registry.py +127 -0
  189. package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
  190. package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
  191. package/dist/gaia-ops/tools/scan/scanners/environment.py +324 -0
  192. package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
  193. package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
  194. package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
  195. package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
  196. package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
  197. package/dist/gaia-ops/tools/scan/setup.py +753 -0
  198. package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
  199. package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
  200. package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
  201. package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
  202. package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
  203. package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
  204. package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
  205. package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
  206. package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
  207. package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
  208. package/dist/gaia-ops/tools/scan/ui.py +624 -0
  209. package/dist/gaia-ops/tools/scan/verify.py +266 -0
  210. package/dist/gaia-ops/tools/scan/walk.py +118 -0
  211. package/dist/gaia-ops/tools/scan/workspace.py +85 -0
  212. package/dist/gaia-ops/tools/validation/README.md +244 -0
  213. package/dist/gaia-ops/tools/validation/__init__.py +17 -0
  214. package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
  215. package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
  216. package/dist/gaia-security/.claude-plugin/plugin.json +22 -0
  217. package/dist/gaia-security/config/universal-rules.json +10 -0
  218. package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
  219. package/dist/gaia-security/hooks/adapters/base.py +219 -0
  220. package/dist/gaia-security/hooks/adapters/channel.py +17 -0
  221. package/dist/gaia-security/hooks/adapters/claude_code.py +1477 -0
  222. package/dist/gaia-security/hooks/adapters/types.py +194 -0
  223. package/dist/gaia-security/hooks/adapters/utils.py +25 -0
  224. package/dist/gaia-security/hooks/hooks.json +57 -0
  225. package/dist/gaia-security/hooks/modules/__init__.py +15 -0
  226. package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
  227. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
  228. package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
  229. package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +124 -0
  230. package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
  231. package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
  232. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
  233. package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
  234. package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
  235. package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
  236. package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
  237. package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +576 -0
  238. package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
  239. package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
  240. package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
  241. package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +215 -0
  242. package/dist/gaia-security/hooks/modules/context/context_cache.py +129 -0
  243. package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
  244. package/dist/gaia-security/hooks/modules/context/context_injector.py +427 -0
  245. package/dist/gaia-security/hooks/modules/context/context_writer.py +518 -0
  246. package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
  247. package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
  248. package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
  249. package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
  250. package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
  251. package/dist/gaia-security/hooks/modules/core/plugin_setup.py +558 -0
  252. package/dist/gaia-security/hooks/modules/core/state.py +179 -0
  253. package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
  254. package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
  255. package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
  256. package/dist/gaia-security/hooks/modules/identity/__init__.py +0 -0
  257. package/dist/gaia-security/hooks/modules/identity/identity_provider.py +21 -0
  258. package/dist/gaia-security/hooks/modules/identity/ops_identity.py +34 -0
  259. package/dist/gaia-security/hooks/modules/identity/security_identity.py +10 -0
  260. package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
  261. package/dist/gaia-security/hooks/modules/memory/episode_writer.py +227 -0
  262. package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
  263. package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +128 -0
  264. package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
  265. package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
  266. package/dist/gaia-security/hooks/modules/security/__init__.py +89 -0
  267. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
  268. package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
  269. package/dist/gaia-security/hooks/modules/security/approval_grants.py +912 -0
  270. package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
  271. package/dist/gaia-security/hooks/modules/security/approval_scopes.py +153 -0
  272. package/dist/gaia-security/hooks/modules/security/blocked_commands.py +584 -0
  273. package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +86 -0
  274. package/dist/gaia-security/hooks/modules/security/command_semantics.py +130 -0
  275. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
  276. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +850 -0
  277. package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
  278. package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
  279. package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
  280. package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
  281. package/dist/gaia-security/hooks/modules/session/session_event_injector.py +158 -0
  282. package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
  283. package/dist/gaia-security/hooks/modules/tools/__init__.py +25 -0
  284. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +708 -0
  285. package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +181 -0
  286. package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
  287. package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
  288. package/dist/gaia-security/hooks/modules/tools/task_validator.py +283 -0
  289. package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
  290. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
  291. package/dist/gaia-security/hooks/post_tool_use.py +54 -0
  292. package/dist/gaia-security/hooks/pre_tool_use.py +383 -0
  293. package/dist/gaia-security/hooks/session_start.py +69 -0
  294. package/dist/gaia-security/hooks/stop_hook.py +69 -0
  295. package/dist/gaia-security/hooks/user_prompt_submit.py +177 -0
  296. package/dist/gaia-security/settings.json +58 -0
  297. package/git-hooks/commit-msg +41 -0
  298. package/hooks/README.md +8 -6
  299. package/hooks/adapters/channel.py +0 -25
  300. package/hooks/adapters/claude_code.py +364 -125
  301. package/hooks/elicitation_result.py +132 -0
  302. package/hooks/hooks.json +10 -1
  303. package/hooks/modules/README.md +3 -2
  304. package/hooks/modules/agents/contract_validator.py +3 -51
  305. package/hooks/modules/agents/response_contract.py +4 -8
  306. package/hooks/modules/agents/transcript_reader.py +4 -5
  307. package/hooks/modules/audit/__init__.py +4 -6
  308. package/hooks/modules/audit/event_detector.py +0 -2
  309. package/hooks/modules/audit/metrics.py +108 -187
  310. package/hooks/modules/audit/workflow_auditor.py +0 -4
  311. package/hooks/modules/audit/workflow_recorder.py +0 -5
  312. package/hooks/modules/context/compact_context_builder.py +1 -0
  313. package/hooks/modules/context/context_cache.py +129 -0
  314. package/hooks/modules/context/context_injector.py +18 -40
  315. package/hooks/modules/context/context_writer.py +1 -25
  316. package/hooks/modules/context/contracts_loader.py +7 -10
  317. package/hooks/modules/core/hook_entry.py +1 -0
  318. package/hooks/modules/core/paths.py +12 -13
  319. package/hooks/modules/core/plugin_mode.py +74 -4
  320. package/hooks/modules/core/plugin_setup.py +395 -23
  321. package/hooks/modules/events/__init__.py +1 -0
  322. package/hooks/modules/events/event_writer.py +210 -0
  323. package/hooks/modules/identity/ops_identity.py +18 -27
  324. package/hooks/modules/memory/episode_writer.py +1 -6
  325. package/hooks/modules/orchestrator/__init__.py +1 -0
  326. package/hooks/modules/orchestrator/delegate_mode.py +128 -0
  327. package/hooks/modules/security/__init__.py +2 -4
  328. package/hooks/modules/security/approval_constants.py +5 -1
  329. package/hooks/modules/security/approval_grants.py +189 -6
  330. package/hooks/modules/security/approval_messages.py +9 -21
  331. package/hooks/modules/security/blocked_commands.py +98 -34
  332. package/hooks/modules/security/command_semantics.py +0 -4
  333. package/hooks/modules/security/gitops_validator.py +1 -11
  334. package/hooks/modules/security/mutative_verbs.py +179 -38
  335. package/hooks/modules/security/tiers.py +1 -19
  336. package/hooks/modules/session/session_event_injector.py +1 -25
  337. package/hooks/modules/tools/bash_validator.py +310 -94
  338. package/hooks/modules/tools/shell_parser.py +0 -1
  339. package/hooks/modules/tools/task_validator.py +9 -29
  340. package/hooks/post_tool_use.py +0 -72
  341. package/hooks/pre_tool_use.py +42 -102
  342. package/hooks/session_start.py +4 -2
  343. package/hooks/subagent_start.py +6 -2
  344. package/hooks/subagent_stop.py +1 -13
  345. package/hooks/user_prompt_submit.py +119 -37
  346. package/index.js +1 -1
  347. package/package.json +5 -3
  348. package/skills/README.md +3 -5
  349. package/skills/agent-protocol/SKILL.md +17 -16
  350. package/skills/agent-protocol/examples.md +6 -6
  351. package/skills/agent-response/SKILL.md +11 -14
  352. package/skills/approval/SKILL.md +28 -13
  353. package/skills/approval/reference.md +2 -2
  354. package/skills/execution/SKILL.md +1 -1
  355. package/skills/gaia-patterns/SKILL.md +2 -3
  356. package/skills/orchestrator-approval/SKILL.md +22 -50
  357. package/skills/security-tiers/SKILL.md +1 -1
  358. package/templates/README.md +9 -9
  359. package/templates/managed-settings.template.json +43 -0
  360. package/tools/gaia_simulator/runner.py +34 -1
  361. package/tools/scan/orchestrator.py +13 -0
  362. package/tools/scan/scanners/base.py +8 -0
  363. package/tools/scan/scanners/git.py +78 -0
  364. package/tools/scan/scanners/infrastructure.py +65 -0
  365. package/tools/scan/scanners/stack.py +110 -0
  366. package/tools/scan/setup.py +120 -13
  367. package/tools/scan/workspace.py +85 -0
  368. package/config/context-contracts.aws.json +0 -42
  369. package/config/context-contracts.gcp.json +0 -39
  370. package/skills/project-dispatch/SKILL.md +0 -34
  371. package/templates/settings.template.json +0 -226
@@ -0,0 +1,296 @@
1
+ """
2
+ Workflow metrics capture and persistence.
3
+
4
+ Renamed from metrics_recorder.py for clarity.
5
+
6
+ Provides:
7
+ - get_workflow_memory_dir(): Resolve workflow memory directory
8
+ - record(): Build metrics dict, write to JSONL
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ import os
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ from ..context.context_injector import build_context_telemetry_snapshot
19
+ from ..core.paths import get_plugin_data_dir
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def get_workflow_memory_dir() -> Path:
25
+ """
26
+ Get workflow memory directory path.
27
+
28
+ Resolution order:
29
+ 1. WORKFLOW_MEMORY_BASE_PATH env var (testing override)
30
+ 2. CLAUDE_PLUGIN_DATA / project-context / workflow-episodic-memory
31
+ 3. .claude/project-context/workflow-episodic-memory (backward compat)
32
+ """
33
+ base_path = os.environ.get("WORKFLOW_MEMORY_BASE_PATH")
34
+ if base_path:
35
+ return Path(base_path) / "project-context" / "workflow-episodic-memory"
36
+ return get_plugin_data_dir() / "project-context" / "workflow-episodic-memory"
37
+
38
+
39
+ def _append_jsonl(path: Path, payload: Dict[str, Any]) -> None:
40
+ """Append one JSON record per line."""
41
+ path.parent.mkdir(parents=True, exist_ok=True)
42
+ with open(path, "a") as f:
43
+ f.write(json.dumps(payload) + "\n")
44
+
45
+
46
+ def _parse_frontmatter(text: str) -> Dict[str, Any]:
47
+ """Parse simple markdown frontmatter without external dependencies."""
48
+ if not text.startswith("---"):
49
+ return {}
50
+
51
+ try:
52
+ end = text.index("---", 3)
53
+ except ValueError:
54
+ return {}
55
+
56
+ frontmatter = text[3:end]
57
+ result: Dict[str, Any] = {}
58
+ current_key: Optional[str] = None
59
+ current_list: Optional[List[str]] = None
60
+
61
+ for line in frontmatter.splitlines():
62
+ stripped = line.strip()
63
+ if not stripped or stripped.startswith("#"):
64
+ continue
65
+
66
+ if stripped.startswith("- ") and current_key and current_list is not None:
67
+ current_list.append(stripped[2:].strip())
68
+ continue
69
+
70
+ if ":" in stripped:
71
+ if current_key and current_list is not None:
72
+ result[current_key] = current_list
73
+
74
+ key, _, value = stripped.partition(":")
75
+ key = key.strip()
76
+ value = value.strip()
77
+
78
+ if value:
79
+ result[key] = value
80
+ current_key = key
81
+ current_list = None
82
+ else:
83
+ current_key = key
84
+ current_list = []
85
+
86
+ if current_key and current_list is not None:
87
+ result[current_key] = current_list
88
+
89
+ return result
90
+
91
+
92
+ def _parse_tools_field(value: Any) -> List[str]:
93
+ """Normalize frontmatter tools into a list."""
94
+ if isinstance(value, list):
95
+ return [item for item in value if isinstance(item, str) and item.strip()]
96
+ if isinstance(value, str):
97
+ return [item.strip() for item in value.split(",") if item.strip()]
98
+ return []
99
+
100
+
101
+ def _resolve_agent_file(agent_type: str) -> Optional[Path]:
102
+ """Resolve the markdown definition for a project agent."""
103
+ if not agent_type:
104
+ return None
105
+
106
+ candidates = [
107
+ Path(".claude/agents") / f"{agent_type}.md",
108
+ Path(__file__).resolve().parents[3] / "agents" / f"{agent_type}.md",
109
+ ]
110
+ for candidate in candidates:
111
+ if candidate.exists():
112
+ return candidate
113
+ return None
114
+
115
+
116
+ def load_agent_runtime_profile(agent_type: str) -> Dict[str, Any]:
117
+ """Load runtime-default metadata from an agent definition file."""
118
+ agent_file = _resolve_agent_file(agent_type)
119
+ if not agent_file:
120
+ return {
121
+ "agent": agent_type or "unknown",
122
+ "model": "",
123
+ "tools": [],
124
+ "skills": [],
125
+ "skills_count": 0,
126
+ }
127
+
128
+ frontmatter = _parse_frontmatter(agent_file.read_text())
129
+ skills = frontmatter.get("skills", [])
130
+ if not isinstance(skills, list):
131
+ skills = []
132
+
133
+ return {
134
+ "agent": agent_type or "unknown",
135
+ "model": frontmatter.get("model", ""),
136
+ "tools": _parse_tools_field(frontmatter.get("tools", [])),
137
+ "skills": skills,
138
+ "skills_count": len(skills),
139
+ }
140
+
141
+
142
+ def record_agent_skill_snapshot(
143
+ agent_type: str,
144
+ session_context: Optional[Dict[str, Any]] = None,
145
+ task_description: str = "",
146
+ ) -> Dict[str, Any]:
147
+ """Persist a historical snapshot of an agent's runtime defaults."""
148
+ session_context = session_context or {}
149
+ profile = load_agent_runtime_profile(agent_type)
150
+ snapshot = {
151
+ "timestamp": session_context.get("timestamp", datetime.now().isoformat()),
152
+ "session_id": session_context.get("session_id", ""),
153
+ "agent": profile.get("agent", agent_type or "unknown"),
154
+ "task_description": task_description[:200],
155
+ "model": profile.get("model", ""),
156
+ "tools": profile.get("tools", []),
157
+ "skills": profile.get("skills", []),
158
+ "skills_count": profile.get("skills_count", 0),
159
+ }
160
+
161
+ try:
162
+ _append_jsonl(get_workflow_memory_dir() / "agent-skills.jsonl", snapshot)
163
+ except Exception as exc:
164
+ logger.debug("Could not persist agent skill snapshot: %s", exc)
165
+
166
+ return snapshot
167
+
168
+
169
+ def record(
170
+ task_info: Dict[str, Any],
171
+ agent_output: str,
172
+ session_context: Dict[str, Any],
173
+ commands_executed: Optional[List[str]] = None,
174
+ context_update_result: Optional[Dict[str, Any]] = None,
175
+ anchor_hits: Optional[Dict[str, Any]] = None,
176
+ transcript_analysis: Optional[Any] = None,
177
+ compliance_result: Optional[Any] = None,
178
+ ) -> Dict[str, Any]:
179
+ """
180
+ Capture workflow execution metrics for analysis.
181
+
182
+ Args:
183
+ task_info: Task metadata
184
+ agent_output: Output from agent execution
185
+ session_context: Current session context
186
+ commands_executed: List of commands the agent executed.
187
+ context_update_result: Result of context update processing.
188
+ anchor_hits: Context anchor hit data.
189
+ transcript_analysis: Optional TranscriptAnalysis from transcript_analyzer.
190
+ When provided, real token counts and tool metrics are added to the
191
+ metrics dict alongside the existing output_tokens_approx.
192
+ compliance_result: Optional ComplianceScore from transcript_analyzer.
193
+ When provided, compliance_score and compliance_grade are added.
194
+
195
+ Returns:
196
+ Dict with duration, exit_code, agent, tier, etc.
197
+ """
198
+ # Duration cannot be reliably measured from within this hook because
199
+ # it fires only at agent completion (no start timestamp available).
200
+ duration_ms = None
201
+
202
+ # Use exit_code from task_info (derived from AGENT_STATUS block) instead
203
+ # of naive text matching which gives false positives on "No errors found".
204
+ exit_code = task_info.get("exit_code", 0)
205
+
206
+ # Approximate token count: 4 chars per token is a reliable heuristic for LLM output
207
+ output_tokens_approx = len(agent_output) // 4
208
+
209
+ commands_executed = commands_executed or []
210
+ context_update_result = context_update_result or {}
211
+ context_snapshot = build_context_telemetry_snapshot(
212
+ task_info.get("injected_context") or {}
213
+ )
214
+ default_skills_snapshot = load_agent_runtime_profile(task_info.get("agent", "unknown"))
215
+
216
+ metrics = {
217
+ "timestamp": session_context["timestamp"],
218
+ "session_id": session_context["session_id"],
219
+ "task_id": task_info.get("task_id", "unknown"),
220
+ "agent_id": task_info.get("agent_id", "unknown"),
221
+ "agent": task_info.get("agent", "unknown"),
222
+ "tier": task_info.get("tier", "T0"),
223
+ "duration_ms": duration_ms,
224
+ "exit_code": exit_code,
225
+ "plan_status": task_info.get("plan_status", ""),
226
+ "output_length": len(agent_output),
227
+ "output_tokens_approx": output_tokens_approx,
228
+ "tags": task_info.get("tags", []),
229
+ "prompt": task_info.get("description", ""), # Store for episodic
230
+ "commands_executed": commands_executed,
231
+ "commands_executed_count": len(commands_executed),
232
+ "context_snapshot": context_snapshot,
233
+ "context_updated": bool(context_update_result.get("updated", False)),
234
+ "context_sections_updated": context_update_result.get("sections_updated", []),
235
+ "context_rejected_sections": context_update_result.get("rejected", []),
236
+ "default_skills_snapshot": default_skills_snapshot,
237
+ "context_anchor_hits": anchor_hits,
238
+ "context_anchor_hit_rate": anchor_hits.get("hit_rate") if anchor_hits else None,
239
+ }
240
+
241
+ # --- Transcript-analysis enrichment (T010) ---
242
+ if transcript_analysis is not None:
243
+ metrics["input_tokens"] = transcript_analysis.input_tokens
244
+ metrics["cache_creation_tokens"] = transcript_analysis.cache_creation_tokens
245
+ metrics["cache_read_tokens"] = transcript_analysis.cache_read_tokens
246
+ metrics["output_tokens_real"] = transcript_analysis.output_tokens
247
+ metrics["duration_ms"] = transcript_analysis.duration_ms
248
+ metrics["tool_call_count"] = transcript_analysis.tool_call_count
249
+ metrics["skills_injected"] = transcript_analysis.skills_injected
250
+ metrics["model_used"] = transcript_analysis.model
251
+ metrics["api_call_count"] = transcript_analysis.api_call_count
252
+
253
+ # --- Compliance enrichment (T010) ---
254
+ if compliance_result is not None:
255
+ metrics["compliance_score"] = compliance_result.total
256
+ metrics["compliance_grade"] = compliance_result.grade
257
+
258
+ run_snapshot = {
259
+ "timestamp": metrics["timestamp"],
260
+ "session_id": metrics["session_id"],
261
+ "task_id": metrics["task_id"],
262
+ "agent_id": metrics["agent_id"],
263
+ "agent": metrics["agent"],
264
+ "tier": metrics["tier"],
265
+ "plan_status": metrics["plan_status"],
266
+ "context_snapshot": context_snapshot,
267
+ "context_updated": metrics["context_updated"],
268
+ "context_sections_updated": metrics["context_sections_updated"],
269
+ "context_rejected_sections": metrics["context_rejected_sections"],
270
+ "default_skills_snapshot": default_skills_snapshot,
271
+ "context_anchor_hits": anchor_hits,
272
+ "context_anchor_hit_rate": anchor_hits.get("hit_rate") if anchor_hits else None,
273
+ }
274
+
275
+ try:
276
+ workflow_memory_dir = get_workflow_memory_dir()
277
+ workflow_memory_dir.mkdir(parents=True, exist_ok=True)
278
+ _append_jsonl(workflow_memory_dir / "run-snapshots.jsonl", run_snapshot)
279
+ except Exception as exc:
280
+ logger.debug("Could not persist run telemetry snapshot: %s", exc)
281
+
282
+ # Save to workflow memory (gated behind env var; default: no write)
283
+ if os.environ.get("GAIA_WRITE_WORKFLOW_METRICS") == "1":
284
+ workflow_memory_dir = get_workflow_memory_dir()
285
+ workflow_memory_dir.mkdir(parents=True, exist_ok=True)
286
+
287
+ metrics_file = workflow_memory_dir / "metrics.jsonl"
288
+ with open(metrics_file, "a") as f:
289
+ f.write(json.dumps(metrics) + "\n")
290
+
291
+ logger.debug(
292
+ "Captured workflow metrics: %s (duration: %sms, exit: %s, commands: %s)",
293
+ metrics["agent"], duration_ms, exit_code, len(commands_executed),
294
+ )
295
+
296
+ return metrics
@@ -0,0 +1,11 @@
1
+ """
2
+ Context Management Module
3
+
4
+ This module provides tools for managing context in agent conversations:
5
+ - context_writer: Progressive enrichment of project-context.json via CONTEXT_UPDATE blocks
6
+ - contracts_loader: Load context contracts, detect cloud provider, merge agent permissions
7
+ - context_injector: Core context injection subsystem for project agents
8
+ - context_freshness: Check staleness of project-context.json for SessionStart
9
+ """
10
+
11
+ __all__ = []
@@ -0,0 +1,317 @@
1
+ """
2
+ Context anchor hit tracking for project context effectiveness measurement.
3
+
4
+ Extracts "anchors" (paths, names, IDs) from injected project context and checks
5
+ whether the agent's early tool calls reference them. This measures whether agents
6
+ use injected context as search anchors versus discovering on their own.
7
+
8
+ Provides:
9
+ - extract_anchors(): Extract searchable anchors from a context payload
10
+ - save_anchors(): Persist anchors to a session-scoped temp file
11
+ - load_anchors(): Load persisted anchors for a session
12
+ - extract_tool_calls_from_transcript(): Parse early tool calls from JSONL transcript
13
+ - compute_anchor_hits(): Compare tool call args against anchors
14
+ """
15
+
16
+ import json
17
+ import logging
18
+ import re
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional, Set
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # How many early tool calls to check
25
+ MAX_TOOL_CALLS_TO_CHECK = 5
26
+
27
+ # Tool types that have inspectable path/keyword arguments
28
+ TRACKABLE_TOOLS = {"Glob", "Grep", "Read", "Bash"}
29
+
30
+ # Minimum anchor length to avoid false-positive matches on short strings
31
+ MIN_ANCHOR_LENGTH = 4
32
+
33
+
34
+ def _anchors_dir() -> Path:
35
+ """Return the directory for anchor temp files."""
36
+ return Path("/tmp/gaia-context-anchors")
37
+
38
+
39
+ def extract_anchors(context_payload: Dict[str, Any]) -> Set[str]:
40
+ """Extract searchable anchor strings from a context payload.
41
+
42
+ Walks the project knowledge sections and collects values from fields that
43
+ are likely to appear in agent tool calls: paths, names, IDs, clusters,
44
+ regions, namespaces, service accounts.
45
+
46
+ Args:
47
+ context_payload: The full context JSON payload injected into agent prompt.
48
+
49
+ Returns:
50
+ Set of anchor strings (paths, names, identifiers).
51
+ """
52
+ anchors: Set[str] = set()
53
+ contract = context_payload.get("project_knowledge", {})
54
+
55
+ # Anchor-worthy field name patterns
56
+ anchor_fields = re.compile(
57
+ r"(path|name|cluster|project|region|namespace|service|image|"
58
+ r"base_path|config_path|module_path|repository|bucket|sa$|"
59
+ r"service_account|pod_name|terragrunt_path)",
60
+ re.IGNORECASE,
61
+ )
62
+
63
+ def _walk(obj: Any, depth: int = 0) -> None:
64
+ if depth > 10:
65
+ return
66
+ if isinstance(obj, dict):
67
+ for key, value in obj.items():
68
+ if isinstance(value, str) and value and anchor_fields.search(key):
69
+ # Normalize: strip leading ./ for path matching
70
+ clean = value.lstrip("./")
71
+ if len(clean) >= MIN_ANCHOR_LENGTH:
72
+ anchors.add(clean)
73
+ elif isinstance(value, (dict, list)):
74
+ _walk(value, depth + 1)
75
+ elif isinstance(obj, list):
76
+ for item in obj:
77
+ _walk(item, depth + 1)
78
+
79
+ _walk(contract)
80
+
81
+ # Also extract from top-level metadata
82
+ metadata = context_payload.get("metadata", {})
83
+ for key in ("project_id", "cluster_name", "region"):
84
+ val = metadata.get(key)
85
+ if isinstance(val, str) and len(val) >= MIN_ANCHOR_LENGTH:
86
+ anchors.add(val)
87
+
88
+ return anchors
89
+
90
+
91
+ def save_anchors(session_id: str, agent_type: str, anchors: Set[str]) -> Optional[Path]:
92
+ """Persist anchors to a session+agent-scoped temp file.
93
+
94
+ Args:
95
+ session_id: Current session identifier.
96
+ agent_type: Agent name (e.g. "terraform-architect").
97
+ anchors: Set of anchor strings to save.
98
+
99
+ Returns:
100
+ Path to the saved file, or None on failure.
101
+ """
102
+ if not anchors:
103
+ return None
104
+
105
+ try:
106
+ anchor_dir = _anchors_dir()
107
+ anchor_dir.mkdir(parents=True, exist_ok=True)
108
+
109
+ safe_session = re.sub(r"[^a-zA-Z0-9_-]", "_", session_id or "unknown")[:32]
110
+ safe_agent = re.sub(r"[^a-zA-Z0-9_-]", "_", agent_type or "unknown")[:32]
111
+ anchor_file = anchor_dir / f"{safe_session}-{safe_agent}.json"
112
+
113
+ anchor_file.write_text(json.dumps(sorted(anchors)))
114
+ logger.debug(
115
+ "Saved %d anchors for %s/%s -> %s",
116
+ len(anchors), session_id, agent_type, anchor_file,
117
+ )
118
+ return anchor_file
119
+ except Exception as e:
120
+ logger.debug("Failed to save anchors: %s", e)
121
+ return None
122
+
123
+
124
+ def load_anchors(session_id: str, agent_type: str) -> Set[str]:
125
+ """Load persisted anchors for a session+agent.
126
+
127
+ Args:
128
+ session_id: Current session identifier.
129
+ agent_type: Agent name.
130
+
131
+ Returns:
132
+ Set of anchor strings, or empty set if not found.
133
+ """
134
+ try:
135
+ safe_session = re.sub(r"[^a-zA-Z0-9_-]", "_", session_id or "unknown")[:32]
136
+ safe_agent = re.sub(r"[^a-zA-Z0-9_-]", "_", agent_type or "unknown")[:32]
137
+ anchor_file = _anchors_dir() / f"{safe_session}-{safe_agent}.json"
138
+
139
+ if not anchor_file.exists():
140
+ return set()
141
+
142
+ data = json.loads(anchor_file.read_text())
143
+ return set(data) if isinstance(data, list) else set()
144
+ except Exception as e:
145
+ logger.debug("Failed to load anchors: %s", e)
146
+ return set()
147
+
148
+
149
+ def extract_tool_calls_from_transcript(
150
+ transcript_path: str,
151
+ max_calls: int = MAX_TOOL_CALLS_TO_CHECK,
152
+ ) -> List[Dict[str, Any]]:
153
+ """Extract the first N trackable tool calls from a Claude Code transcript JSONL.
154
+
155
+ Claude Code transcripts contain tool_use entries in the assistant messages
156
+ (content blocks with type "tool_use").
157
+
158
+ Args:
159
+ transcript_path: Path to the transcript JSONL file.
160
+ max_calls: Maximum number of tool calls to extract.
161
+
162
+ Returns:
163
+ List of dicts with keys: tool_name, arguments (dict), call_index (1-based).
164
+ """
165
+ if not transcript_path:
166
+ return []
167
+
168
+ try:
169
+ path = Path(transcript_path).expanduser()
170
+ if not path.exists():
171
+ return []
172
+
173
+ tool_calls: List[Dict[str, Any]] = []
174
+ call_index = 0
175
+
176
+ for line in path.read_text().strip().splitlines():
177
+ if not line.strip():
178
+ continue
179
+ if call_index >= max_calls:
180
+ break
181
+
182
+ try:
183
+ entry = json.loads(line)
184
+ msg = entry.get("message", entry)
185
+
186
+ if msg.get("role") != "assistant":
187
+ continue
188
+
189
+ content = msg.get("content", [])
190
+ if not isinstance(content, list):
191
+ continue
192
+
193
+ for block in content:
194
+ if call_index >= max_calls:
195
+ break
196
+ if not isinstance(block, dict):
197
+ continue
198
+ if block.get("type") != "tool_use":
199
+ continue
200
+
201
+ tool_name = block.get("name", "")
202
+ if tool_name not in TRACKABLE_TOOLS:
203
+ continue
204
+
205
+ call_index += 1
206
+ tool_calls.append({
207
+ "tool_name": tool_name,
208
+ "arguments": block.get("input", {}),
209
+ "call_index": call_index,
210
+ })
211
+
212
+ except (json.JSONDecodeError, TypeError):
213
+ continue
214
+
215
+ return tool_calls
216
+
217
+ except Exception as e:
218
+ logger.debug("Failed to extract tool calls from transcript: %s", e)
219
+ return []
220
+
221
+
222
+ def _extract_searchable_text(tool_name: str, arguments: Dict[str, Any]) -> str:
223
+ """Extract the searchable text from a tool call's arguments.
224
+
225
+ Returns a single string containing all path/keyword-relevant arguments
226
+ concatenated for substring matching.
227
+ """
228
+ parts: List[str] = []
229
+
230
+ if tool_name == "Glob":
231
+ parts.append(arguments.get("pattern", ""))
232
+ parts.append(arguments.get("path", ""))
233
+ elif tool_name == "Grep":
234
+ parts.append(arguments.get("pattern", ""))
235
+ parts.append(arguments.get("path", ""))
236
+ parts.append(arguments.get("glob", ""))
237
+ elif tool_name == "Read":
238
+ parts.append(arguments.get("file_path", ""))
239
+ elif tool_name == "Bash":
240
+ parts.append(arguments.get("command", ""))
241
+
242
+ return " ".join(p for p in parts if p)
243
+
244
+
245
+ def compute_anchor_hits(
246
+ tool_calls: List[Dict[str, Any]],
247
+ anchors: Set[str],
248
+ ) -> Dict[str, Any]:
249
+ """Compare tool call arguments against known anchors.
250
+
251
+ For each tool call, checks if any anchor appears as a substring in the
252
+ tool's searchable arguments. This is a lightweight prefix/substring match.
253
+
254
+ Args:
255
+ tool_calls: List from extract_tool_calls_from_transcript().
256
+ anchors: Set of anchor strings from extract_anchors().
257
+
258
+ Returns:
259
+ Dict with hit tracking data.
260
+ """
261
+ if not tool_calls or not anchors:
262
+ return {
263
+ "total_checked": len(tool_calls),
264
+ "hits": 0,
265
+ "hit_rate": 0.0,
266
+ "details": [],
267
+ }
268
+
269
+ details: List[Dict[str, Any]] = []
270
+ hits = 0
271
+
272
+ for call in tool_calls:
273
+ searchable = _extract_searchable_text(call["tool_name"], call["arguments"])
274
+ matched_anchor: Optional[str] = None
275
+
276
+ if searchable:
277
+ for anchor in anchors:
278
+ if anchor in searchable:
279
+ matched_anchor = anchor
280
+ break
281
+
282
+ is_hit = matched_anchor is not None
283
+ if is_hit:
284
+ hits += 1
285
+
286
+ details.append({
287
+ "call_index": call["call_index"],
288
+ "tool": call["tool_name"],
289
+ "anchor": matched_anchor,
290
+ "hit": is_hit,
291
+ })
292
+
293
+ total = len(tool_calls)
294
+ return {
295
+ "total_checked": total,
296
+ "hits": hits,
297
+ "hit_rate": round(hits / total, 2) if total > 0 else 0.0,
298
+ "details": details,
299
+ }
300
+
301
+
302
+ def cleanup_anchors(session_id: str, agent_type: str) -> None:
303
+ """Remove the anchor temp file after use.
304
+
305
+ Args:
306
+ session_id: Current session identifier.
307
+ agent_type: Agent name.
308
+ """
309
+ try:
310
+ safe_session = re.sub(r"[^a-zA-Z0-9_-]", "_", session_id or "unknown")[:32]
311
+ safe_agent = re.sub(r"[^a-zA-Z0-9_-]", "_", agent_type or "unknown")[:32]
312
+ anchor_file = _anchors_dir() / f"{safe_session}-{safe_agent}.json"
313
+ if anchor_file.exists():
314
+ anchor_file.unlink()
315
+ logger.debug("Cleaned up anchor file: %s", anchor_file)
316
+ except Exception as e:
317
+ logger.debug("Failed to cleanup anchors: %s", e)