@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,1068 @@
1
+ """
2
+ gaia metrics -- mirror of gaia-metrics.js
3
+
4
+ Displays system metrics dashboard:
5
+ - Security tier usage distribution
6
+ - Command type breakdown
7
+ - Top commands by frequency
8
+ - Agent invocations
9
+ - Agent outcomes
10
+ - Token usage (approx)
11
+ - Anomaly summary (last 30 days)
12
+ - Activity today
13
+
14
+ With --agent NAME shows a detail view for that agent.
15
+
16
+ Data sources:
17
+ .claude/logs/audit-*.jsonl
18
+ .claude/project-context/episodic-memory/index.json
19
+ .claude/project-context/workflow-episodic-memory/metrics.jsonl (fallback)
20
+ .claude/project-context/workflow-episodic-memory/anomalies.jsonl
21
+ .claude/project-context/workflow-episodic-memory/run-snapshots.jsonl
22
+ .claude/project-context/workflow-episodic-memory/agent-skills.jsonl
23
+
24
+ Flags:
25
+ --agent NAME Show detail view for a specific agent
26
+ --json Machine-readable output
27
+ """
28
+
29
+ import fnmatch
30
+ import json
31
+ import os
32
+ import re
33
+ import sys
34
+ from datetime import datetime, timezone, timedelta
35
+ from pathlib import Path
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Project root detection
40
+ # ---------------------------------------------------------------------------
41
+
42
+ def _find_project_root() -> Path:
43
+ start = Path(os.environ.get("INIT_CWD", "")) if os.environ.get("INIT_CWD") else None
44
+ if start and (start / ".claude").exists():
45
+ return start
46
+
47
+ current = Path.cwd()
48
+ while True:
49
+ if (current / ".claude").exists():
50
+ return current
51
+ parent = current.parent
52
+ if parent == current:
53
+ break
54
+ current = parent
55
+
56
+ return Path(os.environ.get("INIT_CWD", str(Path.cwd())))
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Data readers
61
+ # ---------------------------------------------------------------------------
62
+
63
+ def _read_jsonl(path: Path) -> list:
64
+ if not path.exists():
65
+ return []
66
+ entries = []
67
+ try:
68
+ for line in path.read_text(encoding="utf-8").splitlines():
69
+ line = line.strip()
70
+ if not line:
71
+ continue
72
+ try:
73
+ entries.append(json.loads(line))
74
+ except json.JSONDecodeError:
75
+ pass
76
+ except OSError:
77
+ pass
78
+ return entries
79
+
80
+
81
+ def _read_audit_logs(root: Path) -> list:
82
+ logs_dir = root / ".claude" / "logs"
83
+ if not logs_dir.exists():
84
+ return []
85
+ all_entries = []
86
+ try:
87
+ for f in logs_dir.iterdir():
88
+ if f.name.startswith("audit-") and f.name.endswith(".jsonl"):
89
+ all_entries.extend(_read_jsonl(f))
90
+ except OSError:
91
+ pass
92
+ return all_entries
93
+
94
+
95
+ def _read_workflow_metrics(root: Path) -> list:
96
+ """Primary: episodic-memory/index.json; fallback: workflow metrics.jsonl."""
97
+ index_path = root / ".claude" / "project-context" / "episodic-memory" / "index.json"
98
+ if index_path.exists():
99
+ try:
100
+ data = json.loads(index_path.read_text(encoding="utf-8"))
101
+ episodes = [e for e in (data.get("episodes") or []) if e.get("agent")]
102
+ if episodes:
103
+ return episodes
104
+ except (json.JSONDecodeError, OSError):
105
+ pass
106
+
107
+ metrics_path = root / ".claude" / "project-context" / "workflow-episodic-memory" / "metrics.jsonl"
108
+ return [e for e in _read_jsonl(metrics_path) if e.get("agent")]
109
+
110
+
111
+ def _read_run_snapshots(root: Path) -> list:
112
+ return _read_jsonl(
113
+ root / ".claude" / "project-context" / "workflow-episodic-memory" / "run-snapshots.jsonl"
114
+ )
115
+
116
+
117
+ def _read_agent_skill_snapshots(root: Path) -> list:
118
+ return _read_jsonl(
119
+ root / ".claude" / "project-context" / "workflow-episodic-memory" / "agent-skills.jsonl"
120
+ )
121
+
122
+
123
+ def _read_anomaly_entries(root: Path) -> list:
124
+ return _read_jsonl(
125
+ root / ".claude" / "project-context" / "workflow-episodic-memory" / "anomalies.jsonl"
126
+ )
127
+
128
+
129
+ def _read_agent_definition(root: Path, agent_name: str) -> dict:
130
+ """Extract description and skills from agent .md frontmatter."""
131
+ agent_path = root / ".claude" / "agents" / f"{agent_name}.md"
132
+ if not agent_path.exists():
133
+ return {}
134
+ try:
135
+ content = agent_path.read_text(encoding="utf-8")
136
+ if not content.startswith("---"):
137
+ return {}
138
+ end = content.find("---", 3)
139
+ if end == -1:
140
+ return {}
141
+ fm = content[3:end]
142
+ description = ""
143
+ skills = []
144
+ in_skills = False
145
+ for line in fm.splitlines():
146
+ stripped = line.strip()
147
+ if stripped.startswith("description:"):
148
+ description = stripped[len("description:"):].strip().strip("'\"")
149
+ in_skills = False
150
+ elif stripped == "skills:":
151
+ in_skills = True
152
+ elif in_skills and stripped.startswith("- "):
153
+ skills.append(stripped[2:].strip())
154
+ elif in_skills and stripped and not stripped.startswith("-"):
155
+ in_skills = False
156
+ return {"description": description, "skills": skills}
157
+ except OSError:
158
+ return {}
159
+
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # Utility functions
163
+ # ---------------------------------------------------------------------------
164
+
165
+ def _classify_command(command: str) -> str:
166
+ if not command:
167
+ return "general"
168
+ cmd = command.strip().lower()
169
+ if cmd.startswith("terragrunt") or cmd.startswith("terraform"):
170
+ return "terraform"
171
+ if cmd.startswith("kubectl"):
172
+ return "kubernetes"
173
+ if cmd.startswith("helm") or cmd.startswith("flux"):
174
+ return "gitops"
175
+ if cmd.startswith("git") or cmd.startswith("glab"):
176
+ return "git"
177
+ if cmd.startswith("gcloud") or cmd.startswith("gsutil"):
178
+ return "gcp"
179
+ if cmd.startswith("aws"):
180
+ return "aws"
181
+ if cmd.startswith("docker"):
182
+ return "docker"
183
+ if cmd.startswith(("npm", "node", "python", "pip")):
184
+ return "dev"
185
+ return "general"
186
+
187
+
188
+ def _extract_command_label(command: str) -> str:
189
+ """Extract short human-readable label from full command string."""
190
+ if not command:
191
+ return "(unknown)"
192
+ cmd = command.strip()
193
+ # Strip env var assignments
194
+ cmd = re.sub(r'^(?:[A-Z_][A-Z0-9_]*=\S+\s+)+', '', cmd)
195
+ # Strip timeout wrapper
196
+ cmd = re.sub(r'^timeout\s+\S+\s+', '', cmd)
197
+ # Strip cd/pushd navigation
198
+ m = re.match(r'^(?:cd|pushd)\s+\S+\s*(?:&&|;)\s*(.*)', cmd)
199
+ if m:
200
+ cmd = m.group(1).strip()
201
+ # Strip at pipe/semicolon/&&
202
+ cmd = re.split(r'\s*(?:[|;&]|&&|\|\|)\s*', cmd)[0].strip()
203
+ # Strip trailing redirections
204
+ cmd = re.sub(r'\s*\d*>.*$', '', cmd).strip()
205
+
206
+ tokens = cmd.split()
207
+ parts = [tokens[0]] if tokens else ["(unknown)"]
208
+ for t in tokens[1:]:
209
+ if len(parts) >= 3:
210
+ break
211
+ if not t.startswith(("-", "/", '"', "'")):
212
+ parts.append(t)
213
+ return " ".join(parts)[:32]
214
+
215
+
216
+ def _format_tokens(n) -> str:
217
+ if n is None:
218
+ return "n/a"
219
+ if n >= 1_000_000:
220
+ return f"{n / 1_000_000:.1f}M"
221
+ if n >= 1_000:
222
+ return f"{n / 1_000:.1f}k"
223
+ return str(n)
224
+
225
+
226
+ def _format_chars(n) -> str:
227
+ if n is None:
228
+ return "n/a"
229
+ if n >= 1000:
230
+ return f"{n / 1000:.1f}k"
231
+ return str(n)
232
+
233
+
234
+ def _make_bar(percentage: float, max_width: int = 14) -> str:
235
+ filled = max(0, round((percentage / 100) * max_width))
236
+ return "#" * filled
237
+
238
+
239
+ def _count_values(values: list) -> dict:
240
+ counts = {}
241
+ for v in values:
242
+ if not v:
243
+ continue
244
+ counts[v] = counts.get(v, 0) + 1
245
+ return counts
246
+
247
+
248
+ def _sorted_counts(counts: dict) -> list:
249
+ return sorted(
250
+ [{"name": k, "count": v} for k, v in counts.items()],
251
+ key=lambda x: (-x["count"], x["name"]),
252
+ )
253
+
254
+
255
+ def _top_counts(values: list, limit: int = 5) -> list:
256
+ return _sorted_counts(_count_values(values))[:limit]
257
+
258
+
259
+ def _format_count_summary(entries: list, empty_label: str = "none") -> str:
260
+ if not entries:
261
+ return empty_label
262
+ return ", ".join(f"{e['name']}({e['count']})" for e in entries)
263
+
264
+
265
+ def _format_skills(skills: list, limit: int = 4) -> str:
266
+ if not skills:
267
+ return "none"
268
+ if len(skills) <= limit:
269
+ return ", ".join(skills)
270
+ return ", ".join(skills[:limit]) + f", +{len(skills) - limit} more"
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Metric calculators
275
+ # ---------------------------------------------------------------------------
276
+
277
+ def _calculate_tier_usage(audit_logs: list) -> dict:
278
+ tier_entries = [l for l in audit_logs if l.get("tier")]
279
+ counts = {}
280
+ for e in tier_entries:
281
+ t = e.get("tier", "unknown")
282
+ counts[t] = counts.get(t, 0) + 1
283
+
284
+ total = len(tier_entries)
285
+ distribution = sorted(
286
+ [{"tier": t, "count": c, "percentage": c / total * 100 if total else 0}
287
+ for t, c in counts.items()],
288
+ key=lambda x: x["tier"],
289
+ )
290
+
291
+ today = datetime.now(timezone.utc).date().isoformat()
292
+ today_entries = [l for l in audit_logs if (l.get("timestamp") or "").startswith(today)]
293
+ today_t3 = sum(1 for l in today_entries if l.get("tier") == "T3")
294
+
295
+ hour_counts = {}
296
+ for e in today_entries:
297
+ ts = e.get("timestamp")
298
+ if ts and len(ts) >= 13:
299
+ h = ts[11:13]
300
+ hour_counts[h] = hour_counts.get(h, 0) + 1
301
+
302
+ peak_hour = None
303
+ peak_count = 0
304
+ for h, c in hour_counts.items():
305
+ if c > peak_count:
306
+ peak_count = c
307
+ peak_hour = h
308
+
309
+ return {
310
+ "total": total,
311
+ "distribution": distribution,
312
+ "today_count": len(today_entries),
313
+ "today_t3": today_t3,
314
+ "peak_hour": peak_hour,
315
+ "peak_count": peak_count,
316
+ }
317
+
318
+
319
+ def _calculate_command_type_breakdown(audit_logs: list) -> dict:
320
+ counts = {}
321
+ for e in audit_logs:
322
+ t = _classify_command(e.get("command") or "")
323
+ counts[t] = counts.get(t, 0) + 1
324
+
325
+ total = len(audit_logs)
326
+ breakdown = sorted(
327
+ [{"type": t, "count": c, "percentage": c / total * 100 if total else 0}
328
+ for t, c in counts.items()],
329
+ key=lambda x: -x["count"],
330
+ )
331
+ return {"total": total, "breakdown": breakdown}
332
+
333
+
334
+ def _calculate_top_commands(audit_logs: list) -> list:
335
+ tier_order = {"T3": 3, "T2": 2, "T1": 1, "T0": 0, "unknown": -1}
336
+ label_map = {}
337
+
338
+ for e in audit_logs:
339
+ if not e.get("command"):
340
+ continue
341
+ label = _extract_command_label(e["command"])
342
+ tier = e.get("tier") or "unknown"
343
+
344
+ if label not in label_map:
345
+ label_map[label] = {"count": 0, "tier": tier, "t3count": 0}
346
+ label_map[label]["count"] += 1
347
+ if tier == "T3":
348
+ label_map[label]["t3count"] += 1
349
+ if tier_order.get(tier, -1) > tier_order.get(label_map[label]["tier"], -1):
350
+ label_map[label]["tier"] = tier
351
+
352
+ return sorted(
353
+ [{"label": l, **v} for l, v in label_map.items()],
354
+ key=lambda x: -x["count"],
355
+ )[:10]
356
+
357
+
358
+ def _calculate_error_rate(audit_logs: list) -> dict:
359
+ with_code = [l for l in audit_logs if "exit_code" in l]
360
+ errors = [l for l in with_code if l["exit_code"] != 0]
361
+ all_zero = bool(with_code) and len(errors) == 0
362
+ total = len(with_code)
363
+ return {
364
+ "total": total,
365
+ "errors": len(errors),
366
+ "error_rate": len(errors) / total * 100 if total else 0,
367
+ "limited_by_api": all_zero,
368
+ }
369
+
370
+
371
+ def _calculate_agent_invocations(workflow_metrics: list) -> dict:
372
+ today = datetime.now(timezone.utc).date().isoformat()
373
+ today_count = sum(1 for r in workflow_metrics if (r.get("timestamp") or "").startswith(today))
374
+
375
+ agent_map = {}
376
+ for e in workflow_metrics:
377
+ name = e.get("agent") or "unknown"
378
+ if name not in agent_map:
379
+ agent_map[name] = {"count": 0, "total_output": 0, "successes": 0}
380
+ agent_map[name]["count"] += 1
381
+ agent_map[name]["total_output"] += e.get("output_length") or 0
382
+ if e.get("exit_code") == 0:
383
+ agent_map[name]["successes"] += 1
384
+
385
+ total = len(workflow_metrics)
386
+ agents = sorted(
387
+ [
388
+ {
389
+ "name": n,
390
+ "count": v["count"],
391
+ "avg_output": round(v["total_output"] / v["count"]) if v["count"] else 0,
392
+ "success_rate": v["successes"] / v["count"] * 100 if v["count"] else 0,
393
+ "percentage": v["count"] / total * 100 if total else 0,
394
+ }
395
+ for n, v in agent_map.items()
396
+ ],
397
+ key=lambda x: -x["count"],
398
+ )
399
+ return {"agents": agents, "total": total, "today_count": today_count}
400
+
401
+
402
+ def _calculate_agent_outcomes(workflow_metrics: list):
403
+ with_status = [r for r in workflow_metrics if r.get("plan_status")]
404
+ if not with_status:
405
+ return None
406
+
407
+ counts = {}
408
+ for e in with_status:
409
+ s = e["plan_status"].upper()
410
+ counts[s] = counts.get(s, 0) + 1
411
+
412
+ total = len(with_status)
413
+ distribution = sorted(
414
+ [{"status": s, "count": c, "percentage": c / total * 100} for s, c in counts.items()],
415
+ key=lambda x: -x["count"],
416
+ )
417
+ return {"distribution": distribution, "total": total}
418
+
419
+
420
+ def _calculate_token_usage(workflow_metrics: list):
421
+ with_tokens = [r for r in workflow_metrics if isinstance(r.get("output_tokens_approx"), (int, float))]
422
+ if not with_tokens:
423
+ return None
424
+
425
+ agent_map = {}
426
+ for e in with_tokens:
427
+ name = e.get("agent") or "unknown"
428
+ if name not in agent_map:
429
+ agent_map[name] = {"total": 0, "count": 0}
430
+ agent_map[name]["total"] += e["output_tokens_approx"]
431
+ agent_map[name]["count"] += 1
432
+
433
+ grand_total = sum(e["output_tokens_approx"] for e in with_tokens)
434
+ agents = sorted(
435
+ [
436
+ {
437
+ "name": n,
438
+ "total": v["total"],
439
+ "avg": round(v["total"] / v["count"]) if v["count"] else 0,
440
+ "count": v["count"],
441
+ }
442
+ for n, v in agent_map.items()
443
+ ],
444
+ key=lambda x: -x["total"],
445
+ )
446
+ return {"agents": agents, "grand_total": grand_total, "entry_count": len(with_tokens)}
447
+
448
+
449
+ def _calculate_anomaly_summary(anomaly_entries: list):
450
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat()
451
+ entries = [e for e in anomaly_entries if e and (e.get("timestamp") or "") >= cutoff]
452
+ if not entries:
453
+ return None
454
+
455
+ type_counts = {}
456
+ agent_counts = {}
457
+ for e in entries:
458
+ agent = (e.get("metrics") or {}).get("agent", "unknown")
459
+ for anomaly in e.get("anomalies") or []:
460
+ t = anomaly.get("type", "unknown")
461
+ type_counts[t] = type_counts.get(t, 0) + 1
462
+ agent_counts[agent] = agent_counts.get(agent, 0) + 1
463
+
464
+ total = sum(type_counts.values())
465
+ by_type = sorted(
466
+ [{"type": t, "count": c, "percentage": c / total * 100 if total else 0}
467
+ for t, c in type_counts.items()],
468
+ key=lambda x: -x["count"],
469
+ )
470
+ by_agent = _sorted_counts(agent_counts)[:5]
471
+
472
+ return {
473
+ "total": total,
474
+ "session_count": len(entries),
475
+ "by_type": by_type,
476
+ "by_agent": by_agent,
477
+ }
478
+
479
+
480
+ def _calculate_runtime_skill_summary(skill_snapshots: list, run_snapshots: list) -> dict:
481
+ explicit = [e for e in skill_snapshots if e and e.get("agent")]
482
+ run_defaults = [
483
+ {
484
+ "timestamp": e.get("timestamp", ""),
485
+ "session_id": e.get("session_id", ""),
486
+ "agent": e.get("agent"),
487
+ "model": (e.get("default_skills_snapshot") or {}).get("model", ""),
488
+ "tools": (e.get("default_skills_snapshot") or {}).get("tools", []),
489
+ "skills": (e.get("default_skills_snapshot") or {}).get("skills", []),
490
+ "skills_count": (e.get("default_skills_snapshot") or {}).get("skills_count", 0),
491
+ "source": "run-default",
492
+ }
493
+ for e in run_snapshots
494
+ if e and e.get("agent") and e.get("default_skills_snapshot")
495
+ ]
496
+
497
+ latest_by_agent = {}
498
+ for snap in run_defaults + explicit:
499
+ agent = snap.get("agent") or "unknown"
500
+ current = latest_by_agent.get(agent)
501
+ if not current or str(snap.get("timestamp", "")) >= str(current.get("timestamp", "")):
502
+ latest_by_agent[agent] = {
503
+ "agent": agent,
504
+ "timestamp": snap.get("timestamp", ""),
505
+ "model": snap.get("model", ""),
506
+ "tools": snap.get("tools") if isinstance(snap.get("tools"), list) else [],
507
+ "skills": snap.get("skills") if isinstance(snap.get("skills"), list) else [],
508
+ "skills_count": snap.get("skills_count") if isinstance(snap.get("skills_count"), int) else len(snap.get("skills") or []),
509
+ "source": snap.get("source", "explicit"),
510
+ }
511
+
512
+ profiles = sorted(latest_by_agent.values(), key=lambda x: x["agent"])
513
+ all_skills = [s for p in profiles for s in p["skills"]]
514
+ top_skills = _top_counts(all_skills, 6)
515
+
516
+ return {
517
+ "explicit_count": len(explicit),
518
+ "run_default_count": len(run_defaults),
519
+ "agent_count": len(profiles),
520
+ "latest_profiles": profiles,
521
+ "top_skills": top_skills,
522
+ }
523
+
524
+
525
+ def _calculate_context_snapshot_summary(run_snapshots: list):
526
+ with_ctx = [e for e in run_snapshots if e and e.get("context_snapshot")]
527
+ if not with_ctx:
528
+ return None
529
+
530
+ primary_surfaces = []
531
+ contract_sections = []
532
+ writable_sections = []
533
+ multi_surface_count = 0
534
+
535
+ for e in with_ctx:
536
+ snap = e["context_snapshot"]
537
+ sr = snap.get("surface_routing") or {}
538
+ if sr.get("primary_surface"):
539
+ primary_surfaces.append(sr["primary_surface"])
540
+ if sr.get("multi_surface"):
541
+ multi_surface_count += 1
542
+ contract_sections.extend(snap.get("contract_sections") or [])
543
+ writable_sections.extend((snap.get("context_update_scope") or {}).get("writable_sections") or [])
544
+
545
+ return {
546
+ "total": len(with_ctx),
547
+ "multi_surface_count": multi_surface_count,
548
+ "primary_surfaces": _top_counts(primary_surfaces, 6),
549
+ "contract_sections": _top_counts(contract_sections, 6),
550
+ "writable_sections": _top_counts(writable_sections, 6),
551
+ }
552
+
553
+
554
+ def _calculate_context_update_summary(run_snapshots: list):
555
+ if not run_snapshots:
556
+ return None
557
+
558
+ updated = [e for e in run_snapshots if e.get("context_updated")]
559
+ rejected = [e for e in run_snapshots if e.get("context_rejected_sections")]
560
+
561
+ updated_sections = [s for e in updated for s in (e.get("context_sections_updated") or [])]
562
+ rejected_sections = [s for e in run_snapshots for s in (e.get("context_rejected_sections") or [])]
563
+
564
+ return {
565
+ "total_runs": len(run_snapshots),
566
+ "updated_runs": len(updated),
567
+ "rejected_runs": len(rejected),
568
+ "updated_sections": _top_counts(updated_sections, 6),
569
+ "rejected_sections": _top_counts(rejected_sections, 6),
570
+ }
571
+
572
+
573
+ # ---------------------------------------------------------------------------
574
+ # Display functions
575
+ # ---------------------------------------------------------------------------
576
+
577
+ def _display_metrics(data: dict):
578
+ SEP = "=" * 52
579
+
580
+ print("\nGaia-Ops System Metrics")
581
+ print(SEP)
582
+
583
+ tiers = data["tiers"]
584
+ cmd_types = data["cmd_types"]
585
+ top_cmds = data["top_cmds"]
586
+ agent_inv = data["agent_invocations"]
587
+ error_stats = data["error_stats"]
588
+ audit_total = data["audit_total"]
589
+ agent_outcomes = data["agent_outcomes"]
590
+ token_usage = data["token_usage"]
591
+ anomaly_summary = data["anomaly_summary"]
592
+ runtime_skills = data["runtime_skills"]
593
+ ctx_snapshots = data["context_snapshots"]
594
+ ctx_updates = data["context_updates"]
595
+
596
+ # Security Tier Usage
597
+ print(f"\nSecurity Tier Usage ({tiers['total']} operations)")
598
+ tier_label = {"T0": "read-only", "T1": "validation", "T2": "simulation", "T3": "realization"}
599
+ if tiers["total"] == 0:
600
+ print(" no tier data")
601
+ else:
602
+ for item in tiers["distribution"]:
603
+ tier = item["tier"]
604
+ count = item["count"]
605
+ pct = item["percentage"]
606
+ bar = _make_bar(pct, 14)
607
+ label = tier_label.get(tier, tier)
608
+ suffix = " realization (!)" if tier == "T3" else f" {label}"
609
+ print(f" {tier:<4} {count:>4} {bar:<14} {pct:>5.1f}%{suffix}")
610
+
611
+ # Command Type Breakdown
612
+ print(f"\nCommand Type Breakdown (derived from {audit_total} audit entries)")
613
+ if not cmd_types["breakdown"]:
614
+ print(" no command data")
615
+ else:
616
+ for item in cmd_types["breakdown"]:
617
+ bar = _make_bar(item["percentage"], 10)
618
+ print(f" {item['type']:<12} {item['count']:>4} {bar:<10} {item['percentage']:>5.1f}%")
619
+
620
+ # Top Commands
621
+ print("\nTop Commands")
622
+ if not top_cmds:
623
+ print(" no command data")
624
+ else:
625
+ for item in top_cmds:
626
+ warn = " (!)" if item["t3count"] > 0 else ""
627
+ print(f" {item['label']:<30} {item['count']:>4} {item['tier']}{warn}")
628
+
629
+ # Agent Invocations
630
+ if agent_inv["today_count"] > 0:
631
+ agent_header = f"({agent_inv['today_count']} sessions today)"
632
+ else:
633
+ agent_header = f"({agent_inv['total']} total)"
634
+ print(f"\nAgent Invocations {agent_header}")
635
+ if not agent_inv["agents"]:
636
+ print(" no invocation data")
637
+ else:
638
+ for item in agent_inv["agents"]:
639
+ bar = _make_bar(item["percentage"], 8)
640
+ avg = f"avg {_format_chars(item['avg_output']):>6} chars"
641
+ ok_pct = item["success_rate"]
642
+ ok_str = f"{ok_pct:.0f}% ok"
643
+ print(f" {item['name']:<24} {item['count']:>3} {bar:<8} {avg} {ok_str}")
644
+ print(" tip: gaia metrics --agent <name> for detail view")
645
+
646
+ # Agent Outcomes
647
+ if agent_outcomes:
648
+ print(f"\nAgent Outcomes ({agent_outcomes['total']} sessions with status)")
649
+ for item in agent_outcomes["distribution"]:
650
+ bar = _make_bar(item["percentage"], 10)
651
+ print(f" {item['status']:<16} {item['count']:>3} {bar:<10} {item['percentage']:>5.1f}%")
652
+
653
+ # Token Usage
654
+ if token_usage:
655
+ print(f"\nToken Usage (approx) total: ~{_format_tokens(token_usage['grand_total'])}")
656
+ for item in token_usage["agents"]:
657
+ print(
658
+ f" {item['name']:<24} {item['count']:>3} sessions"
659
+ f" total {_format_tokens(item['total']):>6}"
660
+ f" avg {_format_tokens(item['avg']):>6}"
661
+ )
662
+
663
+ # Runtime Skill Snapshots
664
+ if runtime_skills and runtime_skills["agent_count"] > 0:
665
+ rs = runtime_skills
666
+ print(
667
+ f"\nRuntime Skill Snapshots ({rs['agent_count']} agents, "
668
+ f"{rs['explicit_count']} explicit, {rs['run_default_count']} run defaults)"
669
+ )
670
+ for profile in rs["latest_profiles"][:6]:
671
+ model = profile.get("model") or "default"
672
+ print(
673
+ f" {profile['agent']:<24} model {model:<8} "
674
+ f"skills {profile['skills_count']:>2} tools {len(profile['tools']):>2} "
675
+ f"{_format_skills(profile['skills'], 3)}"
676
+ )
677
+ if len(rs["latest_profiles"]) > 6:
678
+ print(f" ... {len(rs['latest_profiles']) - 6} more agents with captured snapshots")
679
+ print(f" Common skills: {_format_count_summary(rs['top_skills'])}")
680
+
681
+ # Context Snapshot Summary
682
+ if ctx_snapshots:
683
+ print(f"\nContext Snapshot Summary ({ctx_snapshots['total']} sessions)")
684
+ print(f" Primary surfaces: {_format_count_summary(ctx_snapshots['primary_surfaces'])}")
685
+ print(f" Multi-surface: {ctx_snapshots['multi_surface_count']}/{ctx_snapshots['total']} sessions")
686
+ print(f" Contract sections: {_format_count_summary(ctx_snapshots['contract_sections'])}")
687
+ if ctx_snapshots["writable_sections"]:
688
+ print(f" Writable scope: {_format_count_summary(ctx_snapshots['writable_sections'])}")
689
+
690
+ # Context Updates
691
+ if ctx_updates:
692
+ print(f"\nContext Updates ({ctx_updates['updated_runs']}/{ctx_updates['total_runs']} sessions updated)")
693
+ print(f" Rejected writes: {ctx_updates['rejected_runs']} sessions")
694
+ print(f" Updated sections: {_format_count_summary(ctx_updates['updated_sections'])}")
695
+ if ctx_updates["rejected_sections"]:
696
+ print(f" Rejected sections: {_format_count_summary(ctx_updates['rejected_sections'])}")
697
+
698
+ # Anomaly Summary
699
+ if anomaly_summary and anomaly_summary["total"] > 0:
700
+ a = anomaly_summary
701
+ print(f"\nAnomaly Summary (last 30 days) {a['total']} anomalies across {a['session_count']} sessions")
702
+ for item in a["by_type"]:
703
+ bar = _make_bar(item["percentage"], 10)
704
+ print(f" {item['type']:<28} {item['count']:>3} {bar:<10} {item['percentage']:>5.1f}%")
705
+ if a["by_agent"]:
706
+ print(f" Agents: {_format_count_summary(a['by_agent'])}")
707
+
708
+ # Activity Today
709
+ print("\nActivity Today")
710
+ print(f" Total calls: {tiers['today_count']}")
711
+ print(f" T3 operations: {tiers['today_t3']}" + (" (!)" if tiers["today_t3"] > 0 else ""))
712
+ if tiers["peak_hour"] is not None:
713
+ print(f" Peak hour: {tiers['peak_hour']}:00-{tiers['peak_hour']}:59 ({tiers['peak_count']} calls)")
714
+ else:
715
+ print(" Peak hour: no data for today")
716
+
717
+ if error_stats["limited_by_api"]:
718
+ print(" Error rate: n/a (hook API limitation -- exit_code always 0)")
719
+ elif error_stats["total"] == 0:
720
+ print(" Error rate: no exit_code data")
721
+ else:
722
+ print(f" Error rate: {error_stats['errors']}/{error_stats['total']} ({error_stats['error_rate']:.1f}%)")
723
+
724
+ print("\n" + SEP)
725
+ print("Source: .claude/logs/audit-*.jsonl | episodic-memory/index.json | workflow-episodic-memory/*.jsonl\n")
726
+
727
+
728
+ def _display_agent_detail(root: Path, agent_name: str, data: dict):
729
+ SEP = "=" * 52
730
+ wm = data["workflow_metrics"]
731
+ audit_logs = data["audit_logs"]
732
+ run_snapshots = data["run_snapshots"]
733
+ skill_snapshots = data["skill_snapshots"]
734
+ anomaly_entries = data["anomaly_entries"]
735
+
736
+ print(f"\nAgent: {agent_name}")
737
+ print(SEP)
738
+
739
+ # Profile
740
+ print("\nProfile")
741
+ agent_def = _read_agent_definition(root, agent_name)
742
+ if not agent_def:
743
+ print(" Agent definition not found in .claude/agents/")
744
+ else:
745
+ if agent_def.get("description"):
746
+ print(f" Description: {agent_def['description']}")
747
+ if agent_def.get("skills"):
748
+ skills_str = ", ".join(agent_def["skills"])
749
+ if len(skills_str) <= 60:
750
+ print(f" Skills: {skills_str}")
751
+ else:
752
+ # Wrap skills at ~60 chars
753
+ chunks = []
754
+ current = []
755
+ length = 0
756
+ for s in agent_def["skills"]:
757
+ if length + len(s) + 2 > 56 and current:
758
+ chunks.append(", ".join(current))
759
+ current = [s]
760
+ length = len(s)
761
+ else:
762
+ current.append(s)
763
+ length += len(s) + 2
764
+ if current:
765
+ chunks.append(", ".join(current))
766
+ print(f" Skills: {chunks[0]}")
767
+ for chunk in chunks[1:]:
768
+ print(f" {chunk}")
769
+
770
+ # Runtime Snapshot (latest profile for this agent)
771
+ print("\nRuntime Snapshot")
772
+ # Find latest snapshot
773
+ explicit = [e for e in skill_snapshots if e.get("agent") == agent_name]
774
+ run_defaults = [e for e in run_snapshots if e.get("agent") == agent_name and e.get("default_skills_snapshot")]
775
+ all_snaps = sorted(
776
+ [{"ts": e.get("timestamp", ""), "source": "explicit", **e} for e in explicit]
777
+ + [{"ts": e.get("timestamp", ""), "source": "run-default", "model": (e.get("default_skills_snapshot") or {}).get("model", ""), "tools": (e.get("default_skills_snapshot") or {}).get("tools", []), "skills": (e.get("default_skills_snapshot") or {}).get("skills", []), "skills_count": (e.get("default_skills_snapshot") or {}).get("skills_count", 0)} for e in run_defaults],
778
+ key=lambda x: x["ts"],
779
+ reverse=True,
780
+ )
781
+ if not all_snaps:
782
+ print(" no runtime skill snapshot data")
783
+ else:
784
+ latest = all_snaps[0]
785
+ print(f" Latest model: {latest.get('model') or 'default'}")
786
+ src_label = "agent-skills.jsonl" if latest.get("source") == "explicit" else "run-snapshots default profile"
787
+ print(f" Snapshot source: {src_label}")
788
+ print(f" Snapshots seen: {len(explicit)} explicit, {len(run_defaults)} run defaults")
789
+ tools = latest.get("tools") or []
790
+ print(f" Tools: {', '.join(tools) if tools else 'none'}")
791
+ skills = latest.get("skills") or []
792
+ print(f" Skills: {_format_skills(skills, 6)}")
793
+
794
+ # Invocation History
795
+ agent_sessions = sorted(
796
+ [r for r in wm if r.get("agent") == agent_name],
797
+ key=lambda r: r.get("timestamp") or "",
798
+ )
799
+ success_count = sum(1 for r in agent_sessions if r.get("exit_code") == 0)
800
+ total_output = sum(r.get("output_length") or 0 for r in agent_sessions)
801
+ avg_output = round(total_output / len(agent_sessions)) if agent_sessions else 0
802
+
803
+ print("\nInvocation History (last 7 days)")
804
+ if not agent_sessions:
805
+ print(" no invocations found in episodic-memory/index.json")
806
+ else:
807
+ print(
808
+ f" Total: {len(agent_sessions)} invocations | "
809
+ f"Success: {success_count}/{len(agent_sessions)} | "
810
+ f"Avg output: {_format_chars(avg_output)} chars"
811
+ )
812
+ print()
813
+ for session in agent_sessions:
814
+ dt = (session.get("timestamp") or "")[:16].replace("T", " ")
815
+ ok = "ok" if session.get("exit_code") == 0 else "!!"
816
+ chars = f"{session.get('output_length') or 0:,}"
817
+ task_short = (session.get("task_id") or "n/a")[:8]
818
+ print(f" {dt} {ok} {chars:>7} chars task: {task_short}")
819
+
820
+ # Context Snapshot Summary
821
+ agent_run_snaps = [e for e in run_snapshots if e.get("agent") == agent_name]
822
+ agent_ctx = _calculate_context_snapshot_summary(agent_run_snaps)
823
+ agent_ctx_updates = _calculate_context_update_summary(agent_run_snaps)
824
+
825
+ print("\nContext Snapshot Summary")
826
+ if not agent_ctx:
827
+ print(" no context snapshot data")
828
+ else:
829
+ print(f" Sessions with context: {agent_ctx['total']}")
830
+ print(f" Primary surfaces: {_format_count_summary(agent_ctx['primary_surfaces'])}")
831
+ print(f" Multi-surface: {agent_ctx['multi_surface_count']}/{agent_ctx['total']}")
832
+ print(f" Contract sections: {_format_count_summary(agent_ctx['contract_sections'])}")
833
+ if agent_ctx["writable_sections"]:
834
+ print(f" Writable scope: {_format_count_summary(agent_ctx['writable_sections'])}")
835
+
836
+ # Context Updates + Anomalies
837
+ agent_anomalies_entries = [e for e in anomaly_entries if (e.get("metrics") or {}).get("agent") == agent_name]
838
+ agent_anomaly_type_counts = {}
839
+ for e in agent_anomalies_entries:
840
+ for anomaly in e.get("anomalies") or []:
841
+ t = anomaly.get("type", "unknown")
842
+ agent_anomaly_type_counts[t] = agent_anomaly_type_counts.get(t, 0) + 1
843
+ agent_anomaly_total = sum(agent_anomaly_type_counts.values())
844
+ agent_anomaly_by_type = _sorted_counts(agent_anomaly_type_counts)[:6]
845
+
846
+ print("\nContext Updates + Anomalies")
847
+ if not agent_ctx_updates and not agent_anomaly_total:
848
+ print(" no context update or anomaly data")
849
+ else:
850
+ if agent_ctx_updates:
851
+ print(f" Context updated: {agent_ctx_updates['updated_runs']}/{agent_ctx_updates['total_runs']} sessions")
852
+ print(f" Updated sections: {_format_count_summary(agent_ctx_updates['updated_sections'])}")
853
+ if agent_ctx_updates["rejected_sections"]:
854
+ print(f" Rejected sections: {_format_count_summary(agent_ctx_updates['rejected_sections'])}")
855
+ if agent_anomaly_total:
856
+ print(f" Anomalies: {agent_anomaly_total} across {len(agent_anomalies_entries)} sessions")
857
+ print(f" Types: {_format_count_summary(agent_anomaly_by_type)}")
858
+
859
+ # Top Commands (correlated from audit log -- approximate)
860
+ print("\nTop Commands (sampled from audit log, approximate time windows)")
861
+ if not agent_sessions or not audit_logs:
862
+ print(" no data to correlate")
863
+ else:
864
+ named_stops = sorted([r for r in wm if r.get("agent")], key=lambda r: r.get("timestamp") or "")
865
+ tier_order = {"T3": 3, "T2": 2, "T1": 1, "T0": 0, "unknown": -1}
866
+ label_map = {}
867
+
868
+ for i, session in enumerate(agent_sessions):
869
+ # Find this session's position in named_stops
870
+ stop_idx = next(
871
+ (j for j, r in enumerate(named_stops) if r.get("task_id") == session.get("task_id")),
872
+ -1,
873
+ )
874
+ prev_stop = named_stops[stop_idx - 1] if stop_idx > 0 else None
875
+ window_start = (prev_stop or {}).get("timestamp")
876
+ window_end = session.get("timestamp")
877
+
878
+ if not window_end:
879
+ continue
880
+
881
+ end_ts = _parse_ts(window_end)
882
+ start_ts = _parse_ts(window_start) if window_start else end_ts - 600
883
+
884
+ for e in audit_logs:
885
+ if not e.get("command") or not e.get("timestamp"):
886
+ continue
887
+ ts = _parse_ts(e["timestamp"])
888
+ if start_ts <= ts <= end_ts:
889
+ label = _extract_command_label(e["command"])
890
+ tier = e.get("tier") or "unknown"
891
+ if label not in label_map:
892
+ label_map[label] = {"count": 0, "tier": tier, "t3count": 0}
893
+ label_map[label]["count"] += 1
894
+ if tier == "T3":
895
+ label_map[label]["t3count"] += 1
896
+ if tier_order.get(tier, -1) > tier_order.get(label_map[label]["tier"], -1):
897
+ label_map[label]["tier"] = tier
898
+
899
+ top = sorted(
900
+ [{"label": l, **v} for l, v in label_map.items()],
901
+ key=lambda x: -x["count"],
902
+ )[:10]
903
+
904
+ if not top:
905
+ print(" no overlapping commands found in audit window")
906
+ else:
907
+ for item in top:
908
+ warn = " (!)" if item["t3count"] > 0 else ""
909
+ print(f" {item['tier']:<3} {item['label']:<28} {item['count']:>4}{warn}")
910
+ print("\n Note: command windows are approximated from SubagentStop timestamps")
911
+
912
+ print("\n" + SEP + "\n")
913
+
914
+
915
+ def _parse_ts(ts_str: str) -> float:
916
+ """Parse ISO timestamp to Unix seconds."""
917
+ try:
918
+ return datetime.fromisoformat(ts_str.replace("Z", "+00:00")).timestamp()
919
+ except (ValueError, AttributeError):
920
+ return 0.0
921
+
922
+
923
+ # ---------------------------------------------------------------------------
924
+ # Plugin interface
925
+ # ---------------------------------------------------------------------------
926
+
927
+ def register(subparsers):
928
+ """Register the 'metrics' subcommand."""
929
+ p = subparsers.add_parser(
930
+ "metrics",
931
+ help="Show system metrics dashboard (tiers, commands, agents, anomalies)",
932
+ description=(
933
+ "Display Gaia-Ops system metrics dashboard.\n"
934
+ "\n"
935
+ "Data sources:\n"
936
+ " .claude/logs/audit-*.jsonl\n"
937
+ " .claude/project-context/episodic-memory/index.json\n"
938
+ " .claude/project-context/workflow-episodic-memory/\n"
939
+ ),
940
+ )
941
+ p.add_argument(
942
+ "--agent",
943
+ metavar="NAME",
944
+ default=None,
945
+ help="Show detail view for a specific agent",
946
+ )
947
+ p.add_argument(
948
+ "--json",
949
+ action="store_true",
950
+ default=False,
951
+ help="Output results as JSON",
952
+ )
953
+ return p
954
+
955
+
956
+ def cmd_metrics(args) -> int:
957
+ """Execute the metrics subcommand."""
958
+ root = _find_project_root()
959
+ claude_dir = root / ".claude"
960
+ agent_name = getattr(args, "agent", None)
961
+ as_json = getattr(args, "json", False)
962
+
963
+ if not claude_dir.exists():
964
+ if as_json:
965
+ print(json.dumps({"error": "gaia-ops not installed in this directory"}))
966
+ else:
967
+ print("\nGaia-ops not installed in this directory")
968
+ print("Run: npx gaia-scan\n")
969
+ return 1
970
+
971
+ audit_logs = _read_audit_logs(root)
972
+ workflow_metrics = _read_workflow_metrics(root)
973
+ run_snapshots = _read_run_snapshots(root)
974
+ skill_snapshots = _read_agent_skill_snapshots(root)
975
+ anomaly_entries = _read_anomaly_entries(root)
976
+
977
+ if not audit_logs and not workflow_metrics and not run_snapshots and not skill_snapshots and not anomaly_entries:
978
+ if as_json:
979
+ empty_output = {
980
+ "security_tiers": {"total": 0, "distribution": [], "today_count": 0, "today_t3": 0, "peak_hour": None, "peak_count": 0},
981
+ "cmd_types": {"total": 0, "breakdown": []},
982
+ "top_cmds": [],
983
+ "agent_invocations": {"agents": [], "total": 0, "today_count": 0},
984
+ "error_stats": {"total": 0, "errors": 0, "error_rate": 0, "limited_by_api": False},
985
+ "agent_outcomes": None,
986
+ "token_usage": None,
987
+ "anomaly_summary": None,
988
+ "runtime_skills": {"explicit_count": 0, "run_default_count": 0, "agent_count": 0, "latest_profiles": [], "top_skills": []},
989
+ "context_snapshots": None,
990
+ "context_updates": None,
991
+ }
992
+ print(json.dumps(empty_output))
993
+ else:
994
+ print("\nNo metrics data available yet")
995
+ print("Metrics will be generated as you use the system\n")
996
+ return 0
997
+
998
+ if as_json:
999
+ # Compute all metrics and return as JSON
1000
+ tiers = _calculate_tier_usage(audit_logs)
1001
+ cmd_types = _calculate_command_type_breakdown(audit_logs)
1002
+ top_cmds = _calculate_top_commands(audit_logs)
1003
+ agent_inv = _calculate_agent_invocations(workflow_metrics)
1004
+ error_stats = _calculate_error_rate(audit_logs)
1005
+ agent_outcomes = _calculate_agent_outcomes(workflow_metrics)
1006
+ token_usage = _calculate_token_usage(workflow_metrics)
1007
+ anomaly_summary = _calculate_anomaly_summary(anomaly_entries)
1008
+ runtime_skills = _calculate_runtime_skill_summary(skill_snapshots, run_snapshots)
1009
+ ctx_snapshots = _calculate_context_snapshot_summary(run_snapshots)
1010
+ ctx_updates = _calculate_context_update_summary(run_snapshots)
1011
+
1012
+ output = {
1013
+ "security_tiers": tiers,
1014
+ "cmd_types": cmd_types,
1015
+ "top_cmds": top_cmds,
1016
+ "agent_invocations": agent_inv,
1017
+ "error_stats": error_stats,
1018
+ "agent_outcomes": agent_outcomes,
1019
+ "token_usage": token_usage,
1020
+ "anomaly_summary": anomaly_summary,
1021
+ "runtime_skills": runtime_skills,
1022
+ "context_snapshots": ctx_snapshots,
1023
+ "context_updates": ctx_updates,
1024
+ }
1025
+ if agent_name:
1026
+ output["agent_filter"] = agent_name
1027
+ print(json.dumps(output, indent=2))
1028
+ return 0
1029
+
1030
+ data = {
1031
+ "workflow_metrics": workflow_metrics,
1032
+ "audit_logs": audit_logs,
1033
+ "run_snapshots": run_snapshots,
1034
+ "skill_snapshots": skill_snapshots,
1035
+ "anomaly_entries": anomaly_entries,
1036
+ }
1037
+
1038
+ if agent_name:
1039
+ _display_agent_detail(root, agent_name, data)
1040
+ else:
1041
+ tiers = _calculate_tier_usage(audit_logs)
1042
+ cmd_types = _calculate_command_type_breakdown(audit_logs)
1043
+ top_cmds = _calculate_top_commands(audit_logs)
1044
+ agent_inv = _calculate_agent_invocations(workflow_metrics)
1045
+ error_stats = _calculate_error_rate(audit_logs)
1046
+ agent_outcomes = _calculate_agent_outcomes(workflow_metrics)
1047
+ token_usage = _calculate_token_usage(workflow_metrics)
1048
+ anomaly_summary = _calculate_anomaly_summary(anomaly_entries)
1049
+ runtime_skills = _calculate_runtime_skill_summary(skill_snapshots, run_snapshots)
1050
+ ctx_snapshots = _calculate_context_snapshot_summary(run_snapshots)
1051
+ ctx_updates = _calculate_context_update_summary(run_snapshots)
1052
+
1053
+ _display_metrics({
1054
+ "tiers": tiers,
1055
+ "cmd_types": cmd_types,
1056
+ "top_cmds": top_cmds,
1057
+ "agent_invocations": agent_inv,
1058
+ "error_stats": error_stats,
1059
+ "audit_total": len(audit_logs),
1060
+ "agent_outcomes": agent_outcomes,
1061
+ "token_usage": token_usage,
1062
+ "anomaly_summary": anomaly_summary,
1063
+ "runtime_skills": runtime_skills,
1064
+ "context_snapshots": ctx_snapshots,
1065
+ "context_updates": ctx_updates,
1066
+ })
1067
+
1068
+ return 0